mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 04:20:12 +02:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b95f47a91 | |||
| 2267e9bc9d | |||
| 089311c673 | |||
| 773f349b66 | |||
| 3980c8ea97 | |||
| 257a5ceef0 | |||
| fb022891fe | |||
| 5d9906b8f2 | |||
| 6f7cb468b2 | |||
| 076693e969 | |||
| 781d8055b5 | |||
| 960bb5c660 | |||
| 42bb2bf66f | |||
| f03d88cd8c | |||
| 58f6535ba6 | |||
| 9a59ce3613 | |||
| 6f37e13611 | |||
| 3c494f1c72 | |||
| ec0e7256cb | |||
| 262203b62d | |||
| 41bdc1a7b7 | |||
| d35e73792f | |||
| 4a3604b1a8 | |||
| b9611589ba | |||
| 12c517f0ff | |||
| 01884ab656 | |||
| 0ba830d5d7 | |||
| b08a0d178c | |||
| 9afa64b537 | |||
| be8bc74ab5 | |||
| 2f4e228fa1 | |||
| 35ee7e4606 | |||
| b265f2817b | |||
| 48af447838 | |||
| 397df0c9c6 | |||
| 68759a2613 | |||
| c376293f2f | |||
| e84a4b20bc | |||
| 14e9f6ac41 | |||
| 0115ecb59b | |||
| 1555b827ee |
Generated
+12375
-12685
File diff suppressed because it is too large
Load Diff
+8
-4
@@ -2,7 +2,7 @@
|
|||||||
"name": "feishin",
|
"name": "feishin",
|
||||||
"productName": "Feishin",
|
"productName": "Feishin",
|
||||||
"description": "Feishin music server",
|
"description": "Feishin music server",
|
||||||
"version": "0.12.3",
|
"version": "0.12.7",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "concurrently \"npm run build:main\" \"npm run build:renderer\" \"npm run build:remote\"",
|
"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",
|
"build:main": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.main.prod.ts",
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
"package.json"
|
"package.json"
|
||||||
],
|
],
|
||||||
"afterSign": ".erb/scripts/notarize.js",
|
"afterSign": ".erb/scripts/notarize.js",
|
||||||
"electronVersion": "31.2.0",
|
"electronVersion": "36.1.0",
|
||||||
"mac": {
|
"mac": {
|
||||||
"target": {
|
"target": {
|
||||||
"target": "default",
|
"target": "default",
|
||||||
@@ -234,7 +234,7 @@
|
|||||||
"css-loader": "^6.7.1",
|
"css-loader": "^6.7.1",
|
||||||
"css-minimizer-webpack-plugin": "^3.4.1",
|
"css-minimizer-webpack-plugin": "^3.4.1",
|
||||||
"detect-port": "^1.3.0",
|
"detect-port": "^1.3.0",
|
||||||
"electron": "^33.3.1",
|
"electron": "^36.1.0",
|
||||||
"electron-builder": "^24.13.3",
|
"electron-builder": "^24.13.3",
|
||||||
"electron-devtools-installer": "^3.2.0",
|
"electron-devtools-installer": "^3.2.0",
|
||||||
"electron-notarize": "^1.2.1",
|
"electron-notarize": "^1.2.1",
|
||||||
@@ -360,7 +360,11 @@
|
|||||||
"zustand": "^4.3.9"
|
"zustand": "^4.3.9"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"styled-components": "^6"
|
"styled-components": "^6",
|
||||||
|
"entities": "2.2.0"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"entities": "2.2.0"
|
||||||
},
|
},
|
||||||
"devEngines": {
|
"devEngines": {
|
||||||
"runtime": {
|
"runtime": {
|
||||||
|
|||||||
Generated
+37
-33
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "feishin",
|
"name": "feishin",
|
||||||
"version": "0.12.2",
|
"version": "0.12.7",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "feishin",
|
"name": "feishin",
|
||||||
"version": "0.12.2",
|
"version": "0.12.7",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
"ws": "^8.18.0"
|
"ws": "^8.18.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"electron": "31.1.0"
|
"electron": "36.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@electron/get": {
|
"node_modules/@electron/get": {
|
||||||
@@ -99,12 +99,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.14.9",
|
"version": "22.15.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.12.tgz",
|
||||||
"integrity": "sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==",
|
"integrity": "sha512-K0fpC/ZVeb8G9rm7bH7vI0KAec4XHEhBam616nVJCV51bKzJ6oA3luG4WdKoaztxe70QaNjS/xBmcDLmr4PiGw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~5.26.4"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/responselike": {
|
"node_modules/@types/responselike": {
|
||||||
@@ -456,14 +457,15 @@
|
|||||||
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="
|
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="
|
||||||
},
|
},
|
||||||
"node_modules/electron": {
|
"node_modules/electron": {
|
||||||
"version": "31.1.0",
|
"version": "36.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/electron/-/electron-31.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/electron/-/electron-36.1.0.tgz",
|
||||||
"integrity": "sha512-TBOwqLxSxnx6+pH6GMri7R3JPH2AkuGJHfWZS0p1HsmN+Qr1T9b0IRJnnehSd/3NZAmAre4ft9Ljec7zjyKFJA==",
|
"integrity": "sha512-gnp3BnbKdGsVc7cm1qlEaZc8pJsR08mIs8H/yTo8gHEtFkGGJbDTVZOYNAfbQlL0aXh+ozv+CnyiNeDNkT1Upg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electron/get": "^2.0.0",
|
"@electron/get": "^2.0.0",
|
||||||
"@types/node": "^20.9.0",
|
"@types/node": "^22.7.7",
|
||||||
"extract-zip": "^2.0.1"
|
"extract-zip": "^2.0.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -1274,10 +1276,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "5.26.5",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/universalify": {
|
"node_modules/universalify": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
@@ -1315,9 +1318,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/xml2js": {
|
"node_modules/xml2js": {
|
||||||
"version": "0.4.23",
|
"version": "0.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
|
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",
|
||||||
"integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
|
"integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"sax": ">=0.6.0",
|
"sax": ">=0.6.0",
|
||||||
"xmlbuilder": "~11.0.0"
|
"xmlbuilder": "~11.0.0"
|
||||||
@@ -1417,12 +1421,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "20.14.9",
|
"version": "22.15.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.12.tgz",
|
||||||
"integrity": "sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==",
|
"integrity": "sha512-K0fpC/ZVeb8G9rm7bH7vI0KAec4XHEhBam616nVJCV51bKzJ6oA3luG4WdKoaztxe70QaNjS/xBmcDLmr4PiGw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"undici-types": "~5.26.4"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/responselike": {
|
"@types/responselike": {
|
||||||
@@ -1581,7 +1585,7 @@
|
|||||||
"jsbi": "^2.0.5",
|
"jsbi": "^2.0.5",
|
||||||
"long": "^4.0.0",
|
"long": "^4.0.0",
|
||||||
"safe-buffer": "^5.1.1",
|
"safe-buffer": "^5.1.1",
|
||||||
"xml2js": "^0.4.17"
|
"xml2js": "0.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"debug": {
|
"debug": {
|
||||||
@@ -1684,13 +1688,13 @@
|
|||||||
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="
|
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="
|
||||||
},
|
},
|
||||||
"electron": {
|
"electron": {
|
||||||
"version": "31.1.0",
|
"version": "36.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/electron/-/electron-31.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/electron/-/electron-36.1.0.tgz",
|
||||||
"integrity": "sha512-TBOwqLxSxnx6+pH6GMri7R3JPH2AkuGJHfWZS0p1HsmN+Qr1T9b0IRJnnehSd/3NZAmAre4ft9Ljec7zjyKFJA==",
|
"integrity": "sha512-gnp3BnbKdGsVc7cm1qlEaZc8pJsR08mIs8H/yTo8gHEtFkGGJbDTVZOYNAfbQlL0aXh+ozv+CnyiNeDNkT1Upg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@electron/get": "^2.0.0",
|
"@electron/get": "^2.0.0",
|
||||||
"@types/node": "^20.9.0",
|
"@types/node": "^22.7.7",
|
||||||
"extract-zip": "^2.0.1"
|
"extract-zip": "^2.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2291,9 +2295,9 @@
|
|||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"undici-types": {
|
"undici-types": {
|
||||||
"version": "5.26.5",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"universalify": {
|
"universalify": {
|
||||||
@@ -2314,9 +2318,9 @@
|
|||||||
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="
|
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="
|
||||||
},
|
},
|
||||||
"xml2js": {
|
"xml2js": {
|
||||||
"version": "0.4.23",
|
"version": "0.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
|
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",
|
||||||
"integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
|
"integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"sax": ">=0.6.0",
|
"sax": ">=0.6.0",
|
||||||
"xmlbuilder": "~11.0.0"
|
"xmlbuilder": "~11.0.0"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "feishin",
|
"name": "feishin",
|
||||||
"version": "0.12.3",
|
"version": "0.12.7",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "./dist/main/main.js",
|
"main": "./dist/main/main.js",
|
||||||
"author": {
|
"author": {
|
||||||
@@ -18,7 +18,13 @@
|
|||||||
"ws": "^8.18.0"
|
"ws": "^8.18.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"electron": "31.1.0"
|
"electron": "36.1.0"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"xml2js": "0.5.0"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"xml2js": "0.5.0"
|
||||||
},
|
},
|
||||||
"license": "GPL-3.0"
|
"license": "GPL-3.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -747,8 +747,8 @@
|
|||||||
"folderWithCount_few": "{{count}} složky",
|
"folderWithCount_few": "{{count}} složky",
|
||||||
"folderWithCount_other": "{{count}} složek",
|
"folderWithCount_other": "{{count}} složek",
|
||||||
"albumArtist_one": "umělec alba",
|
"albumArtist_one": "umělec alba",
|
||||||
"albumArtist_few": "umělci alba",
|
"albumArtist_few": "umělci alb",
|
||||||
"albumArtist_other": "umělců alba",
|
"albumArtist_other": "umělci alb",
|
||||||
"track_one": "skladba",
|
"track_one": "skladba",
|
||||||
"track_few": "skladby",
|
"track_few": "skladby",
|
||||||
"track_other": "skladby",
|
"track_other": "skladby",
|
||||||
|
|||||||
+54
-54
@@ -21,8 +21,8 @@
|
|||||||
"repeat_off": "repetir desactivado",
|
"repeat_off": "repetir desactivado",
|
||||||
"queue_clear": "limpiar cola",
|
"queue_clear": "limpiar cola",
|
||||||
"muted": "silenciado",
|
"muted": "silenciado",
|
||||||
"unfavorite": "no favorito",
|
"unfavorite": "no favorita",
|
||||||
"queue_moveToTop": "mover seleccionado al fondo",
|
"queue_moveToTop": "mover seleccionado al final",
|
||||||
"queue_moveToBottom": "mover seleccionado al principio",
|
"queue_moveToBottom": "mover seleccionado al principio",
|
||||||
"shuffle_off": "mezclar desactivado",
|
"shuffle_off": "mezclar desactivado",
|
||||||
"addLast": "añadir último",
|
"addLast": "añadir último",
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
"remotePort_description": "establece el puerto para el control remoto del servidor",
|
"remotePort_description": "establece el puerto para el control remoto del servidor",
|
||||||
"hotkey_skipBackward": "retroceder",
|
"hotkey_skipBackward": "retroceder",
|
||||||
"replayGainMode_description": "ajusta el volumen de ganancia acorde a los valores de {{ReplayGain}} almacenados en los metadatos del archivo",
|
"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)",
|
"audioDevice_description": "selecciona el dispositivo de audio a usar durante la reproducción (solo reproductor web)",
|
||||||
"theme_description": "establece el tema a usar por la aplicación",
|
"theme_description": "establece el tema a usar por la aplicación",
|
||||||
"hotkey_playbackPause": "pausa",
|
"hotkey_playbackPause": "pausa",
|
||||||
"replayGainFallback": "{{ReplayGain}} alternativa",
|
"replayGainFallback": "{{ReplayGain}} alternativa",
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
"scrobble_description": "hace scrobble de las reproducciones en tu servidor de medios",
|
"scrobble_description": "hace scrobble de las reproducciones en tu servidor de medios",
|
||||||
"audioExclusiveMode_description": "activa el modo de audio exclusivo. En este modo, el sistema es normalmente bloqueado, y solo se permitirá mpv en la salida de audio",
|
"audioExclusiveMode_description": "activa el modo de audio exclusivo. En este modo, el sistema es normalmente bloqueado, y solo se permitirá mpv en la salida de audio",
|
||||||
"discordUpdateInterval": "intervalo de actualización del estado de actividad de {{discord}}",
|
"discordUpdateInterval": "intervalo de actualización del estado de actividad de {{discord}}",
|
||||||
"themeLight": "tema (luminoso)",
|
"themeLight": "tema (claro)",
|
||||||
"fontType_optionBuiltIn": "fuente incorporada",
|
"fontType_optionBuiltIn": "fuente incorporada",
|
||||||
"hotkey_playbackPlayPause": "play / pausa",
|
"hotkey_playbackPlayPause": "play / pausa",
|
||||||
"hotkey_rate1": "calificar con 1 estrella",
|
"hotkey_rate1": "calificar con 1 estrella",
|
||||||
@@ -82,8 +82,8 @@
|
|||||||
"hotkey_playbackPlay": "reproducir",
|
"hotkey_playbackPlay": "reproducir",
|
||||||
"hotkey_togglePreviousSongFavorite": "cambia $t(common.previousSong) a favorito",
|
"hotkey_togglePreviousSongFavorite": "cambia $t(common.previousSong) a favorito",
|
||||||
"hotkey_volumeDown": "bajar volumen",
|
"hotkey_volumeDown": "bajar volumen",
|
||||||
"hotkey_unfavoritePreviousSong": "$t(common.previousSong) no favorito",
|
"hotkey_unfavoritePreviousSong": "$t(common.previousSong) no favorita",
|
||||||
"audioPlayer_description": "selecciona el reproductor de audio a usar en la reproducción",
|
"audioPlayer_description": "selecciona el reproductor de audio a usar durante la reproducción",
|
||||||
"globalMediaHotkeys": "teclas de acceso rápido globales a medios",
|
"globalMediaHotkeys": "teclas de acceso rápido globales a medios",
|
||||||
"hotkey_globalSearch": "búsqueda global",
|
"hotkey_globalSearch": "búsqueda global",
|
||||||
"gaplessAudio_description": "establece la configuración de audio sin pausas para mpv",
|
"gaplessAudio_description": "establece la configuración de audio sin pausas para mpv",
|
||||||
@@ -106,11 +106,11 @@
|
|||||||
"font": "fuente",
|
"font": "fuente",
|
||||||
"mpvExtraParameters": "parámetros de mpv",
|
"mpvExtraParameters": "parámetros de mpv",
|
||||||
"replayGainMode_optionTrack": "$t(entity.track_one)",
|
"replayGainMode_optionTrack": "$t(entity.track_one)",
|
||||||
"themeLight_description": "establece el tema luminoso a usar por la aplicación",
|
"themeLight_description": "establece el tema claro a usar por la aplicación",
|
||||||
"hotkey_toggleFullScreenPlayer": "cambia el reproductor a pantalla completa",
|
"hotkey_toggleFullScreenPlayer": "cambia el reproductor a pantalla completa",
|
||||||
"hotkey_localSearch": "búsqueda en la página",
|
"hotkey_localSearch": "búsqueda en la página",
|
||||||
"hotkey_toggleQueue": "cambia la cola",
|
"hotkey_toggleQueue": "cambia la cola",
|
||||||
"remotePassword_description": "establece la contraseña para el control remoto del servidor. Esas credenciales son transferidas de forma insegura por defecto, por lo que deberías usar una contraseña única para que no tengas nada de qué preocuparte",
|
"remotePassword_description": "establece la contraseña para el control remoto del servidor. Esas credenciales son transferidas de forma insegura por defecto, por lo que deberías usar una contraseña única para que no tengas nada de lo que preocuparte",
|
||||||
"hotkey_rate5": "calificar con 5 estrellas",
|
"hotkey_rate5": "calificar con 5 estrellas",
|
||||||
"hotkey_playbackPrevious": "pista anterior",
|
"hotkey_playbackPrevious": "pista anterior",
|
||||||
"showSkipButtons_description": "muestra o esconde los botones de saltar en la barra del reproductor",
|
"showSkipButtons_description": "muestra o esconde los botones de saltar en la barra del reproductor",
|
||||||
@@ -126,7 +126,7 @@
|
|||||||
"hotkey_rate2": "calificar con 2 estrellas",
|
"hotkey_rate2": "calificar con 2 estrellas",
|
||||||
"playButtonBehavior_description": "establece el comportamiento por defecto del botón de reproducción cuando se añaden canciones a la cola",
|
"playButtonBehavior_description": "establece el comportamiento por defecto del botón de reproducción cuando se añaden canciones a la cola",
|
||||||
"minimumScrobblePercentage_description": "el porcentaje mínimo de la canción que debe ser reproducido antes de hacer scrobble",
|
"minimumScrobblePercentage_description": "el porcentaje mínimo de la canción que debe ser reproducido antes de hacer scrobble",
|
||||||
"exitToTray": "salida a bandeja",
|
"exitToTray": "salir a la bandeja",
|
||||||
"hotkey_rate4": "calificar con 4 estrellas",
|
"hotkey_rate4": "calificar con 4 estrellas",
|
||||||
"enableRemote": "activar control remoto del servidor",
|
"enableRemote": "activar control remoto del servidor",
|
||||||
"showSkipButton_description": "muestra o esconde los botones de saltar en la barra del reproductor",
|
"showSkipButton_description": "muestra o esconde los botones de saltar en la barra del reproductor",
|
||||||
@@ -142,13 +142,13 @@
|
|||||||
"replayGainFallback_description": "ganancia en db a aplicar si el archivo no tiene etiquetas de {{ReplayGain}}",
|
"replayGainFallback_description": "ganancia en db a aplicar si el archivo no tiene etiquetas de {{ReplayGain}}",
|
||||||
"replayGainPreamp_description": "ajusta la ganancia del preamplificador aplicada a los valores de {{ReplayGain}}",
|
"replayGainPreamp_description": "ajusta la ganancia del preamplificador aplicada a los valores de {{ReplayGain}}",
|
||||||
"hotkey_toggleRepeat": "alterna repetir",
|
"hotkey_toggleRepeat": "alterna repetir",
|
||||||
"lyricOffset_description": "desfasa la letra por la cantidad de milisegundos especificada",
|
"lyricOffset_description": "desfasa la letra en la cantidad de milisegundos especificada",
|
||||||
"sidebarConfiguration_description": "selecciona los elementos y el orden en que aparecerán en la barra lateral",
|
"sidebarConfiguration_description": "selecciona los elementos y el orden en que aparecerán en la barra lateral",
|
||||||
"fontType": "tipo de fuente",
|
"fontType": "tipo de fuente",
|
||||||
"remotePort": "puerto del control remoto del servidor",
|
"remotePort": "puerto del control remoto del servidor",
|
||||||
"applicationHotkeys": "teclas de acceso rápido de la aplicación",
|
"applicationHotkeys": "teclas de acceso rápido de la aplicación",
|
||||||
"hotkey_playbackNext": "pista siguiente",
|
"hotkey_playbackNext": "pista siguiente",
|
||||||
"useSystemTheme_description": "sigue la preferencia luminosa u oscura definida por el sistema",
|
"useSystemTheme_description": "sigue la preferencia clara u oscura definida por el sistema",
|
||||||
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
||||||
"lyricFetch_description": "busca letras en varias fuentes de Internet",
|
"lyricFetch_description": "busca letras en varias fuentes de Internet",
|
||||||
"lyricFetchProvider_description": "selecciona los proveedores para buscar letras. el orden de los proveedores es el orden en el que se consultarán",
|
"lyricFetchProvider_description": "selecciona los proveedores para buscar letras. el orden de los proveedores es el orden en el que se consultarán",
|
||||||
@@ -160,13 +160,13 @@
|
|||||||
"sidePlayQueueStyle_optionDetached": "separada",
|
"sidePlayQueueStyle_optionDetached": "separada",
|
||||||
"audioPlayer": "reproductor de audio",
|
"audioPlayer": "reproductor de audio",
|
||||||
"hotkey_zoomOut": "reducir",
|
"hotkey_zoomOut": "reducir",
|
||||||
"hotkey_unfavoriteCurrentSong": "$t(common.currentSong) no favorito",
|
"hotkey_unfavoriteCurrentSong": "$t(common.currentSong) no favorita",
|
||||||
"hotkey_rate0": "Limpiar calificación",
|
"hotkey_rate0": "Limpiar calificación",
|
||||||
"discordApplicationId": "id de aplicación {{discord}}",
|
"discordApplicationId": "id de aplicación {{discord}}",
|
||||||
"applicationHotkeys_description": "configura las teclas de acceso rápido de la aplicación. marca la casilla para establecerlas como teclas de acceso rápido globales (solo escritorio)",
|
"applicationHotkeys_description": "configura las teclas de acceso rápido de la aplicación. marca la casilla para establecerlas como teclas de acceso rápido globales (solo escritorio)",
|
||||||
"floatingQueueArea_description": "muestra un icono flotante en el lado derecho de la pantalla para ver la cola de reproducción",
|
"floatingQueueArea_description": "muestra un icono flotante en el lado derecho de la pantalla para ver la cola de reproducción",
|
||||||
"hotkey_volumeMute": "silenciar volumen",
|
"hotkey_volumeMute": "silenciar volumen",
|
||||||
"hotkey_toggleCurrentSongFavorite": "cambia $t(common.currentSong) a favorito",
|
"hotkey_toggleCurrentSongFavorite": "$t(common.currentSong) cambia a favorita",
|
||||||
"remoteUsername": "nombre de usuario del control remoto del servidor",
|
"remoteUsername": "nombre de usuario del control remoto del servidor",
|
||||||
"showSkipButton": "mostrar botones de saltar",
|
"showSkipButton": "mostrar botones de saltar",
|
||||||
"sidebarPlaylistList": "listas de reproducción de la barra lateral",
|
"sidebarPlaylistList": "listas de reproducción de la barra lateral",
|
||||||
@@ -195,12 +195,12 @@
|
|||||||
"hotkey_browserBack": "retroceso",
|
"hotkey_browserBack": "retroceso",
|
||||||
"clearCache": "Limpiar la caché del navegador",
|
"clearCache": "Limpiar la caché del navegador",
|
||||||
"clearQueryCache": "Limpiar la caché de Feishin",
|
"clearQueryCache": "Limpiar la caché de Feishin",
|
||||||
"clearQueryCache_description": "Una 'limpieza suave' de Feishin. Esto refrescará las listas de reproducción, metadatos de pistas y restablecerá las letras guardadas. Se mantienen los ajustes, credenciales del servidor y las imágenes en caché",
|
"clearQueryCache_description": "Una 'limpieza suave' de Feishin. Esto refrescará las listas de reproducción, los metadatos de las pistas y restablecerá las letras guardadas. Se mantienen los ajustes, credenciales del servidor y las imágenes en caché",
|
||||||
"buttonSize": "tamaño del botón de la barra de reproducción",
|
"buttonSize": "tamaño del botón de la barra de reproducción",
|
||||||
"clearCache_description": "Una 'limpieza fuerte' de Feishin. Para limpiar la caché de Feishin, vacía la caché del navegador (imágenes guardadas y otros elementos). Se mantienen las credenciales y ajustes del servidor",
|
"clearCache_description": "Una 'limpieza fuerte' de Feishin. Para limpiar la caché de Feishin, vacía la caché del navegador (imágenes guardadas y otros elementos). Se mantienen las credenciales y ajustes del servidor",
|
||||||
"buttonSize_description": "el tamaño de los botones de la barra de reproducción",
|
"buttonSize_description": "el tamaño de los botones de la barra de reproducción",
|
||||||
"passwordStore_description": "qué método de almacenamiento de contraseñas/claves secretas utilizar. cambie esta opción si tiene problemas para guardar contraseñas.",
|
"passwordStore_description": "qué método de almacenamiento de contraseñas/claves secretas utilizar. cambia esta opción si tienes problemas para guardar contraseñas.",
|
||||||
"startMinimized_description": "iniciar la aplicación en la bandeja del sistema",
|
"startMinimized_description": "inicia la aplicación en la bandeja del sistema",
|
||||||
"startMinimized": "iniciar minimizado",
|
"startMinimized": "iniciar minimizado",
|
||||||
"passwordStore": "contraseñas/almacenamiento secreto",
|
"passwordStore": "contraseñas/almacenamiento secreto",
|
||||||
"playerAlbumArtResolution_description": "la resolución para la vista previa de la carátula del álbum del reproductor grande. más grande hace que parezca más nítido, pero puede ralentizar la carga. El valor predeterminado es 0, lo que significa automático",
|
"playerAlbumArtResolution_description": "la resolución para la vista previa de la carátula del álbum del reproductor grande. más grande hace que parezca más nítido, pero puede ralentizar la carga. El valor predeterminado es 0, lo que significa automático",
|
||||||
@@ -208,8 +208,8 @@
|
|||||||
"homeConfiguration": "Configuración de la página de inicio",
|
"homeConfiguration": "Configuración de la página de inicio",
|
||||||
"mpvExtraParameters_help": "Uno por línea",
|
"mpvExtraParameters_help": "Uno por línea",
|
||||||
"genreBehavior": "Comportamiento predeterminado de la página de géneros",
|
"genreBehavior": "Comportamiento predeterminado de la página de géneros",
|
||||||
"externalLinks_description": "Permite mostrar enlaces externos (Last.fm, MusicBrainz) en páginas de artista/álbum",
|
"externalLinks_description": "Permite mostrar enlaces externos (Last.fm, MusicBrainz) en las páginas del artista/álbum",
|
||||||
"genreBehavior_description": "Determina si al pulsar en un género se abre por defecto la lista de pistas o de álbumes",
|
"genreBehavior_description": "Determina si al hacer clic en un género se abre por defecto la lista de pistas o de álbumes",
|
||||||
"homeConfiguration_description": "Configura qué elementos son mostrados y en qué orden en la página de inicio",
|
"homeConfiguration_description": "Configura qué elementos son mostrados y en qué orden en la página de inicio",
|
||||||
"clearCacheSuccess": "Caché limpiada correctamente",
|
"clearCacheSuccess": "Caché limpiada correctamente",
|
||||||
"externalLinks": "Mostrar enlaces externos",
|
"externalLinks": "Mostrar enlaces externos",
|
||||||
@@ -218,15 +218,15 @@
|
|||||||
"imageAspectRatio_description": "Si está habilitado, la portada será mostrada usando su relación de aspecto nativa. Para arte que no es 1:1, el espacio restante estará vacío",
|
"imageAspectRatio_description": "Si está habilitado, la portada será mostrada usando su relación de aspecto nativa. Para arte que no es 1:1, el espacio restante estará vacío",
|
||||||
"imageAspectRatio": "Usar relación de aspecto nativa de portada",
|
"imageAspectRatio": "Usar relación de aspecto nativa de portada",
|
||||||
"doubleClickBehavior": "poner en cola todas las pistas buscadas al hacer doble clic",
|
"doubleClickBehavior": "poner en cola todas las pistas buscadas al hacer doble clic",
|
||||||
"doubleClickBehavior_description": "si es true, se pondrán en cola todas las pistas que coincidan en una búsqueda de pistas. De lo contrario, solo se pondrá en cola la pista seleccionada",
|
"doubleClickBehavior_description": "si está activado, se pondrán en cola todas las pistas que coincidan en una búsqueda de pistas. De lo contrario, solo se pondrán en cola las pistas seleccionadas",
|
||||||
"volumeWidth": "Ancho del deslizador de volumen",
|
"volumeWidth": "Ancho del deslizador de volumen",
|
||||||
"volumeWidth_description": "La anchura del deslizador de volumen",
|
"volumeWidth_description": "La anchura del deslizador de volumen",
|
||||||
"discordListening_description": "mostrar el estado como escuchando en lugar de jugando",
|
"discordListening_description": "muestra el estado como Escuchando en lugar de Jugando a",
|
||||||
"discordListening": "Mostrar estado como escuchando",
|
"discordListening": "Mostrar estado como escuchando",
|
||||||
"contextMenu": "Configuración del menú de contexto (clic derecho)",
|
"contextMenu": "Configuración del menú de contexto (clic derecho)",
|
||||||
"contextMenu_description": "Te permite esconder elementos que son mostrados en el menú cuando haces clic derecho en un elemento. Los elementos que no estén seleccionados serán escondidos",
|
"contextMenu_description": "Te permite esconder elementos que son mostrados en el menú cuando haces clic derecho en un elemento. Los elementos que no estén seleccionados serán escondidos",
|
||||||
"customCssEnable": "Habilitar CSS personalizado",
|
"customCssEnable": "Habilitar CSS personalizado",
|
||||||
"customCssEnable_description": "Permite la escritura de CSS personalizado.",
|
"customCssEnable_description": "Permite escribir CSS personalizado.",
|
||||||
"customCss": "CSS personalizado",
|
"customCss": "CSS personalizado",
|
||||||
"customCssNotice": "Aviso: mientras hay alguna sanitización (rechazar url() y content:), usar CSS personalizado puede aún entrañar riesgos cambiando la interfaz.",
|
"customCssNotice": "Aviso: mientras hay alguna sanitización (rechazar url() y content:), usar CSS personalizado puede aún entrañar riesgos cambiando la interfaz.",
|
||||||
"customCss_description": "Content CSS personalizado. Nota: content y urls remotas son propiedades rechazadas. Una vista previa de tu content se muestra debajo. Las entradas adicionales que no estableciste están presentes debido a la sanitización.",
|
"customCss_description": "Content CSS personalizado. Nota: content y urls remotas son propiedades rechazadas. Una vista previa de tu content se muestra debajo. Las entradas adicionales que no estableciste están presentes debido a la sanitización.",
|
||||||
@@ -236,20 +236,20 @@
|
|||||||
"transcode_description": "permite la transcodificación a distintos formatos",
|
"transcode_description": "permite la transcodificación a distintos formatos",
|
||||||
"transcodeBitrate": "tasa de bits a transcodificar",
|
"transcodeBitrate": "tasa de bits a transcodificar",
|
||||||
"transcodeBitrate_description": "selecciona el bitrate a transcodificar. 0 significa dejar que el servidor elija",
|
"transcodeBitrate_description": "selecciona el bitrate a transcodificar. 0 significa dejar que el servidor elija",
|
||||||
"transcodeNote": "Se mostrará después de 1 (web) - 2 (mpv) pistas",
|
"transcodeNote": "tendrá efecto después de 1 (web) - 2 (mpv) canciones",
|
||||||
"transcodeFormat": "formato a transcodificar",
|
"transcodeFormat": "formato a transcodificar",
|
||||||
"transcodeFormat_description": "selecciona el formato a transcodificar. dejar vacío para que el servidor decida",
|
"transcodeFormat_description": "selecciona el formato a transcodificar. dejar vacío para que el servidor decida",
|
||||||
"albumBackground": "imagen de fondo del álbum",
|
"albumBackground": "imagen de fondo del álbum",
|
||||||
"albumBackground_description": "Agregar una imagen de fondo a las páginas del álbum que contienen la carátula del álbum",
|
"albumBackground_description": "Añade una imagen de fondo a las páginas del álbum que contienen la carátula del álbum",
|
||||||
"albumBackgroundBlur": "Tamaño de desenfoque de la imagen de fondo del álbum",
|
"albumBackgroundBlur": "Tamaño de desenfoque de la imagen de fondo del álbum",
|
||||||
"albumBackgroundBlur_description": "Ajustar el nivel de desenfoque de la imagen de fondo del álbum",
|
"albumBackgroundBlur_description": "Ajusta la cantidad de desenfoque aplicado a la imagen de fondo del álbum",
|
||||||
"playerbarOpenDrawer": "Cambiar la barra del reproductor a pantalla completa",
|
"playerbarOpenDrawer": "Cambiar la barra del reproductor a pantalla completa",
|
||||||
"playerbarOpenDrawer_description": "Permitir hacer clic en la barra del reproductor para abrir el reproductor en pantalla completa",
|
"playerbarOpenDrawer_description": "Permite hacer clic en la barra del reproductor para abrir el reproductor a pantalla completa",
|
||||||
"artistConfiguration": "Configuración de la página del artista del álbum",
|
"artistConfiguration": "Configuración de la página del artista del álbum",
|
||||||
"artistConfiguration_description": "Configurar qué elementos se muestran y en qué orden en la página del artista del álbum",
|
"artistConfiguration_description": "Configura qué elementos se muestran y en qué orden en la página del artista del álbum",
|
||||||
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
||||||
"trayEnabled": "Mostrar en el área de notificación",
|
"trayEnabled": "Mostrar en el área de notificación",
|
||||||
"trayEnabled_description": "mostrar/ocultar el icono/menú del área de notificación. si está deshabilitado, también deshabilita minimizar/salir a la bandeja",
|
"trayEnabled_description": "muestra/oculta el icono/menú del área de notificación. si está deshabilitado, también deshabilita minimizar/salir a la bandeja",
|
||||||
"translationApiProvider": "Proveedor de API de traducción",
|
"translationApiProvider": "Proveedor de API de traducción",
|
||||||
"translationApiProvider_description": "Proveedor de API para traducción",
|
"translationApiProvider_description": "Proveedor de API para traducción",
|
||||||
"translationApiKey": "clave api de traducción",
|
"translationApiKey": "clave api de traducción",
|
||||||
@@ -273,7 +273,7 @@
|
|||||||
"deletePlaylist": "eliminar $t(entity.playlist_one)",
|
"deletePlaylist": "eliminar $t(entity.playlist_one)",
|
||||||
"removeFromQueue": "eliminar de la cola",
|
"removeFromQueue": "eliminar de la cola",
|
||||||
"deselectAll": "desmarcar todo",
|
"deselectAll": "desmarcar todo",
|
||||||
"moveToBottom": "mover al fondo",
|
"moveToBottom": "mover al final",
|
||||||
"setRating": "establecer calificación",
|
"setRating": "establecer calificación",
|
||||||
"toggleSmartPlaylistEditor": "cambiar editor $t(entity.smartPlaylist)",
|
"toggleSmartPlaylistEditor": "cambiar editor $t(entity.smartPlaylist)",
|
||||||
"removeFromFavorites": "eliminar de $t(entity.favorite_other)",
|
"removeFromFavorites": "eliminar de $t(entity.favorite_other)",
|
||||||
@@ -296,7 +296,7 @@
|
|||||||
"left": "izquierda",
|
"left": "izquierda",
|
||||||
"save": "guardar",
|
"save": "guardar",
|
||||||
"right": "derecha",
|
"right": "derecha",
|
||||||
"currentSong": "actual $t(entity.track_one)",
|
"currentSong": "$t(entity.track_one) actual",
|
||||||
"collapse": "contraer",
|
"collapse": "contraer",
|
||||||
"trackNumber": "pista",
|
"trackNumber": "pista",
|
||||||
"descending": "descendiente",
|
"descending": "descendiente",
|
||||||
@@ -334,7 +334,7 @@
|
|||||||
"saveAndReplace": "guardar y reemplazar",
|
"saveAndReplace": "guardar y reemplazar",
|
||||||
"playerMustBePaused": "el reproductor debe pausarse",
|
"playerMustBePaused": "el reproductor debe pausarse",
|
||||||
"confirm": "confirmar",
|
"confirm": "confirmar",
|
||||||
"resetToDefault": "restablecer a valor por defecto",
|
"resetToDefault": "restablecer al valor predeterminado",
|
||||||
"home": "inicio",
|
"home": "inicio",
|
||||||
"comingSoon": "próximamente…",
|
"comingSoon": "próximamente…",
|
||||||
"reset": "restablecer",
|
"reset": "restablecer",
|
||||||
@@ -365,8 +365,8 @@
|
|||||||
"channel_one": "Canal",
|
"channel_one": "Canal",
|
||||||
"channel_many": "Canales",
|
"channel_many": "Canales",
|
||||||
"channel_other": "Canales",
|
"channel_other": "Canales",
|
||||||
"trackPeak": "la más alta de la canción",
|
"trackPeak": "pico de pista",
|
||||||
"albumPeak": "lo más destacado del álbum",
|
"albumPeak": "pico del álbum",
|
||||||
"albumGain": "Ganancia de álbum",
|
"albumGain": "Ganancia de álbum",
|
||||||
"mbid": "ID de MusicBrainz",
|
"mbid": "ID de MusicBrainz",
|
||||||
"codec": "Códec",
|
"codec": "Códec",
|
||||||
@@ -397,13 +397,13 @@
|
|||||||
"audioDeviceFetchError": "un error ocurrió cuando se intentó obtener los dispositivos de audio",
|
"audioDeviceFetchError": "un error ocurrió cuando se intentó obtener los dispositivos de audio",
|
||||||
"invalidServer": "servidor inválido",
|
"invalidServer": "servidor inválido",
|
||||||
"loginRateError": "demasiados intentos de inicio de sesión, por favor inténtalo en unos segundos",
|
"loginRateError": "demasiados intentos de inicio de sesión, por favor inténtalo en unos segundos",
|
||||||
"badAlbum": "Estás viendo esta página porque esta canción no forma parte de un álbum. Este problema puede ocurrir si tiene una canción en el nivel superior de su carpeta de música. Jellyfin solo agrupa pistas si están en una carpeta.",
|
"badAlbum": "Estás viendo esta página porque esta canción no forma parte de un álbum. Este problema puede ocurrir si tienes una canción en el nivel superior de tu carpeta de música. Jellyfin solo agrupa pistas si están en una carpeta.",
|
||||||
"networkError": "Ocurrió un error de red",
|
"networkError": "Ocurrió un error de red",
|
||||||
"openError": "No se pudo abrir el archivo"
|
"openError": "No se pudo abrir el archivo"
|
||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
"mostPlayed": "más reproducido",
|
"mostPlayed": "más reproducido",
|
||||||
"isCompilation": "es compilación",
|
"isCompilation": "es una compilación",
|
||||||
"recentlyPlayed": "recientemente reproducido",
|
"recentlyPlayed": "recientemente reproducido",
|
||||||
"isRated": "es clasificado",
|
"isRated": "es clasificado",
|
||||||
"title": "título",
|
"title": "título",
|
||||||
@@ -434,7 +434,7 @@
|
|||||||
"criticRating": "calificación de la crítica",
|
"criticRating": "calificación de la crítica",
|
||||||
"trackNumber": "pista",
|
"trackNumber": "pista",
|
||||||
"comment": "comentarios",
|
"comment": "comentarios",
|
||||||
"playCount": "número de reproducción",
|
"playCount": "número de reproducciones",
|
||||||
"recentlyUpdated": "actualizado recientemente",
|
"recentlyUpdated": "actualizado recientemente",
|
||||||
"channels": "$t(common.channel_other)",
|
"channels": "$t(common.channel_other)",
|
||||||
"owner": "$t(common.owner)",
|
"owner": "$t(common.owner)",
|
||||||
@@ -500,8 +500,8 @@
|
|||||||
"mostPlayed": "más reproducidos",
|
"mostPlayed": "más reproducidos",
|
||||||
"newlyAdded": "nuevos lanzamientos añadidos",
|
"newlyAdded": "nuevos lanzamientos añadidos",
|
||||||
"title": "$t(common.home)",
|
"title": "$t(common.home)",
|
||||||
"explore": "explorar desde tu biblioteca",
|
"explore": "explora desde tu biblioteca",
|
||||||
"recentlyPlayed": "recientemente reproducidos"
|
"recentlyPlayed": "reproducidos recientemente"
|
||||||
},
|
},
|
||||||
"fullscreenPlayer": {
|
"fullscreenPlayer": {
|
||||||
"upNext": "siguiente",
|
"upNext": "siguiente",
|
||||||
@@ -519,7 +519,7 @@
|
|||||||
"lyricGap": "desfase de letra",
|
"lyricGap": "desfase de letra",
|
||||||
"dynamicImageBlur": "tamaño de desenfoque de imagen",
|
"dynamicImageBlur": "tamaño de desenfoque de imagen",
|
||||||
"dynamicIsImage": "habilitar imagen de fondo",
|
"dynamicIsImage": "habilitar imagen de fondo",
|
||||||
"lyricOffset": "compensación de letras (ms)"
|
"lyricOffset": "desplazamiento de letras (ms)"
|
||||||
},
|
},
|
||||||
"lyrics": "letras",
|
"lyrics": "letras",
|
||||||
"related": "relacionado",
|
"related": "relacionado",
|
||||||
@@ -529,7 +529,7 @@
|
|||||||
"albumDetail": {
|
"albumDetail": {
|
||||||
"moreFromArtist": "más de este $t(entity.artist_one)",
|
"moreFromArtist": "más de este $t(entity.artist_one)",
|
||||||
"moreFromGeneric": "más de {{item}}",
|
"moreFromGeneric": "más de {{item}}",
|
||||||
"released": "publicado"
|
"released": "publicado el"
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
"playbackTab": "reproducción",
|
"playbackTab": "reproducción",
|
||||||
@@ -549,7 +549,7 @@
|
|||||||
"trackList": {
|
"trackList": {
|
||||||
"title": "$t(entity.track_other)",
|
"title": "$t(entity.track_other)",
|
||||||
"genreTracks": "\"{{genre}}\" $t(entity.track_other)",
|
"genreTracks": "\"{{genre}}\" $t(entity.track_other)",
|
||||||
"artistTracks": "pistas por {{artist}}"
|
"artistTracks": "Pistas de {{artist}}"
|
||||||
},
|
},
|
||||||
"globalSearch": {
|
"globalSearch": {
|
||||||
"commands": {
|
"commands": {
|
||||||
@@ -565,11 +565,11 @@
|
|||||||
"albumList": {
|
"albumList": {
|
||||||
"title": "$t(entity.album_other)",
|
"title": "$t(entity.album_other)",
|
||||||
"genreAlbums": "\"{{genre}}\" $t(entity.album_other)",
|
"genreAlbums": "\"{{genre}}\" $t(entity.album_other)",
|
||||||
"artistAlbums": "álbumes de {{artist}}"
|
"artistAlbums": "Álbumes de {{artist}}"
|
||||||
},
|
},
|
||||||
"albumArtistDetail": {
|
"albumArtistDetail": {
|
||||||
"viewAllTracks": "ver todo de $t(entity.track_other)",
|
"viewAllTracks": "ver todas las $t(entity.track_other)",
|
||||||
"relatedArtists": "$t(entity.artist_other) similar",
|
"relatedArtists": "$t(entity.artist_other) similares",
|
||||||
"topSongs": "mejores canciones",
|
"topSongs": "mejores canciones",
|
||||||
"topSongsFrom": "las mejores canciones de {{title}}",
|
"topSongsFrom": "las mejores canciones de {{title}}",
|
||||||
"viewAll": "Ver todo",
|
"viewAll": "Ver todo",
|
||||||
@@ -675,7 +675,7 @@
|
|||||||
"songCount": "$t(entity.track_other)",
|
"songCount": "$t(entity.track_other)",
|
||||||
"trackNumber": "pista",
|
"trackNumber": "pista",
|
||||||
"genre": "$t(entity.genre_one)",
|
"genre": "$t(entity.genre_one)",
|
||||||
"albumArtist": "artista de álbum",
|
"albumArtist": "artista del álbum",
|
||||||
"path": "ruta",
|
"path": "ruta",
|
||||||
"discNumber": "disco",
|
"discNumber": "disco",
|
||||||
"channels": "$t(common.channel_other)",
|
"channels": "$t(common.channel_other)",
|
||||||
@@ -706,7 +706,7 @@
|
|||||||
"note": "$t(common.note)",
|
"note": "$t(common.note)",
|
||||||
"owner": "$t(common.owner)",
|
"owner": "$t(common.owner)",
|
||||||
"path": "$t(common.path)",
|
"path": "$t(common.path)",
|
||||||
"playCount": "número de reproducción",
|
"playCount": "número de reproducciones",
|
||||||
"genre": "$t(entity.genre_one)",
|
"genre": "$t(entity.genre_one)",
|
||||||
"favorite": "$t(common.favorite)",
|
"favorite": "$t(common.favorite)",
|
||||||
"year": "$t(common.year)",
|
"year": "$t(common.year)",
|
||||||
@@ -747,15 +747,15 @@
|
|||||||
"folderWithCount_one": "{{count}} carpeta",
|
"folderWithCount_one": "{{count}} carpeta",
|
||||||
"folderWithCount_many": "{{count}} carpetas",
|
"folderWithCount_many": "{{count}} carpetas",
|
||||||
"folderWithCount_other": "{{count}} carpetas",
|
"folderWithCount_other": "{{count}} carpetas",
|
||||||
"albumArtist_one": "artista de álbum",
|
"albumArtist_one": "artista del álbum",
|
||||||
"albumArtist_many": "artistas de álbum",
|
"albumArtist_many": "artistas del álbum",
|
||||||
"albumArtist_other": "artistas de álbum",
|
"albumArtist_other": "artistas del álbum",
|
||||||
"track_one": "pista",
|
"track_one": "pista",
|
||||||
"track_many": "pistas",
|
"track_many": "pistas",
|
||||||
"track_other": "pistas",
|
"track_other": "pistas",
|
||||||
"albumArtistCount_one": "{{count}} artista de álbum",
|
"albumArtistCount_one": "{{count}} artista del álbum",
|
||||||
"albumArtistCount_many": "{{count}} artistas de álbum",
|
"albumArtistCount_many": "{{count}} artistas del álbum",
|
||||||
"albumArtistCount_other": "{{count}} artistas de álbum",
|
"albumArtistCount_other": "{{count}} artistas del álbum",
|
||||||
"albumWithCount_one": "{{count}} álbum",
|
"albumWithCount_one": "{{count}} álbum",
|
||||||
"albumWithCount_many": "{{count}} álbumes",
|
"albumWithCount_many": "{{count}} álbumes",
|
||||||
"albumWithCount_other": "{{count}} álbumes",
|
"albumWithCount_other": "{{count}} álbumes",
|
||||||
@@ -777,9 +777,9 @@
|
|||||||
"trackWithCount_one": "{{count}} pista",
|
"trackWithCount_one": "{{count}} pista",
|
||||||
"trackWithCount_many": "{{count}} pistas",
|
"trackWithCount_many": "{{count}} pistas",
|
||||||
"trackWithCount_other": "{{count}} pistas",
|
"trackWithCount_other": "{{count}} pistas",
|
||||||
"play_one": "Reproducir {{count}}",
|
"play_one": "{{count}} reproducción",
|
||||||
"play_many": "Reproducir {{count}}",
|
"play_many": "{{count}} reproducciones",
|
||||||
"play_other": "Reproducir {{count}}",
|
"play_other": "{{count}} reproducciones",
|
||||||
"song_one": "canción",
|
"song_one": "canción",
|
||||||
"song_many": "canciones",
|
"song_many": "canciones",
|
||||||
"song_other": "canciones"
|
"song_other": "canciones"
|
||||||
|
|||||||
@@ -6,11 +6,11 @@
|
|||||||
"skip": "رد کن",
|
"skip": "رد کن",
|
||||||
"toggleFullscreenPlayer": "تغییر به پخشکنندهٔ تمامصفحه",
|
"toggleFullscreenPlayer": "تغییر به پخشکنندهٔ تمامصفحه",
|
||||||
"skip_back": "برو عقب",
|
"skip_back": "برو عقب",
|
||||||
"shuffle": "شافل",
|
"shuffle": "پخش تصادفی",
|
||||||
"repeat_off": "تکرار غیرفعال",
|
"repeat_off": "تکرار غیرفعال",
|
||||||
"pause": "ایست",
|
"pause": "ایست",
|
||||||
"unfavorite": "حذف از موردعلاقهها",
|
"unfavorite": "حذف از موردعلاقهها",
|
||||||
"shuffle_off": "شافل غیرفعال",
|
"shuffle_off": "پخش تصادفی غیر فعال",
|
||||||
"skip_forward": "برو جلو",
|
"skip_forward": "برو جلو",
|
||||||
"queue_moveToTop": "جابجا کردن انتخاب شده به پایین",
|
"queue_moveToTop": "جابجا کردن انتخاب شده به پایین",
|
||||||
"queue_clear": "خالی کردن صف",
|
"queue_clear": "خالی کردن صف",
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
"deselectAll": "لغو انتخاب همه",
|
"deselectAll": "لغو انتخاب همه",
|
||||||
"moveToBottom": "انتقال به پایین",
|
"moveToBottom": "انتقال به پایین",
|
||||||
"setRating": "تعیین امتیاز",
|
"setRating": "تعیین امتیاز",
|
||||||
"toggleSmartPlaylistEditor": "تغییر $t(entity.smartPlaylist) ویرایشگر",
|
"toggleSmartPlaylistEditor": "تغییر ویرایشگر $t(entity.smartPlaylist)",
|
||||||
"removeFromFavorites": "حذف از $t(entity.favorite_other)",
|
"removeFromFavorites": "حذف از $t(entity.favorite_other)",
|
||||||
"openIn": {
|
"openIn": {
|
||||||
"lastfm": "باز کردن در Last.fm",
|
"lastfm": "باز کردن در Last.fm",
|
||||||
@@ -137,7 +137,44 @@
|
|||||||
"audioExclusiveMode_description": "حالت اختصاصی خروجی را فعال میکند. در این حالت، سامانه معمولاً قفل است و فقط mpv میتواند خروجی صدا دهد",
|
"audioExclusiveMode_description": "حالت اختصاصی خروجی را فعال میکند. در این حالت، سامانه معمولاً قفل است و فقط mpv میتواند خروجی صدا دهد",
|
||||||
"clearQueryCache_description": "یک 'پاکسازی نرم' از فیشین. این فهرستهای پخش و فرادادهی قطعهها را تازه میکند و متن شعرهای ذخیره شده را بازنشانی میکند. پیکربندیها، اعتبارنامههای سرویسدهنده و نگارههای کَش شده حفظ میشوند",
|
"clearQueryCache_description": "یک 'پاکسازی نرم' از فیشین. این فهرستهای پخش و فرادادهی قطعهها را تازه میکند و متن شعرهای ذخیره شده را بازنشانی میکند. پیکربندیها، اعتبارنامههای سرویسدهنده و نگارههای کَش شده حفظ میشوند",
|
||||||
"clearCache_description": "یک 'پاکسازی سخت' فیشین. افزون بر پاکسازی کَش فیشین، کَش مرورگر هم تهی میشود (نگارههای ذخیره شده و باقی داراییها). اعتبارنامهها و پیکربندیها حفظ میشوند",
|
"clearCache_description": "یک 'پاکسازی سخت' فیشین. افزون بر پاکسازی کَش فیشین، کَش مرورگر هم تهی میشود (نگارههای ذخیره شده و باقی داراییها). اعتبارنامهها و پیکربندیها حفظ میشوند",
|
||||||
"contextMenu_description": "به شما اجازه میدهد که آیتمهای نمایش داده شده در فهرستی که وقتی روی یک آیتم کلیک راست میکنید پدیدار میشود، را پنهان کنید. آیتمهایی که منتخب نیستند پنهان میشوند"
|
"contextMenu_description": "به شما اجازه میدهد که آیتمهای نمایش داده شده در فهرستی که وقتی روی یک آیتم کلیک راست میکنید پدیدار میشود، را پنهان کنید. آیتمهایی که منتخب نیستند پنهان میشوند",
|
||||||
|
"crossfadeStyle": "شیوهی crossfade",
|
||||||
|
"customCssEnable_description": "اجازه دادن برای نوشتن css سفارشی.",
|
||||||
|
"translationApiKey": "کلید API ترجمه",
|
||||||
|
"webAudio_description": "از صدای وب بهرهمند میشود. این قابلیتهای پیشرفتهای مانند گین بازپخش (replygain) را فعال میکند. غیرفعال کنید اگر غیر از این را تجربه میکنید",
|
||||||
|
"windowBarStyle_description": "گزینش سبک نوار پنجره",
|
||||||
|
"translationApiKey_description": "کلید API برای ترجمه (پشتیبانی فقط برای نقطهی پایانی سرویسدهندهی جهانی)",
|
||||||
|
"theme": "تم",
|
||||||
|
"hotkey_togglePreviousSongFavorite": "تغییر وضعیت برای مورد علاقهی $t(common.previousSong)",
|
||||||
|
"transcode": "فعالسازی رمزگردانی",
|
||||||
|
"transcode_description": "رمزگردانی به فرمتهای گوناگون را فعال میکند",
|
||||||
|
"transcodeBitrate": "نرخ انتقال رمزگردانی",
|
||||||
|
"startMinimized": "پنهانشده آغاز کن",
|
||||||
|
"theme_description": "تم مورد استفاده در نرمافزار را میگزیند",
|
||||||
|
"themeLight": "تم (روشن)",
|
||||||
|
"transcodeBitrate_description": "نرخ انتقال برای رمزگردانی را انتخاب میکند. 0 بدان معناست سرور آن را انتخاب کند",
|
||||||
|
"transcodeFormat": "فرمت رمزگردانی",
|
||||||
|
"transcodeFormat_description": "فرمت رمزگردانی را انتخاب میکند. برای اینکه سرور آن را انتخاب کند، خالی بگذارید",
|
||||||
|
"customCssEnable": "فعال کردن css سفارشی",
|
||||||
|
"translationTargetLanguage": "زبان هدف ترجمه",
|
||||||
|
"hotkey_toggleCurrentSongFavorite": "تغییر وضعیت مورد علاقه برای $t(common.currentSong)",
|
||||||
|
"themeDark_description": "تم تاریک را برای استفادهی نرمافزار میگزیند",
|
||||||
|
"volumeWheelStep_description": "اندازهای از حجم صدا را در زمان اسکرول کردن روی نوار لغزنده تغییر داده شود",
|
||||||
|
"trayEnabled": "نمایش سینی",
|
||||||
|
"trayEnabled_description": "نمایش/پنهان کردن آیکون/فهرست در سینی. اگر غیرفعال باشد، کوچک کردن/خروج به سینی را نیز غیرفعال میکند",
|
||||||
|
"useSystemTheme_description": "از روشنی یا تاریکی که سیستم تعریف کرده است، پیروی میکند",
|
||||||
|
"crossfadeDuration": "زمان محو کردن گذار قطعه به قطعهی بعدی",
|
||||||
|
"themeLight_description": "تم روشن را برای استفادهی نرمافزار میگزیند",
|
||||||
|
"volumeWidth": "عرض نوار لغزندهی حجم صدا",
|
||||||
|
"crossfadeStyle_description": "شیوهی crossfade که میخواهید پخشکننده از آن استفاده کند را انتخاب کنید",
|
||||||
|
"startMinimized_description": "نرمافزار را در سینی اجرا کن",
|
||||||
|
"volumeWidth_description": "عرضی که نوار لغزندهی حجم صدا داشته باشد",
|
||||||
|
"themeDark": "تم (تاریک)",
|
||||||
|
"useSystemTheme": "استفاده از تم سیستم",
|
||||||
|
"volumeWheelStep": "گام چرخ حجم صدا",
|
||||||
|
"webAudio": "استفاده از صدای وب",
|
||||||
|
"windowBarStyle": "سبک نوار پنجره",
|
||||||
|
"crossfadeDuration_description": "زمان افکت crossfade را مشخص میکند"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"backward": "به عقب",
|
"backward": "به عقب",
|
||||||
@@ -184,9 +221,9 @@
|
|||||||
"forceRestartRequired": "برای اعمال تغییرها دوباره راهاندازی کنید… اعلان را برای راهاندازی دوباره ببندید",
|
"forceRestartRequired": "برای اعمال تغییرها دوباره راهاندازی کنید… اعلان را برای راهاندازی دوباره ببندید",
|
||||||
"version": "نسخه",
|
"version": "نسخه",
|
||||||
"title": "عنوان",
|
"title": "عنوان",
|
||||||
"filter_one": "فیلتر",
|
"filter_one": "پالایش",
|
||||||
"filter_other": "فیلتر",
|
"filter_other": "پالایش",
|
||||||
"filters": "فیلتر",
|
"filters": "پالایش",
|
||||||
"create": "ساختن",
|
"create": "ساختن",
|
||||||
"bitrate": "بیتریت",
|
"bitrate": "بیتریت",
|
||||||
"saveAndReplace": "ذخیره و جایگزین",
|
"saveAndReplace": "ذخیره و جایگزین",
|
||||||
@@ -468,7 +505,7 @@
|
|||||||
"synchronized": "همگام شده"
|
"synchronized": "همگام شده"
|
||||||
},
|
},
|
||||||
"noLyrics": "هیچ متن شعری پیدا نشد",
|
"noLyrics": "هیچ متن شعری پیدا نشد",
|
||||||
"lyrics": "متن شعرها",
|
"lyrics": "متن شعر",
|
||||||
"upNext": "در ادامه"
|
"upNext": "در ادامه"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
@@ -544,5 +581,46 @@
|
|||||||
"copiedPath": "مسیر با موفقیت کپی شد",
|
"copiedPath": "مسیر با موفقیت کپی شد",
|
||||||
"openFile": "نمایش قطعه در مدیر پرونده"
|
"openFile": "نمایش قطعه در مدیر پرونده"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"table": {
|
||||||
|
"column": {
|
||||||
|
"size": "$t(common.size)",
|
||||||
|
"lastPlayed": "آخرین بار پخش شده",
|
||||||
|
"discNumber": "دیسک",
|
||||||
|
"songCount": "$t(entity.track_other)",
|
||||||
|
"title": "عنوان",
|
||||||
|
"trackNumber": "قطعه",
|
||||||
|
"favorite": "مورد علاقه",
|
||||||
|
"genre": "$t(entity.genre_one)",
|
||||||
|
"comment": "دیدگاه",
|
||||||
|
"playCount": "تعداد پخش",
|
||||||
|
"rating": "امتیاز",
|
||||||
|
"path": "مسیر",
|
||||||
|
"releaseYear": "سال",
|
||||||
|
"dateAdded": "تاریخ افزوده شدن",
|
||||||
|
"releaseDate": "تاریخ عرضه"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"general": {
|
||||||
|
"followCurrentSong": "آهنگ کنونی را دنبال کن",
|
||||||
|
"displayType": "نوع نمایش",
|
||||||
|
"itemSize": "اندازهی آیتم (px)",
|
||||||
|
"size": "$t(common.size)",
|
||||||
|
"tableColumns": "ستونهای جدول",
|
||||||
|
"autoFitColumns": "تطبیق دادن ستونها به شیوهی خودکار",
|
||||||
|
"gap": "$t(common.gap)",
|
||||||
|
"itemGap": "فاصلهی آیتم (px)"
|
||||||
|
},
|
||||||
|
"view": {
|
||||||
|
"card": "کارت"
|
||||||
|
},
|
||||||
|
"label": {
|
||||||
|
"playCount": "تعداد پخش",
|
||||||
|
"dateAdded": "تاریخ افزوده شدن",
|
||||||
|
"discNumber": "شمارهی دیسک",
|
||||||
|
"lastPlayed": "آخرین بار پخش شده",
|
||||||
|
"actions": "$t(common.action_other)"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+292
-23
@@ -24,7 +24,7 @@
|
|||||||
"dismiss": "hylkää",
|
"dismiss": "hylkää",
|
||||||
"favorite": "suosikki",
|
"favorite": "suosikki",
|
||||||
"filter_one": "suodatin",
|
"filter_one": "suodatin",
|
||||||
"filter_other": "suodatinta",
|
"filter_other": "suodattimet",
|
||||||
"filters": "suodattimet",
|
"filters": "suodattimet",
|
||||||
"forceRestartRequired": "käynnistä uudelleen ottaaksesi muutokset käyttöön… sulje ilmoitus käynnistääksesi uudelleen",
|
"forceRestartRequired": "käynnistä uudelleen ottaaksesi muutokset käyttöön… sulje ilmoitus käynnistääksesi uudelleen",
|
||||||
"gap": "väli",
|
"gap": "väli",
|
||||||
@@ -42,11 +42,11 @@
|
|||||||
"note": "huomautus",
|
"note": "huomautus",
|
||||||
"ok": "ok",
|
"ok": "ok",
|
||||||
"owner": "omistaja",
|
"owner": "omistaja",
|
||||||
"path": "reitti",
|
"path": "polku",
|
||||||
"preview": "esikatsele",
|
"preview": "esikatsele",
|
||||||
"previousSong": "edellinen $t(entity.track_one)",
|
"previousSong": "edellinen $t(entity.track_one)",
|
||||||
"resetToDefault": "palauta oletusarvoihin",
|
"resetToDefault": "palauta oletusarvoihin",
|
||||||
"restartRequired": "uudelleen käynnistys vaaditaan",
|
"restartRequired": "vaatii uudelleenkäynnistyksen",
|
||||||
"right": "oikea",
|
"right": "oikea",
|
||||||
"save": "tallenna",
|
"save": "tallenna",
|
||||||
"saveAndReplace": "tallenna ja korvaa",
|
"saveAndReplace": "tallenna ja korvaa",
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
"yes": "kyllä",
|
"yes": "kyllä",
|
||||||
"close": "sulje",
|
"close": "sulje",
|
||||||
"descending": "laskeva",
|
"descending": "laskeva",
|
||||||
"biography": "elämänkerta",
|
"biography": "biografia",
|
||||||
"cancel": "peruuta",
|
"cancel": "peruuta",
|
||||||
"bpm": "bpm",
|
"bpm": "bpm",
|
||||||
"decrease": "pienennä",
|
"decrease": "pienennä",
|
||||||
@@ -93,37 +93,37 @@
|
|||||||
"entity": {
|
"entity": {
|
||||||
"album_one": "albumi",
|
"album_one": "albumi",
|
||||||
"album_other": "albumit",
|
"album_other": "albumit",
|
||||||
"albumArtist_one": "albumi artisti",
|
"albumArtist_one": "albumin artisti",
|
||||||
"albumArtist_other": "albumi artistit",
|
"albumArtist_other": "albumin artistit",
|
||||||
"artistWithCount_one": "{{count}} artisti",
|
"artistWithCount_one": "{{count}} artisti",
|
||||||
"artistWithCount_other": "{{count}} artistia",
|
"artistWithCount_other": "{{count}} artistia",
|
||||||
"playlist_one": "soittolista",
|
"playlist_one": "soittolista",
|
||||||
"playlist_other": "soittolistaa",
|
"playlist_other": "soittolistat",
|
||||||
"playlistWithCount_one": "{{count}} soittolista",
|
"playlistWithCount_one": "{{count}} soittolista",
|
||||||
"playlistWithCount_other": "{{count}} soittolistaa",
|
"playlistWithCount_other": "{{count}} soittolistaa",
|
||||||
"albumArtistCount_one": "{{count}} albumi artisti",
|
"albumArtistCount_one": "{{count}} albumin artisti",
|
||||||
"albumArtistCount_other": "{{count}} albumi artistia",
|
"albumArtistCount_other": "{{count}} albumin artistia",
|
||||||
"albumWithCount_one": "{{count}} albumi",
|
"albumWithCount_one": "{{count}} albumi",
|
||||||
"albumWithCount_other": "{{count}} albumia",
|
"albumWithCount_other": "{{count}} albumia",
|
||||||
"artist_one": "artisti",
|
"artist_one": "artisti",
|
||||||
"artist_other": "artistia",
|
"artist_other": "artistit",
|
||||||
"favorite_one": "suosikki",
|
"favorite_one": "suosikki",
|
||||||
"favorite_other": "suosikkia",
|
"favorite_other": "suosikit",
|
||||||
"folder_one": "kansio",
|
"folder_one": "kansio",
|
||||||
"folder_other": "kansiota",
|
"folder_other": "kansiot",
|
||||||
"folderWithCount_one": "{{count}} kansio",
|
"folderWithCount_one": "{{count}} kansio",
|
||||||
"folderWithCount_other": "{{count}} kansiota",
|
"folderWithCount_other": "{{count}} kansiota",
|
||||||
"genre_one": "genre",
|
"genre_one": "genre",
|
||||||
"genre_other": "genreä",
|
"genre_other": "genret",
|
||||||
"genreWithCount_one": "{{count}} genre",
|
"genreWithCount_one": "{{count}} genre",
|
||||||
"genreWithCount_other": "{{count}} genreä",
|
"genreWithCount_other": "{{count}} genreä",
|
||||||
"smartPlaylist": "älykäs $t(entity.playlist_one)",
|
"smartPlaylist": "älykäs $t(entity.playlist_one)",
|
||||||
"track_one": "raita",
|
"track_one": "raita",
|
||||||
"track_other": "raitaa",
|
"track_other": "raidat",
|
||||||
"trackWithCount_one": "{{count}} raita",
|
"trackWithCount_one": "{{count}} raita",
|
||||||
"trackWithCount_other": "{{count}} raitaa",
|
"trackWithCount_other": "{{count}} raitaa",
|
||||||
"play_one": "{{count}} toista",
|
"play_one": "{{count}} toisto",
|
||||||
"play_other": "{{count}} toistaa",
|
"play_other": "{{count}} toistoa",
|
||||||
"song_one": "kappale",
|
"song_one": "kappale",
|
||||||
"song_other": "kappaleet"
|
"song_other": "kappaleet"
|
||||||
},
|
},
|
||||||
@@ -139,7 +139,7 @@
|
|||||||
"musicbrainz": "Avaa MusicBrainz:ssä"
|
"musicbrainz": "Avaa MusicBrainz:ssä"
|
||||||
},
|
},
|
||||||
"goToPage": "mene sivulle",
|
"goToPage": "mene sivulle",
|
||||||
"moveToBottom": "siirry alas",
|
"moveToBottom": "siirry pohjalle",
|
||||||
"moveToTop": "siirry ylös",
|
"moveToTop": "siirry ylös",
|
||||||
"addToFavorites": "lisää kohteeseen $t(entity.favorite_other)",
|
"addToFavorites": "lisää kohteeseen $t(entity.favorite_other)",
|
||||||
"addToPlaylist": "lisää kohteeseen $t(entity.playlist_one)",
|
"addToPlaylist": "lisää kohteeseen $t(entity.playlist_one)",
|
||||||
@@ -168,12 +168,12 @@
|
|||||||
"credentialsRequired": "käyttäjätunnuksia vaaditaan",
|
"credentialsRequired": "käyttäjätunnuksia vaaditaan",
|
||||||
"loginRateError": "liian monta kirjautumisyritystä, kokeile muutaman sekuntin päästä uudestaan",
|
"loginRateError": "liian monta kirjautumisyritystä, kokeile muutaman sekuntin päästä uudestaan",
|
||||||
"mpvRequired": "MPV vaadittu",
|
"mpvRequired": "MPV vaadittu",
|
||||||
"networkError": "yhteysvirhe",
|
"networkError": "verkkoyhteysvirhe",
|
||||||
"openError": "tiedostoa ei voitu avata",
|
"openError": "tiedostoa ei voitu avata",
|
||||||
"localFontAccessDenied": "paikallisiin fontteihin pääsy on estetty",
|
"localFontAccessDenied": "paikallisiin fontteihin pääsy on kielletty",
|
||||||
"playbackError": "mediaa toistaessa tapahtui virhe",
|
"playbackError": "mediaa toistaessa tapahtui virhe",
|
||||||
"remotePortWarning": "käynnistä palvelin uudestaan ottaaksesi uuden portin käyttöön",
|
"remotePortWarning": "käynnistä palvelin uudestaan ottaaksesi uuden portin käyttöön",
|
||||||
"endpointNotImplementedError": "endpoint {{endpoint}} ei ole toteutettu {{serverType}} varten"
|
"endpointNotImplementedError": "päätepiste {{endpoint}} ei ole toteutettu {{serverType}} varten"
|
||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
"album": "$t(entity.album_one)",
|
"album": "$t(entity.album_one)",
|
||||||
@@ -221,7 +221,7 @@
|
|||||||
},
|
},
|
||||||
"form": {
|
"form": {
|
||||||
"addServer": {
|
"addServer": {
|
||||||
"input_legacyAuthentication": "käytä vanhaa kirjautumista",
|
"input_legacyAuthentication": "käytä vanhaa kirjautumistapaa",
|
||||||
"ignoreCors": "ohita CORS ($t(common.restartRequired))",
|
"ignoreCors": "ohita CORS ($t(common.restartRequired))",
|
||||||
"input_name": "palvelimen nimi",
|
"input_name": "palvelimen nimi",
|
||||||
"ignoreSsl": "ohita SSL ($t(common.restartRequired))",
|
"ignoreSsl": "ohita SSL ($t(common.restartRequired))",
|
||||||
@@ -243,7 +243,7 @@
|
|||||||
},
|
},
|
||||||
"addToPlaylist": {
|
"addToPlaylist": {
|
||||||
"input_skipDuplicates": "ohita kaksoiskappaleet",
|
"input_skipDuplicates": "ohita kaksoiskappaleet",
|
||||||
"success": "lisätty $t(entity.trackWithCount, {\"count\": {{message}} }) soittolistalle $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
|
"success": "$t(entity.trackWithCount, {\"count\": {{message}} }) lisätty $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
|
||||||
"title": "lisää soittolistalle $t(entity.playlist_one)",
|
"title": "lisää soittolistalle $t(entity.playlist_one)",
|
||||||
"input_playlists": "$t(entity.playlist_other)"
|
"input_playlists": "$t(entity.playlist_other)"
|
||||||
},
|
},
|
||||||
@@ -310,7 +310,201 @@
|
|||||||
"artistConfiguration": "albumin artistin sivun hallinta",
|
"artistConfiguration": "albumin artistin sivun hallinta",
|
||||||
"audioDevice_description": "valitse toistossa käytettävä äänilaite (vain verkkosoittimessa)",
|
"audioDevice_description": "valitse toistossa käytettävä äänilaite (vain verkkosoittimessa)",
|
||||||
"applicationHotkeys": "sovelluksen pikanäppäimet",
|
"applicationHotkeys": "sovelluksen pikanäppäimet",
|
||||||
"albumBackground": "albumin taustakuva"
|
"albumBackground": "albumin taustakuva",
|
||||||
|
"customCss": "oma css",
|
||||||
|
"customFontPath_description": "asettaa polun mukautetulle fontille jota sovellus käyttää",
|
||||||
|
"homeConfiguration": "koti sivun muokkaus",
|
||||||
|
"homeConfiguration_description": "määritä mitä osioita näkyy, ja missä järjestyksessä, koti sivulla",
|
||||||
|
"gaplessAudio_optionWeak": "heikko (suositus)",
|
||||||
|
"genreBehavior_description": "määrittää avautuuko generä painettaessa oletuksena ääniraita vaiko albumi listassa",
|
||||||
|
"hotkey_browserBack": "selain takaisin",
|
||||||
|
"hotkey_playbackPlay": "toista",
|
||||||
|
"hotkey_playbackPlayPause": "toista / tauko",
|
||||||
|
"hotkey_playbackPrevious": "edellinen ääniraita",
|
||||||
|
"hotkey_rate3": "arvostelu 3 tähteä",
|
||||||
|
"hotkey_playbackStop": "lopeta",
|
||||||
|
"hotkey_rate4": "arvostelu 4 tähteä",
|
||||||
|
"hotkey_rate1": "arvostelu 1 tähti",
|
||||||
|
"hotkey_rate2": "arvostelu 2 tähteä",
|
||||||
|
"hotkey_unfavoriteCurrentSong": "poista suosikeista $t(common.currentSong)",
|
||||||
|
"fontType_description": "sisäänrakennettu fontti valitsee yhden Feishinin tuomista fonteista. järjestelmän fontti antaa sinun valita minkä tahansa käyttöjärjestelmään asennetun fontin. mukautettu antaa sinun tuoda oman fontin",
|
||||||
|
"fontType_optionBuiltIn": "sisäänrakennettu fontti",
|
||||||
|
"fontType_optionSystem": "järjestelmän fontti",
|
||||||
|
"fontType_optionCustom": "mukautettu fontti",
|
||||||
|
"hotkey_favoriteCurrentSong": "lisää suosikiksi $t(common.currentSong)",
|
||||||
|
"hotkey_favoritePreviousSong": "lisää suosikiksi $t(common.previousSong)",
|
||||||
|
"hotkey_rate5": "arvostelu 5 tähteä",
|
||||||
|
"hotkey_skipBackward": "ohita taaksepäin",
|
||||||
|
"hotkey_skipForward": "ohita eteenpäin",
|
||||||
|
"font": "kirjaisin",
|
||||||
|
"font_description": "asettaa fontin jota sovellus käyttää",
|
||||||
|
"discordApplicationId": "{{discord}} sovelluksen tunnus",
|
||||||
|
"hotkey_globalSearch": "globaali haku",
|
||||||
|
"hotkey_playbackNext": "seuraava ääniraita",
|
||||||
|
"hotkey_browserForward": "selain eteenpäin",
|
||||||
|
"hotkey_playbackPause": "tauko",
|
||||||
|
"hotkey_localSearch": "hae sivulta",
|
||||||
|
"customFontPath": "mukautetun fontin polku",
|
||||||
|
"fontType": "fonttityyppi",
|
||||||
|
"hotkey_unfavoritePreviousSong": "poista suosikeista $t(common.previousSong)",
|
||||||
|
"customCss_description": "mukautettu CSS-sisältö. Huomautus: content- ja etä-URL-osoitteet ovat estettyjä ominaisuuksia. Esikatselu sisällöstäsi on alla. Lisäkenttiä, joita et ole määrittänyt, on näkyvissä puhdistuksen vuoksi.",
|
||||||
|
"customCssNotice": "Varoitus: vaikka jonkinlainen puhdistus onkin tehty (url()- ja content:-komentojen estäminen), mukautetun CSS:n käyttäminen voi silti aiheuttaa riskejä muuttamalla käyttöliittymää.",
|
||||||
|
"disableLibraryUpdateOnStartup": "poista uusimman version tarkistus käynnistyksen yhteydessä käytöstä",
|
||||||
|
"disableAutomaticUpdates": "poista automaattiset päivitykset käytöstä",
|
||||||
|
"discordIdleStatus": "näytä rich presencen käyttämätön tila",
|
||||||
|
"discordIdleStatus_description": "kun käytössä, päivitä tila kun soitin on käyttämättömänä",
|
||||||
|
"doubleClickBehavior": "lisää kaikki haetut kappaleet soittojonoon tuplaklikkauksella",
|
||||||
|
"discordUpdateInterval_description": "päivitysväli sekunnteina (vähintään 15 sekunttia)",
|
||||||
|
"discordRichPresence": "{{discord}} rich presence",
|
||||||
|
"discordRichPresence_description": "ota toiston tila käyttöön {{discord}}n rich presence-toiminnossa. Kuvakkeiden avaimet ovat {{icon}}, {{playing}} ja {{paused}}. ",
|
||||||
|
"discordUpdateInterval": "{{discord}} rich presencen päivitysväli",
|
||||||
|
"enableRemote": "aktivoi etäohjauspalvelin",
|
||||||
|
"externalLinks_description": "ottaa ulkoiset linkit (Last.fm, MusicBrainz) artistien/albumien sivuilla",
|
||||||
|
"exitToTray": "sulje tehtäväpalkkiin",
|
||||||
|
"doubleClickBehavior_description": "jos päällä, kaikki hakutuloksissa olevat kappaleet lisätään soittojonoon. muuten vain napsautettu kappale lisätään jonoon",
|
||||||
|
"discordApplicationId_description": "{{discord}}n ohjelma-ID rich presenceä varten (oletuksena {{defaultId}})",
|
||||||
|
"enableRemote_description": "aktivoi etäohjauspalvelimen, jolla muut laitteet voivat ohjata sovellusta",
|
||||||
|
"externalLinks": "näytä ulkoiset linkit",
|
||||||
|
"exitToTray_description": "sovellus suljetaan tehtäväpalkkiin",
|
||||||
|
"discordListening_description": "näytä status kuuntelee pelaa sijaan",
|
||||||
|
"discordListening": "näytä status kuuntelee",
|
||||||
|
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
||||||
|
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
|
||||||
|
"lastfmApiKey_description": "API-avain {{lastfm}}:lle. tarvitaan kansikuvia varten",
|
||||||
|
"passwordStore_description": "mitä salasanojen/avaimien tallennusta käytetään. muuta tätä, jos sinulla on ongelmia salasanojen tallennuksessa.",
|
||||||
|
"floatingQueueArea_description": "näyttää ikonin ikkunan oikealla reunalla jonon katselua varten",
|
||||||
|
"homeFeature_description": "ohjaa näytetäänkö suuri esittelykaruselli kotisivulla",
|
||||||
|
"hotkey_rate0": "arvostelun tyhjennys",
|
||||||
|
"hotkey_togglePreviousSongFavorite": "vaihda $t(common.previousSong) suosikkiasetus",
|
||||||
|
"imageAspectRatio_description": "jos käytössä, kansikuvat näytetään niiden alkuperäisellä kuvasuhteella. jos kuvasuhde ei ole 1:1, jäljelle jäävä tila jää tyhjäksi",
|
||||||
|
"language_description": "asettaa sovelluksen kielen $t(common.restartRequired)",
|
||||||
|
"lyricFetch": "hae sanoitukset internetistä",
|
||||||
|
"lyricFetchProvider_description": "valitse lähteet sanoituksien hakua varten. lähteiden järjestys on se järjestys, jossa ne tiedustellaan",
|
||||||
|
"minimumScrobblePercentage": "pienin skrobblauksen kesto (prosenttia)",
|
||||||
|
"mpvExecutablePath": "mpv:n suoritettavan tiedoston polku",
|
||||||
|
"mpvExecutablePath_description": "asettaa mpv:n suoritettavan tiedoston polun. ollessa tyhjä, käytetään oletuspolkua",
|
||||||
|
"mpvExtraParameters_help": "yksi per rivi",
|
||||||
|
"playButtonBehavior_optionPlay": "$t(player.play)",
|
||||||
|
"genreBehavior": "genre-sivun oletustoiminta",
|
||||||
|
"globalMediaHotkeys": "globaalit median pikanäppäimet",
|
||||||
|
"globalMediaHotkeys_description": "ota käyttöön tai poista käytöstä järjestelmän median pikanäppäinten käyttö toiston hallintaa",
|
||||||
|
"hotkey_toggleCurrentSongFavorite": "vaihda $t(common.currentSong) suosikkiasetus",
|
||||||
|
"imageAspectRatio": "käytä alkuperäistä kansikuvan kuvasuhdetta",
|
||||||
|
"language": "kieli",
|
||||||
|
"lyricOffset_description": "siirrä sanoituksia valitun ajan millisekuntteina",
|
||||||
|
"minimizeToTray": "pienennä ilmaisinalueelle",
|
||||||
|
"gaplessAudio_description": "asettaa tauottoman toiston asetukset mpv:hen",
|
||||||
|
"hotkey_volumeDown": "äänenvoimakkuuden vähentäminen",
|
||||||
|
"hotkey_zoomIn": "lähennä",
|
||||||
|
"lyricFetch_description": "hae sanoitukset eri lähteistä internetissä",
|
||||||
|
"lyricFetchProvider": "lähteet sanoituksia varten",
|
||||||
|
"lyricOffset": "sanotuksien siirto (ms)",
|
||||||
|
"mpvExtraParameters": "mpv:n parametrit",
|
||||||
|
"followLyric": "seuraa lyriikoita",
|
||||||
|
"followLyric_description": "vieritä lyriikat tämänhetkiseen paikkaan",
|
||||||
|
"hotkey_toggleQueue": "vaihda jono",
|
||||||
|
"minimumScrobblePercentage_description": "vähimmäisprosentti kappaleesta, joka on soitettava ennen kuin se skrobblataan",
|
||||||
|
"minimumScrobbleSeconds": "pienin skrobblaus (sekunttia)",
|
||||||
|
"minimumScrobbleSeconds_description": "vähimmäisaika kappaleesta, joka on soitettava ennen kuin se skrobblataan",
|
||||||
|
"passwordStore": "salasanojen/avaimien tallennus",
|
||||||
|
"hotkey_volumeUp": "äänenvoimakkuuden lisääminen",
|
||||||
|
"hotkey_toggleShuffle": "vaihda sekoitus",
|
||||||
|
"hotkey_volumeMute": "mykistäminen",
|
||||||
|
"lastfmApiKey": "{{lastfm}} API-avain",
|
||||||
|
"minimizeToTray_description": "pienennä sovellus ilmaisinalueelle",
|
||||||
|
"playButtonBehavior_optionAddLast": "$t(player.addLast)",
|
||||||
|
"hotkey_zoomOut": "loitonna",
|
||||||
|
"floatingQueueArea": "näytä kelluvan jonon avausalue",
|
||||||
|
"homeFeature": "kodin esittelykaruselli",
|
||||||
|
"hotkey_toggleFullScreenPlayer": "vaihda kokonäytön toistin",
|
||||||
|
"hotkey_toggleRepeat": "vaihda kertaus",
|
||||||
|
"gaplessAudio": "tauoton toisto",
|
||||||
|
"transcodeFormat_description": "valitsee transkoodattavan formaatin. jätä tyhjäksi palvelimen valintaa varten",
|
||||||
|
"replayGainMode_optionNone": "$t(common.none)",
|
||||||
|
"replayGainMode_optionTrack": "$t(entity.track_one)",
|
||||||
|
"themeDark": "teema (tumma)",
|
||||||
|
"transcodeNote": "tulee voimaan 1 (web) - 2 (mpv) kappaleen jälkeen",
|
||||||
|
"translationApiKey_description": "API-avain käännöstä varten (tukee vain globaalia palvelun palvelupistettä)",
|
||||||
|
"playbackStyle_description": "valitse toiston tyyli, jota käytetään soittimessa",
|
||||||
|
"transcode_description": "ottaa transkoodaksen käyttöön eri formaateille",
|
||||||
|
"transcodeBitrate": "transkoodattava bittinopeus",
|
||||||
|
"translationApiProvider": "käännös-API:n palveluntarjoaja",
|
||||||
|
"trayEnabled_description": "näytä/piilota järjestelmäpalkin kuvake/valikko. jos poistettu käytöstä, myös pienennä/sulje järjestelmäpalkkiin -toiminto poistetaan käytöstä",
|
||||||
|
"windowBarStyle_description": "valitse ikkunapalkin tyyli",
|
||||||
|
"webAudio": "käytä web-ääntä",
|
||||||
|
"windowBarStyle": "ikkunapalkin tyyli",
|
||||||
|
"zoom": "zoomausprosentti",
|
||||||
|
"playbackStyle": "toiston tyyli",
|
||||||
|
"remotePassword": "kauko-ohjauspalvelimen salasana",
|
||||||
|
"remoteUsername_description": "asettaa käyttäjänimen kauko-ohjauspalvelimelle. jos sekä käyttäjätunnus, että salasana ovat tyhjänä, todennus poistetaan käytöstä",
|
||||||
|
"skipPlaylistPage": "ohita soittolistojen sivu",
|
||||||
|
"themeDark_description": "asettaa tumman teeman käytettäväksi sovelluksessa",
|
||||||
|
"playbackStyle_optionCrossFade": "ristivaihto",
|
||||||
|
"playbackStyle_optionNormal": "normaali",
|
||||||
|
"playButtonBehavior": "toistopainikkeen toiminta",
|
||||||
|
"playButtonBehavior_description": "asettaa toistopainikkeen oletustoiminnan lisättäessä kappaleita jonoon",
|
||||||
|
"remotePort": "kauko-ohjauspalvelimen portti",
|
||||||
|
"replayGainMode": "{{ReplayGain}} tila",
|
||||||
|
"sampleRate_description": "valitse käytettävä näytteenottotaajuus, jos valittu näytetaajuus poikkeaa nykyisen median taajuudesta. arvo, joka on alle 8 000, käyttää oletustaajuutta",
|
||||||
|
"skipDuration": "ohituksen kesto",
|
||||||
|
"sidePlayQueueStyle_description": "asettaa tyylin sivupalkin toistojonolle",
|
||||||
|
"sidePlayQueueStyle_optionAttached": "liitetty",
|
||||||
|
"sidePlayQueueStyle_optionDetached": "irrotettu",
|
||||||
|
"startMinimized_description": "käynnistä sovellus järjestelmäpalkissa",
|
||||||
|
"theme": "teema",
|
||||||
|
"useSystemTheme_description": "seuraa järjestelmän määrittämää asetusta vaalealle tai tummalle asetukselle",
|
||||||
|
"remoteUsername": "kauko-ohjauspalvelimen käyttäjänimi",
|
||||||
|
"remotePort_description": "asettaa kauko-ohjauspalvelimen portin",
|
||||||
|
"remotePassword_description": "asettaa kauko-ohjauspalvelimen salasanan. Nämä tunnukset siirretään oletuksena turvattomasti, joten sinun kuuluisi käyttää uniikkia salasanaa, josta et välitä",
|
||||||
|
"replayGainClipping": "{{ReplayGain}} leikkaus",
|
||||||
|
"replayGainClipping_description": "Estää {{ReplayGain}}n aiheuttaman leikkauksen laskemalla vahvistusta automaatisesti",
|
||||||
|
"replayGainFallback": "{{ReplayGain}} palautus",
|
||||||
|
"playerAlbumArtResolution_description": "suurien kansikuvien resoluutio soittimen esikatselussa. suurempi tekee niistä terävempiä, mutta voi hidastaa latausta. oletuksena on 0, joka tarkoittaa automaattista",
|
||||||
|
"replayGainMode_optionAlbum": "$t(entity.album_one)",
|
||||||
|
"replayGainPreamp": "{{ReplayGain}} esivahvistus (dB)",
|
||||||
|
"scrobble_description": "skrobblaa toistot mediapalvelimellesi",
|
||||||
|
"replayGainPreamp_description": "säätää esivahvistuksen määrää {{ReplayGain}} arvoon",
|
||||||
|
"showSkipButtons": "näytä ohituspainikkeet",
|
||||||
|
"showSkipButtons_description": "näytä tai piilota soitinpalkin ohituspainikkeet",
|
||||||
|
"showSkipButton": "näytä ohituspainikkeet",
|
||||||
|
"showSkipButton_description": "näytä tai piilota soitinpalkin ohituspainikkeet",
|
||||||
|
"sidebarPlaylistList": "sivupakin soittolistojen lista",
|
||||||
|
"skipDuration_description": "asettaa ohitettavan ajan käytettäessä soitinpalkin ohituspainikkeita",
|
||||||
|
"volumeWidth": "äänenvoimakkuuden säätimen leveys",
|
||||||
|
"sidebarCollapsedNavigation_description": "näytä tai piilota navigointi romautetussa sivupalkissa",
|
||||||
|
"sidebarConfiguration": "sivupalkin asetukset",
|
||||||
|
"sidebarConfiguration_description": "valitse kohteet ja niiden järjestys sivupalkissa",
|
||||||
|
"volumeWidth_description": "äänenvoimakkuuden säätimen leveys",
|
||||||
|
"playerAlbumArtResolution": "soittimen kansikuvien resoluutio",
|
||||||
|
"playerbarOpenDrawer": "toistipalkin kokoruudun kytkin",
|
||||||
|
"playerbarOpenDrawer_description": "sallii toistopalkin klikkaamisen avaamaan kokonäytön soittimen",
|
||||||
|
"replayGainFallback_description": "asetettava vahvistus desibelinä (dB), jos tiedostolla ei ole {{ReplayGain}} tageja",
|
||||||
|
"replayGainMode_description": "säätää äänenvoimmakkuutta {{ReplayGain}} arvojen mukaisesti tiedoston metadatasta",
|
||||||
|
"sampleRate": "näytteenottotaajuus",
|
||||||
|
"savePlayQueue": "tallenna toistojono",
|
||||||
|
"savePlayQueue_description": "tallenna toistojono, kun sovellus suljetaan ja avaa se uudestaan, kun sovellus avataan",
|
||||||
|
"scrobble": "skrobblaus",
|
||||||
|
"sidebarCollapsedNavigation": "sivupalkin (romautettu) navigointi",
|
||||||
|
"sidebarPlaylistList_description": "näytä tai piilota soittolistojen lista sivupalkissa",
|
||||||
|
"sidePlayQueueStyle": "sivupalkin jonon tyyli",
|
||||||
|
"skipPlaylistPage_description": "navigoidessa soittolistaan, mene soittolistan kappaleiden listaan oletussivun sijaan",
|
||||||
|
"theme_description": "asettaa ohjelmassa käytettävän teeman",
|
||||||
|
"themeLight": "teema (vaalea)",
|
||||||
|
"themeLight_description": "asettaa vaalean teeman käytettäväksi sovelluksessa",
|
||||||
|
"transcode": "ota transkoodaus käyttöön",
|
||||||
|
"transcodeBitrate_description": "valitsee transkoodattavan bittinopeuden. 0 tarkoittaa palvelimen valintaa",
|
||||||
|
"transcodeFormat": "transkoodattava formaatti",
|
||||||
|
"translationApiProvider_description": "palveluntarjoajan API käännöstä varten",
|
||||||
|
"translationApiKey": "käännöksen API-avain",
|
||||||
|
"translationTargetLanguage": "käännöksen kohdekieli",
|
||||||
|
"translationTargetLanguage_description": "kohdekieli käännöstä varten",
|
||||||
|
"trayEnabled": "näytä järjestelmäpalkki",
|
||||||
|
"volumeWheelStep_description": "äänenvoimakkuuden muutoksen suuruus rullattaessa hiiren rullalla äänenvoimakkuuden säätimen päällä",
|
||||||
|
"zoom_description": "asettaa sovelluksen zoomausprosentin",
|
||||||
|
"webAudio_description": "käytä web-ääntä. tämä mahdollistaa edistyneet ominaisuudet, kuten replaygainin. poista käytöstä, jos koet ongelmia",
|
||||||
|
"startMinimized": "käynnistä pienennettynä",
|
||||||
|
"useSystemTheme": "käytä järjestelmän teemaa",
|
||||||
|
"volumeWheelStep": "äänenvoimakkuusrullan askel"
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
"itemDetail": {
|
"itemDetail": {
|
||||||
@@ -494,5 +688,80 @@
|
|||||||
"repeat_off": "kertaus pois päältä",
|
"repeat_off": "kertaus pois päältä",
|
||||||
"shuffle_off": "sekoitus pois päältä",
|
"shuffle_off": "sekoitus pois päältä",
|
||||||
"toggleFullscreenPlayer": "vaihda kokoruudun soittimeen"
|
"toggleFullscreenPlayer": "vaihda kokoruudun soittimeen"
|
||||||
|
},
|
||||||
|
"table": {
|
||||||
|
"config": {
|
||||||
|
"general": {
|
||||||
|
"gap": "$t(common.gap)",
|
||||||
|
"size": "$t(common.size)",
|
||||||
|
"autoFitColumns": "sovita sarakkeet",
|
||||||
|
"followCurrentSong": "seuraa nykyistä kappaletta",
|
||||||
|
"displayType": "näytön tyyppi",
|
||||||
|
"itemGap": "kohteiden väli (px)",
|
||||||
|
"itemSize": "kohteiden koko (px)",
|
||||||
|
"tableColumns": "taulukon sarakkeet"
|
||||||
|
},
|
||||||
|
"label": {
|
||||||
|
"channels": "$t(common.channel_other)",
|
||||||
|
"trackNumber": "raidan numero",
|
||||||
|
"album": "$t(entity.album_one)",
|
||||||
|
"actions": "$t(common.action_other)",
|
||||||
|
"codec": "$t(common.codec)",
|
||||||
|
"dateAdded": "lisäyspäivämäärä",
|
||||||
|
"owner": "$t(common.owner)",
|
||||||
|
"path": "$t(common.path)",
|
||||||
|
"albumArtist": "$t(entity.albumArtist_one)",
|
||||||
|
"artist": "$t(entity.artist_one)",
|
||||||
|
"discNumber": "levyn numero",
|
||||||
|
"duration": "$t(common.duration)",
|
||||||
|
"favorite": "$t(common.favorite)",
|
||||||
|
"lastPlayed": "viimeksi soitettu",
|
||||||
|
"note": "$t(common.note)",
|
||||||
|
"titleCombined": "$t(common.title) (yhdistetty)",
|
||||||
|
"rowIndex": "rivin indeksi",
|
||||||
|
"biography": "$t(common.biography)",
|
||||||
|
"bitrate": "$t(common.bitrate)",
|
||||||
|
"bpm": "$t(common.bpm)",
|
||||||
|
"genre": "$t(entity.genre_one)",
|
||||||
|
"playCount": "toistojen lukumäärä",
|
||||||
|
"rating": "$t(common.rating)",
|
||||||
|
"releaseDate": "julkaisupäivämäärä",
|
||||||
|
"size": "$t(common.size)",
|
||||||
|
"songCount": "$t(entity.track_other)",
|
||||||
|
"title": "$t(common.title)",
|
||||||
|
"year": "$t(common.year)"
|
||||||
|
},
|
||||||
|
"view": {
|
||||||
|
"table": "taulukko",
|
||||||
|
"card": "kortti",
|
||||||
|
"poster": "juliste"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"column": {
|
||||||
|
"releaseYear": "vuosi",
|
||||||
|
"bpm": "bpm",
|
||||||
|
"artist": "$t(entity.artist_one)",
|
||||||
|
"biography": "biografia",
|
||||||
|
"dateAdded": "lisäyspäivämäärä",
|
||||||
|
"album": "albumi",
|
||||||
|
"albumArtist": "albumin artisti",
|
||||||
|
"lastPlayed": "viimeksi toistettu",
|
||||||
|
"path": "polku",
|
||||||
|
"size": "$t(common.size)",
|
||||||
|
"songCount": "$t(entity.track_other)",
|
||||||
|
"title": "nimi",
|
||||||
|
"trackNumber": "raita",
|
||||||
|
"codec": "$t(common.codec)",
|
||||||
|
"comment": "kommentti",
|
||||||
|
"albumCount": "$t(entity.album_other)",
|
||||||
|
"bitrate": "bittinopeus",
|
||||||
|
"channels": "$t(common.channel_other)",
|
||||||
|
"discNumber": "levy",
|
||||||
|
"favorite": "suosikki",
|
||||||
|
"genre": "$t(entity.genre_one)",
|
||||||
|
"playCount": "toistoja",
|
||||||
|
"rating": "arvostelu",
|
||||||
|
"releaseDate": "julkaisupäivämäärä"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+98
-19
@@ -20,7 +20,8 @@
|
|||||||
"openIn": {
|
"openIn": {
|
||||||
"lastfm": "Otwórz w Last.fm",
|
"lastfm": "Otwórz w Last.fm",
|
||||||
"musicbrainz": "Otwórz w MusicBrainz"
|
"musicbrainz": "Otwórz w MusicBrainz"
|
||||||
}
|
},
|
||||||
|
"moveToNext": "przesuń na następne"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"increase": "zwiększ",
|
"increase": "zwiększ",
|
||||||
@@ -113,7 +114,8 @@
|
|||||||
"trackPeak": "peak utworu",
|
"trackPeak": "peak utworu",
|
||||||
"codec": "kodek",
|
"codec": "kodek",
|
||||||
"preview": "podgląd",
|
"preview": "podgląd",
|
||||||
"close": "zamknij"
|
"close": "zamknij",
|
||||||
|
"translation": "tłumaczenie"
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
"genre_one": "gatunek",
|
"genre_one": "gatunek",
|
||||||
@@ -161,7 +163,13 @@
|
|||||||
"genreWithCount_many": "{{count}} gatunków",
|
"genreWithCount_many": "{{count}} gatunków",
|
||||||
"trackWithCount_one": "{{count}} utwór",
|
"trackWithCount_one": "{{count}} utwór",
|
||||||
"trackWithCount_few": "{{count}} utwory",
|
"trackWithCount_few": "{{count}} utwory",
|
||||||
"trackWithCount_many": "{{count}} utworów"
|
"trackWithCount_many": "{{count}} utworów",
|
||||||
|
"play_one": "{{count}} odtworzenie",
|
||||||
|
"play_few": "{{count}} odtworzenia",
|
||||||
|
"play_many": "{{count}} odtworzeń",
|
||||||
|
"song_one": "piosenka",
|
||||||
|
"song_few": "piosenki",
|
||||||
|
"song_many": "piosenek"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"remotePortWarning": "uruchom ponownie serwer aby używać nowego portu",
|
"remotePortWarning": "uruchom ponownie serwer aby używać nowego portu",
|
||||||
@@ -259,7 +267,7 @@
|
|||||||
"error_savePassword": "wystąpił błąd podczas próby zapisania hasła"
|
"error_savePassword": "wystąpił błąd podczas próby zapisania hasła"
|
||||||
},
|
},
|
||||||
"addToPlaylist": {
|
"addToPlaylist": {
|
||||||
"success": "dodano {{message}} $t(entity.track_other) do {{numOfPlaylists}} $t(entity.playlist_other)",
|
"success": "dodano $t(entity.trackWithCount, {\"count\": {{message}} }) do $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
|
||||||
"title": "dodano do $t(entity.playlist_one)",
|
"title": "dodano do $t(entity.playlist_one)",
|
||||||
"input_skipDuplicates": "pomiń duplikaty",
|
"input_skipDuplicates": "pomiń duplikaty",
|
||||||
"input_playlists": "$t(entity.playlist_other)"
|
"input_playlists": "$t(entity.playlist_other)"
|
||||||
@@ -278,7 +286,9 @@
|
|||||||
"title": "wyszukiwanie tekstów"
|
"title": "wyszukiwanie tekstów"
|
||||||
},
|
},
|
||||||
"editPlaylist": {
|
"editPlaylist": {
|
||||||
"title": "edytuj $t(entity.playlist_one)"
|
"title": "edytuj $t(entity.playlist_one)",
|
||||||
|
"success": "$t(entity.playlist_one) zaktualizowana pomyślnie",
|
||||||
|
"publicJellyfinNote": "Z jakiegoś powodu Jellyfin nie udostępnia informacji na temat publiczności playlisty. Jeżeli chcesz, aby ta pozostała publiczna, mniej wybraną poniższą opcję"
|
||||||
},
|
},
|
||||||
"shareItem": {
|
"shareItem": {
|
||||||
"allowDownloading": "zezwól na pobieranie",
|
"allowDownloading": "zezwól na pobieranie",
|
||||||
@@ -304,11 +314,14 @@
|
|||||||
"useImageAspectRatio": "użyj współczynnika proporcji obrazu",
|
"useImageAspectRatio": "użyj współczynnika proporcji obrazu",
|
||||||
"lyricGap": "odstępy tekstu",
|
"lyricGap": "odstępy tekstu",
|
||||||
"dynamicImageBlur": "rozmiar rozmycia obrazu",
|
"dynamicImageBlur": "rozmiar rozmycia obrazu",
|
||||||
"dynamicIsImage": "włącz obraz w tle"
|
"dynamicIsImage": "włącz obraz w tle",
|
||||||
|
"lyricOffset": "opóźnienie tekstów (ms)"
|
||||||
},
|
},
|
||||||
"upNext": "następny",
|
"upNext": "następny",
|
||||||
"lyrics": "tekst",
|
"lyrics": "tekst",
|
||||||
"related": "powiązane"
|
"related": "powiązane",
|
||||||
|
"visualizer": "wizualizer",
|
||||||
|
"noLyrics": "nie znaleziono tekstu"
|
||||||
},
|
},
|
||||||
"appMenu": {
|
"appMenu": {
|
||||||
"selectServer": "wybierz serwer",
|
"selectServer": "wybierz serwer",
|
||||||
@@ -340,11 +353,16 @@
|
|||||||
"numberSelected": "zaznaczono {{count}}",
|
"numberSelected": "zaznaczono {{count}}",
|
||||||
"removeFromQueue": "$t(action.removeFromQueue)",
|
"removeFromQueue": "$t(action.removeFromQueue)",
|
||||||
"shareItem": "udostępnij pozycję",
|
"shareItem": "udostępnij pozycję",
|
||||||
"showDetails": "zobacz informacje"
|
"showDetails": "zobacz informacje",
|
||||||
|
"download": "pobierz",
|
||||||
|
"playShuffled": "$t(player.shuffle)",
|
||||||
|
"playSimilarSongs": "$t(player.playSimilarSongs)",
|
||||||
|
"moveToNext": "$t(action.moveToNext)"
|
||||||
},
|
},
|
||||||
"albumDetail": {
|
"albumDetail": {
|
||||||
"moreFromArtist": "więcej od $t(entity.artist_one)",
|
"moreFromArtist": "więcej od $t(entity.artist_one)",
|
||||||
"moreFromGeneric": "więcej od {{item}}"
|
"moreFromGeneric": "więcej od {{item}}",
|
||||||
|
"released": "wydany"
|
||||||
},
|
},
|
||||||
"albumArtistList": {
|
"albumArtistList": {
|
||||||
"title": "$t(entity.albumArtist_other)"
|
"title": "$t(entity.albumArtist_other)"
|
||||||
@@ -384,7 +402,8 @@
|
|||||||
"playbackTab": "odtworzenia",
|
"playbackTab": "odtworzenia",
|
||||||
"generalTab": "ogólne",
|
"generalTab": "ogólne",
|
||||||
"hotkeysTab": "skróty klawiszowe",
|
"hotkeysTab": "skróty klawiszowe",
|
||||||
"windowTab": "okno"
|
"windowTab": "okno",
|
||||||
|
"advanced": "zaawansowane"
|
||||||
},
|
},
|
||||||
"trackList": {
|
"trackList": {
|
||||||
"title": "$t(entity.track_other)",
|
"title": "$t(entity.track_other)",
|
||||||
@@ -417,6 +436,17 @@
|
|||||||
"copyPath": "kopiuj ścieżkę do schowka",
|
"copyPath": "kopiuj ścieżkę do schowka",
|
||||||
"copiedPath": "ścieżka została skopiowana pomyślnie",
|
"copiedPath": "ścieżka została skopiowana pomyślnie",
|
||||||
"openFile": "pokaż utwór w menedżerze plików"
|
"openFile": "pokaż utwór w menedżerze plików"
|
||||||
|
},
|
||||||
|
"manageServers": {
|
||||||
|
"title": "zarządzaj serwerami",
|
||||||
|
"url": "URL",
|
||||||
|
"username": "nazwa użytkownika",
|
||||||
|
"removeServer": "usuń serwer",
|
||||||
|
"serverDetails": "szczegóły serwera",
|
||||||
|
"editServerDetailsTooltip": "edytuj szczegóły serwera"
|
||||||
|
},
|
||||||
|
"playlist": {
|
||||||
|
"reorder": "zmiana kolejności jest możliwa tylko podczas sortowania według id"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
@@ -431,7 +461,7 @@
|
|||||||
"skip_back": "przeskocz do tyłu",
|
"skip_back": "przeskocz do tyłu",
|
||||||
"favorite": "ulubione",
|
"favorite": "ulubione",
|
||||||
"next": "następny",
|
"next": "następny",
|
||||||
"shuffle": "losowa kolejność",
|
"shuffle": "odtwarzaj losowo",
|
||||||
"playbackFetchNoResults": "nie znaleziono utworów",
|
"playbackFetchNoResults": "nie znaleziono utworów",
|
||||||
"playbackFetchInProgress": "wczytywanie utworów…",
|
"playbackFetchInProgress": "wczytywanie utworów…",
|
||||||
"addNext": "dodaj następny",
|
"addNext": "dodaj następny",
|
||||||
@@ -448,7 +478,9 @@
|
|||||||
"shuffle_off": "losowa kolejność wyłączona",
|
"shuffle_off": "losowa kolejność wyłączona",
|
||||||
"addLast": "dodaj na końcu",
|
"addLast": "dodaj na końcu",
|
||||||
"mute": "wycisz",
|
"mute": "wycisz",
|
||||||
"skip_forward": "przeskocz do przodu"
|
"skip_forward": "przeskocz do przodu",
|
||||||
|
"viewQueue": "zobacz kolejkę",
|
||||||
|
"playSimilarSongs": "odtwarzaj podobne"
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
"crossfadeStyle_description": "wybierz styl przenikania, który ma być używany do odtwarzania dźwięku",
|
"crossfadeStyle_description": "wybierz styl przenikania, który ma być używany do odtwarzania dźwięku",
|
||||||
@@ -471,7 +503,7 @@
|
|||||||
"hotkey_rate1": "oceń na 1 gwiazdkę",
|
"hotkey_rate1": "oceń na 1 gwiazdkę",
|
||||||
"hotkey_skipForward": "przeskocz do przodu",
|
"hotkey_skipForward": "przeskocz do przodu",
|
||||||
"disableLibraryUpdateOnStartup": "wyłącz wyszukiwanie aktualizacji podczas uruchamiania aplikacji",
|
"disableLibraryUpdateOnStartup": "wyłącz wyszukiwanie aktualizacji podczas uruchamiania aplikacji",
|
||||||
"discordApplicationId_description": "id dla aplikacji {{discord}} obszernie obecne (domyślnie {{defaultId}})",
|
"discordApplicationId_description": "id dla aplikacji {{discord}} rich presence (domyślnie {{defaultId}})",
|
||||||
"gaplessAudio": "dźwięk bez przerw",
|
"gaplessAudio": "dźwięk bez przerw",
|
||||||
"hotkey_playbackPlay": "odtwarzaj",
|
"hotkey_playbackPlay": "odtwarzaj",
|
||||||
"hotkey_togglePreviousSongFavorite": "dodaj $t(common.previousSong) do ulubionych",
|
"hotkey_togglePreviousSongFavorite": "dodaj $t(common.previousSong) do ulubionych",
|
||||||
@@ -501,7 +533,7 @@
|
|||||||
"crossfadeDuration_description": "ustaw czas trwania efektu przenikania",
|
"crossfadeDuration_description": "ustaw czas trwania efektu przenikania",
|
||||||
"language": "język",
|
"language": "język",
|
||||||
"hotkey_toggleShuffle": "przełącz kolejność losową",
|
"hotkey_toggleShuffle": "przełącz kolejność losową",
|
||||||
"discordRichPresence_description": "włącz status odtwarzania w {{discord}} rich presence. Dzięki temu będą wyświetlane informacje takie jak: {{icon}}, {{playing}} i {{paused}}. ",
|
"discordRichPresence_description": "włącz status odtwarzania w {{discord}} (rich presence). Dzięki temu będą wyświetlane informacje takie jak: {{icon}}, {{playing}} i {{paused}}. ",
|
||||||
"audioDevice": "urządzenia dźwiękowe",
|
"audioDevice": "urządzenia dźwiękowe",
|
||||||
"hotkey_rate2": "oceń na 2 gwiazdki",
|
"hotkey_rate2": "oceń na 2 gwiazdki",
|
||||||
"exitToTray": "zamknij do zasobnika",
|
"exitToTray": "zamknij do zasobnika",
|
||||||
@@ -522,7 +554,7 @@
|
|||||||
"customFontPath": "niestandardowa ścieżka czcionki",
|
"customFontPath": "niestandardowa ścieżka czcionki",
|
||||||
"followLyric": "podążaj za tekstem",
|
"followLyric": "podążaj za tekstem",
|
||||||
"crossfadeDuration": "czas trwania przenikania",
|
"crossfadeDuration": "czas trwania przenikania",
|
||||||
"discordIdleStatus": "pokaż obszerne informacje w stanie bezczynności",
|
"discordIdleStatus": "pokaż status w stanie bezczynności",
|
||||||
"audioPlayer": "odtwarzacz dźwięku",
|
"audioPlayer": "odtwarzacz dźwięku",
|
||||||
"hotkey_zoomOut": "oddal",
|
"hotkey_zoomOut": "oddal",
|
||||||
"hotkey_unfavoriteCurrentSong": "usuń $t(common.currentSong) z ulubionych",
|
"hotkey_unfavoriteCurrentSong": "usuń $t(common.currentSong) z ulubionych",
|
||||||
@@ -537,7 +569,7 @@
|
|||||||
"customFontPath_description": "ustaw ścieżkę dla niestandardowych czcionek dla aplikacji",
|
"customFontPath_description": "ustaw ścieżkę dla niestandardowych czcionek dla aplikacji",
|
||||||
"gaplessAudio_optionWeak": "słabe (rekomendowane)",
|
"gaplessAudio_optionWeak": "słabe (rekomendowane)",
|
||||||
"hotkey_playbackStop": "zatrzymaj",
|
"hotkey_playbackStop": "zatrzymaj",
|
||||||
"discordRichPresence": "{{discord}} obszernie obecny",
|
"discordRichPresence": "Status {{discord}} (rich presence)",
|
||||||
"font_description": "ustaw czcionkę dla aplikacji",
|
"font_description": "ustaw czcionkę dla aplikacji",
|
||||||
"playButtonBehavior_optionPlay": "$t(player.play)",
|
"playButtonBehavior_optionPlay": "$t(player.play)",
|
||||||
"minimumScrobblePercentage": "minimalny czas trwania scrobble (procentowy)",
|
"minimumScrobblePercentage": "minimalny czas trwania scrobble (procentowy)",
|
||||||
@@ -630,7 +662,52 @@
|
|||||||
"genreBehavior": "domyślne zachowanie strony gatunek",
|
"genreBehavior": "domyślne zachowanie strony gatunek",
|
||||||
"externalLinks_description": "umożliwia wyświetlanie linków zewnętrznych (Last.fm, MusicBrainz) na stronach artystów/albumów",
|
"externalLinks_description": "umożliwia wyświetlanie linków zewnętrznych (Last.fm, MusicBrainz) na stronach artystów/albumów",
|
||||||
"homeConfiguration": "konfiguracja strony głównej",
|
"homeConfiguration": "konfiguracja strony głównej",
|
||||||
"homeConfiguration_description": "konfiguracja elementów wyświetlanych na stronie głównej i ich kolejności"
|
"homeConfiguration_description": "konfiguracja elementów wyświetlanych na stronie głównej i ich kolejności",
|
||||||
|
"albumBackground_description": "dodaje obraz tła dla stron albumu zawierających grafikę albumu",
|
||||||
|
"albumBackgroundBlur": "rozmiar rozmycia obrazu tła albumu",
|
||||||
|
"albumBackgroundBlur_description": "dostosowywuje ilość rozmycia nakladanego na obraz tła albumu",
|
||||||
|
"albumBackground": "obraz tła albumu",
|
||||||
|
"artistConfiguration_description": "skonfiguruj jakie elementy są pokazywane, i w jakiej kolejności, na stronie albumu wykonawcy",
|
||||||
|
"discordListening_description": "pokazuje status jako słucha zamiast w grze",
|
||||||
|
"transcodeNote": "przynosi efekt po 1 (web) - 2 (mpv) piosenkach",
|
||||||
|
"transcode_description": "włącza transkodowanie na inne formaty",
|
||||||
|
"transcodeBitrate": "bitrate do transkodowania",
|
||||||
|
"transcode": "włącz transkodowanie",
|
||||||
|
"translationApiProvider": "usługodawca do api tłumaczeń",
|
||||||
|
"translationApiProvider_description": "wybór usługodawcy do api tłumaczeń",
|
||||||
|
"translationApiKey": "klucz api do tłumaczeń",
|
||||||
|
"transcodeFormat_description": "wybiera format do transkodowania. zostaw pusty aby serwer wybrał format",
|
||||||
|
"translationApiKey_description": "klucz api do tłumaczenia (Obsługuje tylko globalny endpoint)",
|
||||||
|
"homeFeature": "karuzela polecanych na stronie głównej",
|
||||||
|
"customCssEnable": "włącz niestandardowy css",
|
||||||
|
"customCssEnable_description": "pozwalaj na pisanie niestandardowego css.",
|
||||||
|
"customCssNotice": "Ostrzeżenie: chociaż istnieje pewne filtrowanie (uniemożliwia używanie url() i content:), używanie niestandardowego CSS-a może stwarzać ryzyko przez zmiany w interfejsie.",
|
||||||
|
"customCss_description": "zawartość niestandardowego css. Uwaga: content i zdalne url są niedozwolonymi właściwościami. Podgląd twojej zawartości jest pokazana poniżej. Dodatkowe pola których nie ustawiłeś, są obecne z powodu sanityzacji.",
|
||||||
|
"customCss": "niestandardowy css",
|
||||||
|
"doubleClickBehavior": "zakolejkuj wszystkie wyszukane utwory gdy podwójnie kliknięto",
|
||||||
|
"trayEnabled_description": "pokaż/ukryj ikonę/menu w zasobniku. jeżeli wyłączone, wyłącza też minimalizowanie.wyjście do zasobnika",
|
||||||
|
"webAudio_description": "używaj web audio. włącza to zaawansowane funkcje takie jak replaygain. wyłącz jeżeli nie działa poprawnie",
|
||||||
|
"artistConfiguration": "konfiguracja strony albumu wykonawcy",
|
||||||
|
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
|
||||||
|
"playerbarOpenDrawer_description": "pozwala przełączyć na odtwarzacz pełnoekranowy po kliknięciu paska odtwarzania",
|
||||||
|
"playerbarOpenDrawer": "przełącznik pełnego ekranu na pasku odtwarzania",
|
||||||
|
"imageAspectRatio": "używaj natywnych proporcji okładki",
|
||||||
|
"volumeWidth": "szerokość paska głośności",
|
||||||
|
"discordListening": "pokazuj status jako słucha",
|
||||||
|
"imageAspectRatio_description": "jeżeli włączone, okładka będzie pokazywana z użyciem jej natywnych proporcji. dla okładek które nie mają proporcji 1:1, pozostałe miejsce będzie puste",
|
||||||
|
"volumeWidth_description": "szerokość paska głośności",
|
||||||
|
"contextMenu_description": "pozwala ci na ukrycie elementów które są pokazywane w menu po kliknięciu prawym przyciskiem myszy na element. elementy które zostały odznaczone będą ukryte",
|
||||||
|
"contextMenu": "konfiguracja menu kontekstowego (pod prawym przyciskiem myszy)",
|
||||||
|
"transcodeBitrate_description": "wybiera bitrate do transkodowania. 0 pozwala wybrać to serwerowi",
|
||||||
|
"transcodeFormat": "format do transkodowania",
|
||||||
|
"translationTargetLanguage_description": "język do którego będzie tłumaczona treść",
|
||||||
|
"trayEnabled": "pokazuj w zasobniku",
|
||||||
|
"webAudio": "używaj web audio",
|
||||||
|
"homeFeature_description": "ustawienie powoduje to czy wyświetlana jest karuzela z polecanymi utworami na stronie głównej",
|
||||||
|
"doubleClickBehavior_description": "jeżeli włączone, wszystkie pasujące utwory w wyszukiwaniu zostaną zakolejkowane. w przeciwnym wypadku, tylko kliknięty będzie zakolejkowany",
|
||||||
|
"lastfmApiKey": "klucz API {{lastfm}}",
|
||||||
|
"lastfmApiKey_description": "klucz API dla {{lastfm}}. wymagany dla okładek",
|
||||||
|
"translationTargetLanguage": "docelowy język tłumaczenia"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"config": {
|
"config": {
|
||||||
@@ -646,7 +723,8 @@
|
|||||||
"autoFitColumns": "automatyczne dopasowanie kolumn",
|
"autoFitColumns": "automatyczne dopasowanie kolumn",
|
||||||
"size": "$t(common.size)",
|
"size": "$t(common.size)",
|
||||||
"itemSize": "rozmiar elementu (px)",
|
"itemSize": "rozmiar elementu (px)",
|
||||||
"itemGap": "odstęp między elementami (px)"
|
"itemGap": "odstęp między elementami (px)",
|
||||||
|
"followCurrentSong": "śledź aktualną piosenkę"
|
||||||
},
|
},
|
||||||
"label": {
|
"label": {
|
||||||
"releaseDate": "data premiery",
|
"releaseDate": "data premiery",
|
||||||
@@ -675,7 +753,8 @@
|
|||||||
"favorite": "$t(common.favorite)",
|
"favorite": "$t(common.favorite)",
|
||||||
"year": "$t(common.year)",
|
"year": "$t(common.year)",
|
||||||
"albumArtist": "$t(entity.albumArtist_one)",
|
"albumArtist": "$t(entity.albumArtist_one)",
|
||||||
"codec": "$t(common.codec)"
|
"codec": "$t(common.codec)",
|
||||||
|
"songCount": "$t(entity.track_other)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"column": {
|
"column": {
|
||||||
|
|||||||
+243
-25
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"common": {
|
"common": {
|
||||||
"backward": "voltar",
|
"backward": "para trás",
|
||||||
"areYouSure": "tem certeza?",
|
"areYouSure": "tem certeza?",
|
||||||
"add": "adicionar",
|
"add": "adicionar",
|
||||||
"ascending": "ascendente",
|
"ascending": "ascendente",
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
"title": "titulo",
|
"title": "titulo",
|
||||||
"create": "criar",
|
"create": "criar",
|
||||||
"confirm": "confirmar",
|
"confirm": "confirmar",
|
||||||
"home": "inicio",
|
"home": "início",
|
||||||
"comingSoon": "em breve…",
|
"comingSoon": "em breve…",
|
||||||
"channel_one": "canal",
|
"channel_one": "canal",
|
||||||
"channel_many": "canais",
|
"channel_many": "canais",
|
||||||
@@ -56,9 +56,9 @@
|
|||||||
"path": "caminho",
|
"path": "caminho",
|
||||||
"no": "não",
|
"no": "não",
|
||||||
"owner": "dono",
|
"owner": "dono",
|
||||||
"forward": "avançar",
|
"forward": "para frente",
|
||||||
"forceRestartRequired": "reinicie para aplicar as alterações… feche a notificação para reiniciar",
|
"forceRestartRequired": "reinicie para aplicar as alterações… feche a notificação para reiniciar",
|
||||||
"setting": "contexto",
|
"setting": "configuração",
|
||||||
"version": "versão",
|
"version": "versão",
|
||||||
"filter_one": "filtro",
|
"filter_one": "filtro",
|
||||||
"filter_many": "filtros",
|
"filter_many": "filtros",
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
"restartRequired": "é necessário reiniciar",
|
"restartRequired": "é necessário reiniciar",
|
||||||
"previousSong": "anterior $t(entity.track_one)",
|
"previousSong": "anterior $t(entity.track_one)",
|
||||||
"noResultsFromQuery": "a consulta não retornou resultados",
|
"noResultsFromQuery": "a consulta não retornou resultados",
|
||||||
"quit": "abandonar",
|
"quit": "sair",
|
||||||
"search": "procurar",
|
"search": "procurar",
|
||||||
"saveAs": "salvar como",
|
"saveAs": "salvar como",
|
||||||
"yes": "sim",
|
"yes": "sim",
|
||||||
@@ -87,7 +87,11 @@
|
|||||||
"preview": "pré-visualizar",
|
"preview": "pré-visualizar",
|
||||||
"share": "compartilhar",
|
"share": "compartilhar",
|
||||||
"close": "fechar",
|
"close": "fechar",
|
||||||
"translation": "tradução"
|
"translation": "tradução",
|
||||||
|
"albumGain": "ganho do álbum",
|
||||||
|
"trackPeak": "pico da faixa",
|
||||||
|
"albumPeak": "pico do álbum",
|
||||||
|
"trackGain": "ganho da faixa"
|
||||||
},
|
},
|
||||||
"action": {
|
"action": {
|
||||||
"goToPage": "vá para página",
|
"goToPage": "vá para página",
|
||||||
@@ -116,7 +120,8 @@
|
|||||||
"form": {
|
"form": {
|
||||||
"deletePlaylist": {
|
"deletePlaylist": {
|
||||||
"title": "deletar $t(entity.playlist_one)",
|
"title": "deletar $t(entity.playlist_one)",
|
||||||
"input_confirm": "escreva o nome da $t(entity.playlist_one) para confirmar"
|
"input_confirm": "escreva o nome da $t(entity.playlist_one) para confirmar",
|
||||||
|
"success": "$t(entity.playlist_one) deletada com sucesso"
|
||||||
},
|
},
|
||||||
"addServer": {
|
"addServer": {
|
||||||
"title": "adicionar servidor",
|
"title": "adicionar servidor",
|
||||||
@@ -128,19 +133,25 @@
|
|||||||
"input_url": "url",
|
"input_url": "url",
|
||||||
"success": "servidor adicionado com sucesso",
|
"success": "servidor adicionado com sucesso",
|
||||||
"input_name": "nome do servidor",
|
"input_name": "nome do servidor",
|
||||||
"input_username": "nome de usuário"
|
"input_username": "nome de usuário",
|
||||||
|
"ignoreCors": "ignorar CORS ($t(common.restartRequired))"
|
||||||
},
|
},
|
||||||
"createPlaylist": {
|
"createPlaylist": {
|
||||||
"title": "criar $t(entity.playlist_one)",
|
"title": "criar $t(entity.playlist_one)",
|
||||||
"input_public": "público",
|
"input_public": "público",
|
||||||
"input_description": "$t(common.description)",
|
"input_description": "$t(common.description)",
|
||||||
"success": "$t(entity.playlist_one) criada com sucesso"
|
"success": "$t(entity.playlist_one) criada com sucesso",
|
||||||
|
"input_owner": "$t(common.owner)",
|
||||||
|
"input_name": "$t(common.name)"
|
||||||
},
|
},
|
||||||
"updateServer": {
|
"updateServer": {
|
||||||
"title": "atualizar servidor"
|
"title": "atualizar servidor",
|
||||||
|
"success": "servidor atualizado com sucesso"
|
||||||
},
|
},
|
||||||
"editPlaylist": {
|
"editPlaylist": {
|
||||||
"title": "editar $t(entity.playlist_one)"
|
"title": "editar $t(entity.playlist_one)",
|
||||||
|
"publicJellyfinNote": "O Jellyfin por algum motivo não expõe se uma playlist é pública ou não. Se você deseja que ela permaneça pública, por favor selecione a seguinte entrada",
|
||||||
|
"success": "$t(entity.playlist_one) atualizada com sucesso"
|
||||||
},
|
},
|
||||||
"addToPlaylist": {
|
"addToPlaylist": {
|
||||||
"title": "adicionar à $t(entity.playlist_one)",
|
"title": "adicionar à $t(entity.playlist_one)",
|
||||||
@@ -149,14 +160,52 @@
|
|||||||
"success": "adicionado $t(entity.trackWithCount, {\"count\": {{message}} }) para $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })"
|
"success": "adicionado $t(entity.trackWithCount, {\"count\": {{message}} }) para $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })"
|
||||||
},
|
},
|
||||||
"lyricSearch": {
|
"lyricSearch": {
|
||||||
"title": "pesquisa de letras"
|
"title": "pesquisa de letras",
|
||||||
|
"input_artist": "$t(entity.artist_one)",
|
||||||
|
"input_name": "$t(common.name)"
|
||||||
|
},
|
||||||
|
"shareItem": {
|
||||||
|
"createFailed": "falha ao criar compartilhamento (o compartilhamento está ativado?)",
|
||||||
|
"setExpiration": "definir expiração",
|
||||||
|
"success": "link de compartilhamento copiado para a área de transferência (ou clique aqui para abrir)",
|
||||||
|
"allowDownloading": "permitir downloads",
|
||||||
|
"description": "descrição",
|
||||||
|
"expireInvalid": "a expiração deve ser uma data futura"
|
||||||
|
},
|
||||||
|
"queryEditor": {
|
||||||
|
"input_optionMatchAny": "corresponder qualquer um",
|
||||||
|
"input_optionMatchAll": "corresponder todos"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
"discordIdleStatus_description": "quando ativado, atualiza o status enquanto o player está ocioso",
|
"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)",
|
"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",
|
"playButtonBehavior_description": "define o comportamento padrão do botão play ao adicionar músicas à fila",
|
||||||
"discordApplicationId": "{{discord}} ID do aplicativo"
|
"discordApplicationId": "{{discord}} ID do aplicativo",
|
||||||
|
"audioPlayer": "player de áudio",
|
||||||
|
"applicationHotkeys": "teclas de atalho da aplicação",
|
||||||
|
"applicationHotkeys_description": "configure as teclas de atalho da aplicação. clique na caixa de seleção para definir como tecla de atalho global (somente desktop)",
|
||||||
|
"contextMenu": "configuração do menu de contexto (clique do botão direito do mouse)",
|
||||||
|
"clearQueryCache": "limpar cache do feishin",
|
||||||
|
"clearCache": "limpar cache do navegador",
|
||||||
|
"clearQueryCache_description": "uma 'limpeza leve' do feishin. isso irá renovar playlists, metadados de faixas, e resetar letras salvas. as configurações, as credenciais de servidor e o cache de imagens serão mantidos",
|
||||||
|
"audioPlayer_description": "selecione o player de áudio usado para reprodução",
|
||||||
|
"audioExclusiveMode": "modo de áudio exclusivo",
|
||||||
|
"buttonSize": "tamanho do botão da barra de reprodução",
|
||||||
|
"albumBackground_description": "adiciona uma imagem de fundo contendo a arte do álbum para a página de álbum",
|
||||||
|
"clearCache_description": "uma 'limpeza geral' do feishin. em adição a limpar o cache do feishin, limpa o cache do navegador (imagens salvas e outros recursos). as credenciais de servidor e as configurações serão mantidas",
|
||||||
|
"clearCacheSuccess": "cache limpo com sucesso",
|
||||||
|
"audioDevice": "dispositivo de áudio",
|
||||||
|
"audioDevice_description": "selecione o dispositivo de áudio usado para reprodução (somente player web)",
|
||||||
|
"audioExclusiveMode_description": "ativar modo de saída exclusiva. Neste modo, o sistema é geralmente bloqueado, e apenas mpv terá saída de áudio",
|
||||||
|
"accentColor": "cor de realce",
|
||||||
|
"accentColor_description": "define a cor de realce para a aplicação",
|
||||||
|
"artistConfiguration": "configuração da página de artista de álbum",
|
||||||
|
"artistConfiguration_description": "configure quais itens serão mostrados, e em qual ordem, na página de artista de álbum",
|
||||||
|
"buttonSize_description": "o tamanho dos botões da barra de reprodução",
|
||||||
|
"albumBackgroundBlur": "tamanho de desfoque da imagem de fundo do álbum",
|
||||||
|
"albumBackgroundBlur_description": "ajusta a quantidade de desfoque aplicada para a imagem de fundo do álbum",
|
||||||
|
"albumBackground": "imagem de fundo do álbum"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"config": {
|
"config": {
|
||||||
@@ -184,22 +233,142 @@
|
|||||||
"title": "$t(entity.albumArtist_other)"
|
"title": "$t(entity.albumArtist_other)"
|
||||||
},
|
},
|
||||||
"genreList": {
|
"genreList": {
|
||||||
"title": "$t(entity.genre_other)"
|
"title": "$t(entity.genre_other)",
|
||||||
|
"showTracks": "mostrar $t(entity.genre_one) $t(entity.track_other)",
|
||||||
|
"showAlbums": "mostrar $t(entity.genre_one) $t(entity.album_other)"
|
||||||
},
|
},
|
||||||
"trackList": {
|
"trackList": {
|
||||||
"title": "$t(entity.track_other)"
|
"title": "$t(entity.track_other)",
|
||||||
|
"artistTracks": "faixas de {{artist}}",
|
||||||
|
"genreTracks": "\"{{genre}}\" $t(entity.track_other)"
|
||||||
},
|
},
|
||||||
"globalSearch": {
|
"globalSearch": {
|
||||||
"title": "comandos"
|
"title": "comandos",
|
||||||
|
"commands": {
|
||||||
|
"serverCommands": "comandos do servidor",
|
||||||
|
"goToPage": "ir para a página",
|
||||||
|
"searchFor": "buscar por {{query}}"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"home": "$t(common.home)"
|
"home": "$t(common.home)",
|
||||||
|
"tracks": "$t(entity.track_other)",
|
||||||
|
"shared": "$t(entity.playlist_other) compartilhada",
|
||||||
|
"albums": "$t(entity.album_other)",
|
||||||
|
"genres": "$t(entity.genre_other)",
|
||||||
|
"folders": "$t(entity.folder_other)",
|
||||||
|
"albumArtists": "$t(entity.albumArtist_other)",
|
||||||
|
"artists": "$t(entity.artist_other)",
|
||||||
|
"nowPlaying": "tocando agora",
|
||||||
|
"playlists": "$t(entity.playlist_other)",
|
||||||
|
"search": "$t(common.search)",
|
||||||
|
"settings": "$t(common.setting_other)"
|
||||||
},
|
},
|
||||||
"playlistList": {
|
"playlistList": {
|
||||||
"title": "$t(entity.playlist_other)"
|
"title": "$t(entity.playlist_other)"
|
||||||
},
|
},
|
||||||
"albumList": {
|
"albumList": {
|
||||||
"title": "$t(entity.album_other)"
|
"title": "$t(entity.album_other)",
|
||||||
|
"artistAlbums": "álbuns de {{artist}}",
|
||||||
|
"genreAlbums": "\"{{genre}}\" $t(entity.album_other)"
|
||||||
|
},
|
||||||
|
"appMenu": {
|
||||||
|
"openBrowserDevtools": "abrir ferramentas do desenvolvedor",
|
||||||
|
"quit": "$t(common.quit)",
|
||||||
|
"selectServer": "selecionar servidor",
|
||||||
|
"collapseSidebar": "recolher barra lateral",
|
||||||
|
"expandSidebar": "expandir barra lateral",
|
||||||
|
"goBack": "voltar",
|
||||||
|
"goForward": "avançar",
|
||||||
|
"version": "versão {{version}}",
|
||||||
|
"manageServers": "gerenciar servidores",
|
||||||
|
"settings": "$t(common.setting_other)"
|
||||||
|
},
|
||||||
|
"contextMenu": {
|
||||||
|
"moveToTop": "$t(action.moveToTop)",
|
||||||
|
"moveToBottom": "$t(action.moveToBottom)",
|
||||||
|
"removeFromFavorites": "$t(action.removeFromFavorites)",
|
||||||
|
"numberSelected": "{{count}} selecionado",
|
||||||
|
"addFavorite": "$t(action.addToFavorites)",
|
||||||
|
"addLast": "$t(player.addLast)",
|
||||||
|
"addNext": "$t(player.addNext)",
|
||||||
|
"addToFavorites": "$t(action.addToFavorites)",
|
||||||
|
"playSimilarSongs": "$t(player.playSimilarSongs)",
|
||||||
|
"removeFromQueue": "$t(action.removeFromQueue)",
|
||||||
|
"play": "$t(player.play)",
|
||||||
|
"playShuffled": "$t(player.shuffle)",
|
||||||
|
"createPlaylist": "$t(action.createPlaylist)",
|
||||||
|
"download": "baixar",
|
||||||
|
"shareItem": "compartilhar item",
|
||||||
|
"showDetails": "obter informações",
|
||||||
|
"addToPlaylist": "$t(action.addToPlaylist)",
|
||||||
|
"deletePlaylist": "$t(action.deletePlaylist)",
|
||||||
|
"deselectAll": "$t(action.deselectAll)",
|
||||||
|
"moveToNext": "$t(action.moveToNext)",
|
||||||
|
"removeFromPlaylist": "$t(action.removeFromPlaylist)",
|
||||||
|
"setRating": "$t(action.setRating)"
|
||||||
|
},
|
||||||
|
"albumArtistDetail": {
|
||||||
|
"viewAllTracks": "ver todas as $t(entity.track_other)",
|
||||||
|
"appearsOn": "aparece em",
|
||||||
|
"recentReleases": "lançamentos recentes",
|
||||||
|
"viewDiscography": "ver discografia",
|
||||||
|
"relatedArtists": "$t(entity.artist_other) relacionados",
|
||||||
|
"viewAll": "ver tudo",
|
||||||
|
"topSongsFrom": "músicas mais tocadas de {{title}}",
|
||||||
|
"topSongs": "músicas mais tocadas",
|
||||||
|
"about": "Sobre {{artist}}"
|
||||||
|
},
|
||||||
|
"fullscreenPlayer": {
|
||||||
|
"config": {
|
||||||
|
"unsynchronized": "não sincronizado",
|
||||||
|
"dynamicIsImage": "habilitar imagem de fundo",
|
||||||
|
"dynamicImageBlur": "tamanho do desfoque da imagem",
|
||||||
|
"lyricAlignment": "alinhamento da letra",
|
||||||
|
"showLyricMatch": "exibir correspondência da letra",
|
||||||
|
"showLyricProvider": "exibir origem da letra",
|
||||||
|
"synchronized": "sincronizado",
|
||||||
|
"lyricOffset": "deslocamento da letra (ms)",
|
||||||
|
"followCurrentLyric": "acompanhar letra",
|
||||||
|
"useImageAspectRatio": "usar proporção da imagem",
|
||||||
|
"lyricGap": "espaçamento da letra",
|
||||||
|
"lyricSize": "tamanho da letra",
|
||||||
|
"dynamicBackground": "fundo dinâmico",
|
||||||
|
"opacity": "opacidade"
|
||||||
|
},
|
||||||
|
"related": "relacionado",
|
||||||
|
"visualizer": "visualizador",
|
||||||
|
"upNext": "a seguir",
|
||||||
|
"lyrics": "letra",
|
||||||
|
"noLyrics": "nenhuma letra encontrada"
|
||||||
|
},
|
||||||
|
"albumDetail": {
|
||||||
|
"moreFromArtist": "mais deste $t(entity.artist_one)",
|
||||||
|
"moreFromGeneric": "mais de {{item}}",
|
||||||
|
"released": "lançado"
|
||||||
|
},
|
||||||
|
"itemDetail": {
|
||||||
|
"copyPath": "copiar caminho para a área de transferência",
|
||||||
|
"copiedPath": "caminho copiado com sucesso",
|
||||||
|
"openFile": "mostrar faixa no gerenciador de arquivos"
|
||||||
|
},
|
||||||
|
"manageServers": {
|
||||||
|
"serverDetails": "detalhes do servidor",
|
||||||
|
"url": "URL",
|
||||||
|
"username": "nome de usuário",
|
||||||
|
"editServerDetailsTooltip": "editar detalhes do servidor",
|
||||||
|
"removeServer": "remover servidor",
|
||||||
|
"title": "gerenciar servidores"
|
||||||
|
},
|
||||||
|
"setting": {
|
||||||
|
"generalTab": "geral",
|
||||||
|
"hotkeysTab": "teclas de atalho",
|
||||||
|
"windowTab": "janela",
|
||||||
|
"advanced": "avançado",
|
||||||
|
"playbackTab": "reprodução"
|
||||||
|
},
|
||||||
|
"playlist": {
|
||||||
|
"reorder": "reordenar apenas disponível quando ordenado pelo id"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
@@ -229,13 +398,55 @@
|
|||||||
"recentlyAdded": "adicionado recentemente",
|
"recentlyAdded": "adicionado recentemente",
|
||||||
"releaseDate": "data de lançamento",
|
"releaseDate": "data de lançamento",
|
||||||
"recentlyPlayed": "tocado recentemente",
|
"recentlyPlayed": "tocado recentemente",
|
||||||
"criticRating": "Nota da crítica",
|
"criticRating": "avaliação da crítica",
|
||||||
"isFavorited": "é favoritado",
|
"isFavorited": "é favoritado",
|
||||||
"releaseYear": "ano de lançamento"
|
"releaseYear": "ano de lançamento",
|
||||||
|
"rating": "avaliação",
|
||||||
|
"artist": "$t(entity.artist_one)",
|
||||||
|
"bpm": "bpm",
|
||||||
|
"channels": "$t(common.channel_other)",
|
||||||
|
"comment": "comentário",
|
||||||
|
"owner": "$t(common.owner)",
|
||||||
|
"path": "caminho",
|
||||||
|
"id": "id",
|
||||||
|
"bitrate": "bitrate",
|
||||||
|
"isRated": "possui avaliação",
|
||||||
|
"note": "nota",
|
||||||
|
"albumCount": "número de $t(entity.album_other)",
|
||||||
|
"genre": "$t(entity.genre_one)"
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
"playbackFetchNoResults": "nenhuma música encontrada",
|
"playbackFetchNoResults": "nenhuma música encontrada",
|
||||||
"playbackFetchInProgress": "carregando músicas…"
|
"playbackFetchInProgress": "carregando músicas…",
|
||||||
|
"skip_forward": "avançar",
|
||||||
|
"mute": "mudo",
|
||||||
|
"playSimilarSongs": "tocar músicas similares",
|
||||||
|
"skip": "pular",
|
||||||
|
"stop": "parar",
|
||||||
|
"addNext": "adicionar a seguir",
|
||||||
|
"muted": "mudo",
|
||||||
|
"queue_clear": "limpar fila",
|
||||||
|
"toggleFullscreenPlayer": "alternar player de tela cheia",
|
||||||
|
"addLast": "adicionar no final",
|
||||||
|
"next": "próximo",
|
||||||
|
"play": "tocar",
|
||||||
|
"playRandom": "tocar aleatório",
|
||||||
|
"shuffle_off": "aleatório desativado",
|
||||||
|
"queue_moveToBottom": "mover selecionados para o topo",
|
||||||
|
"queue_moveToTop": "mover selecionados para o fim",
|
||||||
|
"skip_back": "retroceder",
|
||||||
|
"unfavorite": "remover favorito",
|
||||||
|
"playbackSpeed": "velocidade de reprodução",
|
||||||
|
"previous": "anterior",
|
||||||
|
"favorite": "favorito",
|
||||||
|
"playbackFetchCancel": "isso está demorando um pouco... feche a notificação para cancelar",
|
||||||
|
"queue_remove": "remover selecionados",
|
||||||
|
"repeat": "repetir",
|
||||||
|
"repeat_all": "repetir tudo",
|
||||||
|
"repeat_off": "repetição desativada",
|
||||||
|
"shuffle": "tocar aleatório",
|
||||||
|
"pause": "pausar",
|
||||||
|
"viewQueue": "ver fila"
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
"albumArtist_one": "artista do álbum",
|
"albumArtist_one": "artista do álbum",
|
||||||
@@ -277,12 +488,19 @@
|
|||||||
"genreWithCount_one": "{{count}} gênero",
|
"genreWithCount_one": "{{count}} gênero",
|
||||||
"genreWithCount_many": "{{count}} gêneros",
|
"genreWithCount_many": "{{count}} gêneros",
|
||||||
"genreWithCount_other": "{{count}} gêneros",
|
"genreWithCount_other": "{{count}} gêneros",
|
||||||
"trackWithCount_one": "faixa",
|
"trackWithCount_one": "{{count}} faixa",
|
||||||
"trackWithCount_many": "faixas",
|
"trackWithCount_many": "{{count}} faixas",
|
||||||
"trackWithCount_other": "faixas",
|
"trackWithCount_other": "{{count}} faixas",
|
||||||
"track_one": "faixa",
|
"track_one": "faixa",
|
||||||
"track_many": "faixas",
|
"track_many": "faixas",
|
||||||
"track_other": "faixas"
|
"track_other": "faixas",
|
||||||
|
"smartPlaylist": "$t(entity.playlist_one) inteligente",
|
||||||
|
"song_one": "música",
|
||||||
|
"song_many": "músicas",
|
||||||
|
"song_other": "músicas",
|
||||||
|
"play_one": "{{count}} reprodução",
|
||||||
|
"play_many": "{{count}} reproduções",
|
||||||
|
"play_other": "{{count}} reproduções"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"remotePortWarning": "reinicie o servidor para aplicar a nova porta",
|
"remotePortWarning": "reinicie o servidor para aplicar a nova porta",
|
||||||
|
|||||||
@@ -20,7 +20,8 @@
|
|||||||
"openIn": {
|
"openIn": {
|
||||||
"lastfm": "открыть на Last.fm",
|
"lastfm": "открыть на Last.fm",
|
||||||
"musicbrainz": "открыть на MusicBrainz"
|
"musicbrainz": "открыть на MusicBrainz"
|
||||||
}
|
},
|
||||||
|
"moveToNext": "следующий"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"backward": "назад",
|
"backward": "назад",
|
||||||
@@ -437,13 +438,15 @@
|
|||||||
},
|
},
|
||||||
"albumDetail": {
|
"albumDetail": {
|
||||||
"moreFromArtist": "больше от $t(entity.artist_one)",
|
"moreFromArtist": "больше от $t(entity.artist_one)",
|
||||||
"moreFromGeneric": "больше из {{item}}"
|
"moreFromGeneric": "больше из {{item}}",
|
||||||
|
"released": "выпущен"
|
||||||
},
|
},
|
||||||
"setting": {
|
"setting": {
|
||||||
"playbackTab": "воспроизведение",
|
"playbackTab": "воспроизведение",
|
||||||
"generalTab": "общее",
|
"generalTab": "общее",
|
||||||
"hotkeysTab": "горячие клавиши",
|
"hotkeysTab": "горячие клавиши",
|
||||||
"windowTab": "окно"
|
"windowTab": "окно",
|
||||||
|
"advanced": "расширенные"
|
||||||
},
|
},
|
||||||
"albumArtistList": {
|
"albumArtistList": {
|
||||||
"title": "$t(entity.albumArtist_other)"
|
"title": "$t(entity.albumArtist_other)"
|
||||||
@@ -540,7 +543,8 @@
|
|||||||
"title": "поиск слов песни"
|
"title": "поиск слов песни"
|
||||||
},
|
},
|
||||||
"editPlaylist": {
|
"editPlaylist": {
|
||||||
"title": "редактировать $t(entity.playlist_one)"
|
"title": "редактировать $t(entity.playlist_one)",
|
||||||
|
"success": "$t(entity.playlist_one) обновлён успешно"
|
||||||
},
|
},
|
||||||
"shareItem": {
|
"shareItem": {
|
||||||
"success": "ссылка скопирована в буфер обмена (нажмите здесь, чтобы открыть)",
|
"success": "ссылка скопирована в буфер обмена (нажмите здесь, чтобы открыть)",
|
||||||
|
|||||||
@@ -494,6 +494,9 @@ const createWindow = async (first = true) => {
|
|||||||
|
|
||||||
app.commandLine.appendSwitch('disable-features', 'HardwareMediaKeyHandling,MediaSessionService');
|
app.commandLine.appendSwitch('disable-features', 'HardwareMediaKeyHandling,MediaSessionService');
|
||||||
|
|
||||||
|
// https://github.com/electron/electron/issues/46538#issuecomment-2808806722
|
||||||
|
app.commandLine.appendSwitch('gtk-version', '3');
|
||||||
|
|
||||||
// Must duplicate with the one in renderer process settings.store.ts
|
// Must duplicate with the one in renderer process settings.store.ts
|
||||||
enum BindingActions {
|
enum BindingActions {
|
||||||
GLOBAL_SEARCH = 'globalSearch',
|
GLOBAL_SEARCH = 'globalSearch',
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ export const hotkeyToElectronAccelerator = (hotkey: string) => {
|
|||||||
let accelerator = hotkey;
|
let accelerator = hotkey;
|
||||||
|
|
||||||
const replacements = {
|
const replacements = {
|
||||||
|
arrowdown: 'Down',
|
||||||
|
arrowleft: 'Left',
|
||||||
|
arrowright: 'Right',
|
||||||
|
arrowup: 'Up',
|
||||||
mod: 'CmdOrCtrl',
|
mod: 'CmdOrCtrl',
|
||||||
numpad: 'num',
|
numpad: 'num',
|
||||||
numpadadd: 'numadd',
|
numpadadd: 'numadd',
|
||||||
|
|||||||
@@ -99,6 +99,12 @@ export const controller: GeneralController = {
|
|||||||
getAlbumListCount(args) {
|
getAlbumListCount(args) {
|
||||||
return apiController('getAlbumListCount', args.apiClientProps.server?.type)?.(args);
|
return apiController('getAlbumListCount', args.apiClientProps.server?.type)?.(args);
|
||||||
},
|
},
|
||||||
|
getArtistList(args) {
|
||||||
|
return apiController('getArtistList', args.apiClientProps.server?.type)?.(args);
|
||||||
|
},
|
||||||
|
getArtistListCount(args) {
|
||||||
|
return apiController('getArtistListCount', args.apiClientProps.server?.type)?.(args);
|
||||||
|
},
|
||||||
getDownloadUrl(args) {
|
getDownloadUrl(args) {
|
||||||
return apiController('getDownloadUrl', args.apiClientProps.server?.type)?.(args);
|
return apiController('getDownloadUrl', args.apiClientProps.server?.type)?.(args);
|
||||||
},
|
},
|
||||||
@@ -126,6 +132,9 @@ export const controller: GeneralController = {
|
|||||||
getRandomSongList(args) {
|
getRandomSongList(args) {
|
||||||
return apiController('getRandomSongList', args.apiClientProps.server?.type)?.(args);
|
return apiController('getRandomSongList', args.apiClientProps.server?.type)?.(args);
|
||||||
},
|
},
|
||||||
|
getRoles(args) {
|
||||||
|
return apiController('getRoles', args.apiClientProps.server?.type)?.(args);
|
||||||
|
},
|
||||||
getServerInfo(args) {
|
getServerInfo(args) {
|
||||||
return apiController('getServerInfo', args.apiClientProps.server?.type)?.(args);
|
return apiController('getServerInfo', args.apiClientProps.server?.type)?.(args);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -152,7 +152,6 @@ export const JellyfinController: ControllerEndpoint = {
|
|||||||
const { query, apiClientProps } = args;
|
const { query, apiClientProps } = args;
|
||||||
|
|
||||||
const res = await jfApiClient(apiClientProps).deletePlaylist({
|
const res = await jfApiClient(apiClientProps).deletePlaylist({
|
||||||
body: null,
|
|
||||||
params: {
|
params: {
|
||||||
id: query.id,
|
id: query.id,
|
||||||
},
|
},
|
||||||
@@ -331,6 +330,41 @@ export const JellyfinController: ControllerEndpoint = {
|
|||||||
apiClientProps,
|
apiClientProps,
|
||||||
query: { ...query, limit: 1, startIndex: 0 },
|
query: { ...query, limit: 1, startIndex: 0 },
|
||||||
}).then((result) => result!.totalRecordCount!),
|
}).then((result) => result!.totalRecordCount!),
|
||||||
|
getArtistList: async (args) => {
|
||||||
|
const { query, apiClientProps } = args;
|
||||||
|
|
||||||
|
const res = await jfApiClient(apiClientProps).getArtistList({
|
||||||
|
query: {
|
||||||
|
Fields: 'Genres, DateCreated, ExternalUrls, Overview',
|
||||||
|
ImageTypeLimit: 1,
|
||||||
|
Limit: query.limit,
|
||||||
|
ParentId: query.musicFolderId,
|
||||||
|
Recursive: true,
|
||||||
|
SearchTerm: query.searchTerm,
|
||||||
|
SortBy: albumArtistListSortMap.jellyfin[query.sortBy] || 'SortName,Name',
|
||||||
|
SortOrder: sortOrderMap.jellyfin[query.sortOrder],
|
||||||
|
StartIndex: query.startIndex,
|
||||||
|
UserId: apiClientProps.server?.userId || undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to get album artist list');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: res.body.Items.map((item) =>
|
||||||
|
jfNormalize.albumArtist(item, apiClientProps.server),
|
||||||
|
),
|
||||||
|
startIndex: query.startIndex,
|
||||||
|
totalRecordCount: res.body.TotalRecordCount,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getArtistListCount: async ({ apiClientProps, query }) =>
|
||||||
|
JellyfinController.getArtistList({
|
||||||
|
apiClientProps,
|
||||||
|
query: { ...query, limit: 1, startIndex: 0 },
|
||||||
|
}).then((result) => result!.totalRecordCount!),
|
||||||
getDownloadUrl: (args) => {
|
getDownloadUrl: (args) => {
|
||||||
const { apiClientProps, query } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
@@ -559,6 +593,7 @@ export const JellyfinController: ControllerEndpoint = {
|
|||||||
totalRecordCount: res.body.Items.length || 0,
|
totalRecordCount: res.body.Items.length || 0,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
getRoles: async () => [],
|
||||||
getServerInfo: async (args) => {
|
getServerInfo: async (args) => {
|
||||||
const { apiClientProps } = args;
|
const { apiClientProps } = args;
|
||||||
|
|
||||||
@@ -660,56 +695,98 @@ export const JellyfinController: ControllerEndpoint = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const yearsFilter = yearsGroup.length ? formatCommaDelimitedString(yearsGroup) : undefined;
|
const yearsFilter = yearsGroup.length ? formatCommaDelimitedString(yearsGroup) : undefined;
|
||||||
const albumIdsFilter = query.albumIds
|
|
||||||
? formatCommaDelimitedString(query.albumIds)
|
|
||||||
: undefined;
|
|
||||||
const artistIdsFilter = query.artistIds
|
const artistIdsFilter = query.artistIds
|
||||||
? formatCommaDelimitedString(query.artistIds)
|
? formatCommaDelimitedString(query.artistIds)
|
||||||
: undefined;
|
: query.albumArtistIds
|
||||||
|
? formatCommaDelimitedString(query.albumArtistIds)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const res = await jfApiClient(apiClientProps).getSongList({
|
let items: z.infer<typeof jfType._response.song>[] = [];
|
||||||
params: {
|
let totalRecordCount = 0;
|
||||||
userId: apiClientProps.server?.userId,
|
const batchSize = 50;
|
||||||
},
|
|
||||||
query: {
|
|
||||||
AlbumIds: albumIdsFilter,
|
|
||||||
ArtistIds: artistIdsFilter,
|
|
||||||
Fields: 'Genres, DateCreated, MediaSources, ParentId',
|
|
||||||
GenreIds: query.genreIds?.join(','),
|
|
||||||
IncludeItemTypes: 'Audio',
|
|
||||||
IsFavorite: query.favorite,
|
|
||||||
Limit: query.limit,
|
|
||||||
ParentId: query.musicFolderId,
|
|
||||||
Recursive: true,
|
|
||||||
SearchTerm: query.searchTerm,
|
|
||||||
SortBy: songListSortMap.jellyfin[query.sortBy] || 'Album,SortName',
|
|
||||||
SortOrder: sortOrderMap.jellyfin[query.sortOrder],
|
|
||||||
StartIndex: query.startIndex,
|
|
||||||
...query._custom?.jellyfin,
|
|
||||||
Years: yearsFilter,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.status !== 200) {
|
// Handle albumIds fetches in batches to prevent HTTP 414 errors
|
||||||
throw new Error('Failed to get song list');
|
if (query.albumIds && query.albumIds.length > batchSize) {
|
||||||
}
|
const albumIdBatches = chunk(query.albumIds, batchSize);
|
||||||
|
|
||||||
let items: z.infer<typeof jfType._response.song>[];
|
for (const batch of albumIdBatches) {
|
||||||
|
const albumIdsFilter = formatCommaDelimitedString(batch);
|
||||||
|
|
||||||
// Jellyfin Bodge because of code from https://github.com/jellyfin/jellyfin/blob/c566ccb63bf61f9c36743ddb2108a57c65a2519b/Emby.Server.Implementations/Data/SqliteItemRepository.cs#L3622
|
const res = await jfApiClient(apiClientProps).getSongList({
|
||||||
// If the Album ID filter is passed, Jellyfin will search for
|
params: {
|
||||||
// 1. the matching album id
|
userId: apiClientProps.server?.userId,
|
||||||
// 2. An album with the name of the album.
|
},
|
||||||
// It is this second condition causing issues,
|
query: {
|
||||||
if (query.albumIds) {
|
AlbumIds: albumIdsFilter,
|
||||||
const albumIdSet = new Set(query.albumIds);
|
ArtistIds: artistIdsFilter,
|
||||||
items = res.body.Items.filter((item) => albumIdSet.has(item.AlbumId!));
|
Fields: 'Genres, DateCreated, MediaSources, ParentId',
|
||||||
|
GenreIds: query.genreIds?.join(','),
|
||||||
|
IncludeItemTypes: 'Audio',
|
||||||
|
IsFavorite: query.favorite,
|
||||||
|
Limit: query.limit,
|
||||||
|
ParentId: query.musicFolderId,
|
||||||
|
Recursive: true,
|
||||||
|
SearchTerm: query.searchTerm,
|
||||||
|
SortBy: songListSortMap.jellyfin[query.sortBy] || 'Album,SortName',
|
||||||
|
SortOrder: sortOrderMap.jellyfin[query.sortOrder],
|
||||||
|
StartIndex: query.startIndex,
|
||||||
|
...query._custom?.jellyfin,
|
||||||
|
Years: yearsFilter,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (items.length < res.body.Items.length) {
|
if (res.status !== 200) {
|
||||||
res.body.TotalRecordCount -= res.body.Items.length - items.length;
|
throw new Error('Failed to get song list');
|
||||||
|
}
|
||||||
|
|
||||||
|
items = [...items, ...res.body.Items];
|
||||||
|
totalRecordCount += res.body.Items.length;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
items = res.body.Items;
|
const albumIdsFilter = query.albumIds
|
||||||
|
? formatCommaDelimitedString(query.albumIds)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const res = await jfApiClient(apiClientProps).getSongList({
|
||||||
|
params: {
|
||||||
|
userId: apiClientProps.server?.userId,
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
AlbumIds: albumIdsFilter,
|
||||||
|
ArtistIds: artistIdsFilter,
|
||||||
|
Fields: 'Genres, DateCreated, MediaSources, ParentId',
|
||||||
|
GenreIds: query.genreIds?.join(','),
|
||||||
|
IncludeItemTypes: 'Audio',
|
||||||
|
IsFavorite: query.favorite,
|
||||||
|
Limit: query.limit,
|
||||||
|
ParentId: query.musicFolderId,
|
||||||
|
Recursive: true,
|
||||||
|
SearchTerm: query.searchTerm,
|
||||||
|
SortBy: songListSortMap.jellyfin[query.sortBy] || 'Album,SortName',
|
||||||
|
SortOrder: sortOrderMap.jellyfin[query.sortOrder],
|
||||||
|
StartIndex: query.startIndex,
|
||||||
|
...query._custom?.jellyfin,
|
||||||
|
Years: yearsFilter,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to get song list');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jellyfin Bodge because of code from https://github.com/jellyfin/jellyfin/blob/c566ccb63bf61f9c36743ddb2108a57c65a2519b/Emby.Server.Implementations/Data/SqliteItemRepository.cs#L3622
|
||||||
|
// If the Album ID filter is passed, Jellyfin will search for
|
||||||
|
// 1. the matching album id
|
||||||
|
// 2. An album with the name of the album.
|
||||||
|
// It is this second condition causing issues,
|
||||||
|
if (query.albumIds) {
|
||||||
|
const albumIdSet = new Set(query.albumIds);
|
||||||
|
items = res.body.Items.filter((item) => albumIdSet.has(item.AlbumId!));
|
||||||
|
totalRecordCount = items.length;
|
||||||
|
} else {
|
||||||
|
items = res.body.Items;
|
||||||
|
totalRecordCount = res.body.TotalRecordCount;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -717,7 +794,7 @@ export const JellyfinController: ControllerEndpoint = {
|
|||||||
jfNormalize.song(item, apiClientProps.server, '', query.imageSize),
|
jfNormalize.song(item, apiClientProps.server, '', query.imageSize),
|
||||||
),
|
),
|
||||||
startIndex: query.startIndex,
|
startIndex: query.startIndex,
|
||||||
totalRecordCount: res.body.TotalRecordCount,
|
totalRecordCount,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getSongListCount: async ({ apiClientProps, query }) =>
|
getSongListCount: async ({ apiClientProps, query }) =>
|
||||||
@@ -775,7 +852,6 @@ export const JellyfinController: ControllerEndpoint = {
|
|||||||
const { apiClientProps, query } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
const res = await jfApiClient(apiClientProps).movePlaylistItem({
|
const res = await jfApiClient(apiClientProps).movePlaylistItem({
|
||||||
body: null,
|
|
||||||
params: {
|
params: {
|
||||||
itemId: query.trackId,
|
itemId: query.trackId,
|
||||||
newIdx: query.endingIndex.toString(),
|
newIdx: query.endingIndex.toString(),
|
||||||
@@ -794,7 +870,6 @@ export const JellyfinController: ControllerEndpoint = {
|
|||||||
|
|
||||||
for (const chunk of chunks) {
|
for (const chunk of chunks) {
|
||||||
const res = await jfApiClient(apiClientProps).removeFromPlaylist({
|
const res = await jfApiClient(apiClientProps).removeFromPlaylist({
|
||||||
body: null,
|
|
||||||
params: {
|
params: {
|
||||||
id: query.id,
|
id: query.id,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -236,7 +236,6 @@ const normalizeAlbum = (
|
|||||||
})),
|
})),
|
||||||
id: item.Id,
|
id: item.Id,
|
||||||
imagePlaceholderUrl: null,
|
imagePlaceholderUrl: null,
|
||||||
participants: null,
|
|
||||||
imageUrl: getAlbumCoverArtUrl({
|
imageUrl: getAlbumCoverArtUrl({
|
||||||
baseUrl: server?.url || '',
|
baseUrl: server?.url || '',
|
||||||
item,
|
item,
|
||||||
@@ -248,6 +247,7 @@ const normalizeAlbum = (
|
|||||||
mbzId: item.ProviderIds?.MusicBrainzAlbum || null,
|
mbzId: item.ProviderIds?.MusicBrainzAlbum || null,
|
||||||
name: item.Name,
|
name: item.Name,
|
||||||
originalDate: null,
|
originalDate: null,
|
||||||
|
participants: null,
|
||||||
playCount: item.UserData?.PlayCount || 0,
|
playCount: item.UserData?.PlayCount || 0,
|
||||||
releaseDate: item.PremiereDate?.split('T')[0] || null,
|
releaseDate: item.PremiereDate?.split('T')[0] || null,
|
||||||
releaseYear: item.ProductionYear || null,
|
releaseYear: item.ProductionYear || null,
|
||||||
@@ -284,7 +284,7 @@ const normalizeAlbumArtist = (
|
|||||||
) || [];
|
) || [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
albumCount: null,
|
albumCount: item.AlbumCount ?? null,
|
||||||
backgroundImageUrl: null,
|
backgroundImageUrl: null,
|
||||||
biography: item.Overview || null,
|
biography: item.Overview || null,
|
||||||
duration: item.RunTimeTicks / 10000,
|
duration: item.RunTimeTicks / 10000,
|
||||||
@@ -308,7 +308,7 @@ const normalizeAlbumArtist = (
|
|||||||
serverId: server?.id || '',
|
serverId: server?.id || '',
|
||||||
serverType: ServerType.JELLYFIN,
|
serverType: ServerType.JELLYFIN,
|
||||||
similarArtists,
|
similarArtists,
|
||||||
songCount: null,
|
songCount: item.SongCount ?? null,
|
||||||
userFavorite: item.UserData?.IsFavorite || false,
|
userFavorite: item.UserData?.IsFavorite || false,
|
||||||
userRating: null,
|
userRating: null,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -431,6 +431,7 @@ const providerIds = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const albumArtist = z.object({
|
const albumArtist = z.object({
|
||||||
|
AlbumCount: z.number().optional(),
|
||||||
BackdropImageTags: z.array(z.string()),
|
BackdropImageTags: z.array(z.string()),
|
||||||
ChannelId: z.null(),
|
ChannelId: z.null(),
|
||||||
DateCreated: z.string(),
|
DateCreated: z.string(),
|
||||||
@@ -446,6 +447,7 @@ const albumArtist = z.object({
|
|||||||
ProviderIds: providerIds.optional(),
|
ProviderIds: providerIds.optional(),
|
||||||
RunTimeTicks: z.number(),
|
RunTimeTicks: z.number(),
|
||||||
ServerId: z.string(),
|
ServerId: z.string(),
|
||||||
|
SongCount: z.number().optional(),
|
||||||
Type: z.string(),
|
Type: z.string(),
|
||||||
UserData: userData.optional(),
|
UserData: userData.optional(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -30,6 +30,22 @@ const VERSION_INFO: VersionInfo = [
|
|||||||
['0.48.0', { [ServerFeature.PLAYLISTS_SMART]: [1] }],
|
['0.48.0', { [ServerFeature.PLAYLISTS_SMART]: [1] }],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const NAVIDROME_ROLES: Array<string | { label: string; value: string }> = [
|
||||||
|
{ label: 'all artists', value: '' },
|
||||||
|
'arranger',
|
||||||
|
'artist',
|
||||||
|
'composer',
|
||||||
|
'conductor',
|
||||||
|
'director',
|
||||||
|
'djmixer',
|
||||||
|
'engineer',
|
||||||
|
'lyricist',
|
||||||
|
'mixer',
|
||||||
|
'performer',
|
||||||
|
'producer',
|
||||||
|
'remixer',
|
||||||
|
];
|
||||||
|
|
||||||
const excludeMissing = (server: ServerListItem | null) => {
|
const excludeMissing = (server: ServerListItem | null) => {
|
||||||
if (hasFeature(server, ServerFeature.BFR)) {
|
if (hasFeature(server, ServerFeature.BFR)) {
|
||||||
return { missing: false };
|
return { missing: false };
|
||||||
@@ -105,7 +121,6 @@ export const NavidromeController: ControllerEndpoint = {
|
|||||||
const { query, apiClientProps } = args;
|
const { query, apiClientProps } = args;
|
||||||
|
|
||||||
const res = await ndApiClient(apiClientProps).deletePlaylist({
|
const res = await ndApiClient(apiClientProps).deletePlaylist({
|
||||||
body: null,
|
|
||||||
params: {
|
params: {
|
||||||
id: query.id,
|
id: query.id,
|
||||||
},
|
},
|
||||||
@@ -261,6 +276,47 @@ export const NavidromeController: ControllerEndpoint = {
|
|||||||
apiClientProps,
|
apiClientProps,
|
||||||
query: { ...query, limit: 1, startIndex: 0 },
|
query: { ...query, limit: 1, startIndex: 0 },
|
||||||
}).then((result) => result!.totalRecordCount!),
|
}).then((result) => result!.totalRecordCount!),
|
||||||
|
getArtistList: async (args) => {
|
||||||
|
const { query, apiClientProps } = args;
|
||||||
|
|
||||||
|
const res = await ndApiClient(apiClientProps).getAlbumArtistList({
|
||||||
|
query: {
|
||||||
|
_end: query.startIndex + (query.limit || 0),
|
||||||
|
_order: sortOrderMap.navidrome[query.sortOrder],
|
||||||
|
_sort: albumArtistListSortMap.navidrome[query.sortBy],
|
||||||
|
_start: query.startIndex,
|
||||||
|
name: query.searchTerm,
|
||||||
|
...query._custom?.navidrome,
|
||||||
|
role: query.role || undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to get artist list');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: res.body.data.map((albumArtist) =>
|
||||||
|
// Navidrome native API will return only external URL small/medium/large
|
||||||
|
// image URL. Set large image to undefined to force `albumArtist` to use
|
||||||
|
// /rest/getCoverArt.view?id=ar-...
|
||||||
|
ndNormalize.albumArtist(
|
||||||
|
{
|
||||||
|
...albumArtist,
|
||||||
|
largeImageUrl: undefined,
|
||||||
|
},
|
||||||
|
apiClientProps.server,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
startIndex: query.startIndex,
|
||||||
|
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getArtistListCount: async ({ apiClientProps, query }) =>
|
||||||
|
NavidromeController.getArtistList({
|
||||||
|
apiClientProps,
|
||||||
|
query: { ...query, limit: 1, startIndex: 0 },
|
||||||
|
}).then((result) => result!.totalRecordCount!),
|
||||||
getDownloadUrl: SubsonicController.getDownloadUrl,
|
getDownloadUrl: SubsonicController.getDownloadUrl,
|
||||||
getGenreList: async (args) => {
|
getGenreList: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { query, apiClientProps } = args;
|
||||||
@@ -369,6 +425,8 @@ export const NavidromeController: ControllerEndpoint = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
getRandomSongList: SubsonicController.getRandomSongList,
|
getRandomSongList: SubsonicController.getRandomSongList,
|
||||||
|
getRoles: async ({ apiClientProps }) =>
|
||||||
|
hasFeature(apiClientProps.server, ServerFeature.BFR) ? NAVIDROME_ROLES : [],
|
||||||
getServerInfo: async (args) => {
|
getServerInfo: async (args) => {
|
||||||
const { apiClientProps } = args;
|
const { apiClientProps } = args;
|
||||||
|
|
||||||
@@ -490,8 +548,9 @@ export const NavidromeController: ControllerEndpoint = {
|
|||||||
_order: sortOrderMap.navidrome[query.sortOrder],
|
_order: sortOrderMap.navidrome[query.sortOrder],
|
||||||
_sort: songListSortMap.navidrome[query.sortBy],
|
_sort: songListSortMap.navidrome[query.sortBy],
|
||||||
_start: query.startIndex,
|
_start: query.startIndex,
|
||||||
album_artist_id: query.artistIds,
|
album_artist_id: query.albumArtistIds,
|
||||||
album_id: query.albumIds,
|
album_id: query.albumIds,
|
||||||
|
artist_id: query.artistIds,
|
||||||
genre_id: query.genreIds,
|
genre_id: query.genreIds,
|
||||||
starred: query.favorite,
|
starred: query.favorite,
|
||||||
title: query.searchTerm,
|
title: query.searchTerm,
|
||||||
@@ -564,7 +623,6 @@ export const NavidromeController: ControllerEndpoint = {
|
|||||||
const { query, apiClientProps } = args;
|
const { query, apiClientProps } = args;
|
||||||
|
|
||||||
const res = await ndApiClient(apiClientProps).removeFromPlaylist({
|
const res = await ndApiClient(apiClientProps).removeFromPlaylist({
|
||||||
body: null,
|
|
||||||
params: {
|
params: {
|
||||||
id: query.id,
|
id: query.id,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -278,8 +278,25 @@ const normalizeAlbumArtist = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let albumCount: number;
|
||||||
|
let songCount: number;
|
||||||
|
|
||||||
|
if (item.stats) {
|
||||||
|
albumCount = Math.max(
|
||||||
|
item.stats.albumartist?.albumCount ?? 0,
|
||||||
|
item.stats.artist?.albumCount ?? 0,
|
||||||
|
);
|
||||||
|
songCount = Math.max(
|
||||||
|
item.stats.albumartist?.songCount ?? 0,
|
||||||
|
item.stats.artist?.songCount ?? 0,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
albumCount = item.albumCount;
|
||||||
|
songCount = item.songCount;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
albumCount: item.stats?.albumartist.albumCount || item.albumCount,
|
albumCount,
|
||||||
backgroundImageUrl: null,
|
backgroundImageUrl: null,
|
||||||
biography: item.biography || null,
|
biography: item.biography || null,
|
||||||
duration: null,
|
duration: null,
|
||||||
@@ -304,7 +321,7 @@ const normalizeAlbumArtist = (
|
|||||||
imageUrl: artist?.artistImageUrl || null,
|
imageUrl: artist?.artistImageUrl || null,
|
||||||
name: artist.name,
|
name: artist.name,
|
||||||
})) || null,
|
})) || null,
|
||||||
songCount: item.stats?.albumartist.songCount || item.songCount,
|
songCount,
|
||||||
userFavorite: item.starred,
|
userFavorite: item.starred,
|
||||||
userRating: item.rating,
|
userRating: item.rating,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -231,6 +231,9 @@ export const queryKeys: Record<
|
|||||||
return [serverId, 'playlists', 'songList'] as const;
|
return [serverId, 'playlists', 'songList'] as const;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
roles: {
|
||||||
|
list: (serverId: string) => [serverId, 'roles'] as const,
|
||||||
|
},
|
||||||
search: {
|
search: {
|
||||||
list: (serverId: string, query?: SearchQuery) => {
|
list: (serverId: string, query?: SearchQuery) => {
|
||||||
if (query) return [serverId, 'search', 'list', query] as const;
|
if (query) return [serverId, 'search', 'list', query] as const;
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ export const SubsonicController: ControllerEndpoint = {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...ssNormalize.albumArtist(artist, apiClientProps.server, 300),
|
...ssNormalize.albumArtist(artist, apiClientProps.server, 300),
|
||||||
albums: artist.album.map((album) => ssNormalize.album(album, apiClientProps.server)),
|
albums: artist.album?.map((album) => ssNormalize.album(album, apiClientProps.server)),
|
||||||
similarArtists:
|
similarArtists:
|
||||||
artistInfo?.similarArtist?.map((artist) =>
|
artistInfo?.similarArtist?.map((artist) =>
|
||||||
ssNormalize.albumArtist(artist, apiClientProps.server, 300),
|
ssNormalize.albumArtist(artist, apiClientProps.server, 300),
|
||||||
@@ -305,7 +305,7 @@ export const SubsonicController: ControllerEndpoint = {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return artist.body.artist.album;
|
return artist.body.artist.album ?? [];
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -515,6 +515,51 @@ export const SubsonicController: ControllerEndpoint = {
|
|||||||
|
|
||||||
return totalRecordCount;
|
return totalRecordCount;
|
||||||
},
|
},
|
||||||
|
getArtistList: async (args) => {
|
||||||
|
const { query, apiClientProps } = args;
|
||||||
|
|
||||||
|
const res = await ssApiClient(apiClientProps).getArtists({
|
||||||
|
query: {
|
||||||
|
musicFolderId: query.musicFolderId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to get artist list');
|
||||||
|
}
|
||||||
|
|
||||||
|
let artists = (res.body.artists?.index || []).flatMap((index) => index.artist);
|
||||||
|
console.log(artists.length);
|
||||||
|
if (query.role) {
|
||||||
|
artists = artists.filter(
|
||||||
|
(artist) => !artist.roles || artist.roles.includes(query.role!),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let results = artists.map((artist) =>
|
||||||
|
ssNormalize.albumArtist(artist, apiClientProps.server, 300),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (query.searchTerm) {
|
||||||
|
const searchResults = filter(results, (artist) => {
|
||||||
|
return artist.name.toLowerCase().includes(query.searchTerm!.toLowerCase());
|
||||||
|
});
|
||||||
|
|
||||||
|
results = searchResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.sortBy) {
|
||||||
|
results = sortAlbumArtistList(results, query.sortBy, query.sortOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: results,
|
||||||
|
startIndex: query.startIndex,
|
||||||
|
totalRecordCount: results?.length || 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getArtistListCount: async (args) =>
|
||||||
|
SubsonicController.getArtistList(args).then((res) => res!.totalRecordCount!),
|
||||||
getDownloadUrl: (args) => {
|
getDownloadUrl: (args) => {
|
||||||
const { apiClientProps, query } = args;
|
const { apiClientProps, query } = args;
|
||||||
|
|
||||||
@@ -711,6 +756,32 @@ export const SubsonicController: ControllerEndpoint = {
|
|||||||
totalRecordCount: res.body.randomSongs?.song?.length || 0,
|
totalRecordCount: res.body.randomSongs?.song?.length || 0,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
getRoles: async (args) => {
|
||||||
|
const { apiClientProps } = args;
|
||||||
|
|
||||||
|
const res = await ssApiClient(apiClientProps).getArtists({});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to get artist list');
|
||||||
|
}
|
||||||
|
|
||||||
|
const roles = new Set<string>();
|
||||||
|
|
||||||
|
for (const index of res.body.artists?.index || []) {
|
||||||
|
for (const artist of index.artist) {
|
||||||
|
for (const role of artist.roles || []) {
|
||||||
|
roles.add(role);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const final: Array<string | { label: string; value: string }> = Array.from(roles).sort();
|
||||||
|
// Always add 'all artist' filter, even if there are no other roles
|
||||||
|
// This is relevant when switching from a server which has roles to one with
|
||||||
|
// no roles.
|
||||||
|
final.splice(0, 0, { label: 'all artists', value: '' });
|
||||||
|
return final;
|
||||||
|
},
|
||||||
getServerInfo: async (args) => {
|
getServerInfo: async (args) => {
|
||||||
const { apiClientProps } = args;
|
const { apiClientProps } = args;
|
||||||
|
|
||||||
@@ -864,7 +935,9 @@ export const SubsonicController: ControllerEndpoint = {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.albumIds || query.artistIds) {
|
const artistIds = query.albumArtistIds || query.artistIds;
|
||||||
|
|
||||||
|
if (query.albumIds || artistIds) {
|
||||||
if (query.albumIds) {
|
if (query.albumIds) {
|
||||||
for (const albumId of query.albumIds) {
|
for (const albumId of query.albumIds) {
|
||||||
fromAlbumPromises.push(
|
fromAlbumPromises.push(
|
||||||
@@ -877,8 +950,8 @@ export const SubsonicController: ControllerEndpoint = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.artistIds) {
|
if (artistIds) {
|
||||||
for (const artistId of query.artistIds) {
|
for (const artistId of artistIds) {
|
||||||
artistDetailPromises.push(
|
artistDetailPromises.push(
|
||||||
ssApiClient(apiClientProps).getArtist({
|
ssApiClient(apiClientProps).getArtist({
|
||||||
query: {
|
query: {
|
||||||
@@ -895,7 +968,7 @@ export const SubsonicController: ControllerEndpoint = {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return artist.body.artist.album;
|
return artist.body.artist.album ?? [];
|
||||||
});
|
});
|
||||||
|
|
||||||
const albumIds = albums.map((album) => album.id);
|
const albumIds = albums.map((album) => album.id);
|
||||||
@@ -1230,6 +1303,7 @@ export const SubsonicController: ControllerEndpoint = {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
||||||
search: async (args) => {
|
search: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { query, apiClientProps } = args;
|
||||||
|
|
||||||
@@ -1296,109 +1370,3 @@ export const SubsonicController: ControllerEndpoint = {
|
|||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// export const getAlbumArtistDetail = async (
|
|
||||||
// args: AlbumArtistDetailArgs,
|
|
||||||
// ): Promise<SSAlbumArtistDetail> => {
|
|
||||||
// const { server, signal, query } = args;
|
|
||||||
// const defaultParams = getDefaultParams(server);
|
|
||||||
|
|
||||||
// const searchParams: SSAlbumArtistDetailParams = {
|
|
||||||
// id: query.id,
|
|
||||||
// ...defaultParams,
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const data = await api
|
|
||||||
// .get('/getArtist.view', {
|
|
||||||
// prefixUrl: server?.url,
|
|
||||||
// searchParams,
|
|
||||||
// signal,
|
|
||||||
// })
|
|
||||||
// .json<SSAlbumArtistDetailResponse>();
|
|
||||||
|
|
||||||
// return data.artist;
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise<SSAlbumArtistList> => {
|
|
||||||
// const { signal, server, query } = args;
|
|
||||||
// const defaultParams = getDefaultParams(server);
|
|
||||||
|
|
||||||
// const searchParams: SSAlbumArtistListParams = {
|
|
||||||
// musicFolderId: query.musicFolderId,
|
|
||||||
// ...defaultParams,
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const data = await api
|
|
||||||
// .get('rest/getArtists.view', {
|
|
||||||
// prefixUrl: server?.url,
|
|
||||||
// searchParams,
|
|
||||||
// signal,
|
|
||||||
// })
|
|
||||||
// .json<SSAlbumArtistListResponse>();
|
|
||||||
|
|
||||||
// const artists = (data.artists?.index || []).flatMap((index: SSArtistIndex) => index.artist);
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// items: artists,
|
|
||||||
// startIndex: query.startIndex,
|
|
||||||
// totalRecordCount: null,
|
|
||||||
// };
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const getGenreList = async (args: GenreListArgs): Promise<SSGenreList> => {
|
|
||||||
// const { server, signal } = args;
|
|
||||||
// const defaultParams = getDefaultParams(server);
|
|
||||||
|
|
||||||
// const data = await api
|
|
||||||
// .get('rest/getGenres.view', {
|
|
||||||
// prefixUrl: server?.url,
|
|
||||||
// searchParams: defaultParams,
|
|
||||||
// signal,
|
|
||||||
// })
|
|
||||||
// .json<SSGenreListResponse>();
|
|
||||||
|
|
||||||
// return data.genres.genre;
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const getAlbumDetail = async (args: AlbumDetailArgs): Promise<SSAlbumDetail> => {
|
|
||||||
// const { server, query, signal } = args;
|
|
||||||
// const defaultParams = getDefaultParams(server);
|
|
||||||
|
|
||||||
// const searchParams = {
|
|
||||||
// id: query.id,
|
|
||||||
// ...defaultParams,
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const data = await api
|
|
||||||
// .get('rest/getAlbum.view', {
|
|
||||||
// prefixUrl: server?.url,
|
|
||||||
// searchParams: parseSearchParams(searchParams),
|
|
||||||
// signal,
|
|
||||||
// })
|
|
||||||
// .json<SSAlbumDetailResponse>();
|
|
||||||
|
|
||||||
// const { song: songs, ...dataWithoutSong } = data.album;
|
|
||||||
// return { ...dataWithoutSong, songs };
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const getAlbumList = async (args: AlbumListArgs): Promise<SSAlbumList> => {
|
|
||||||
// const { server, query, signal } = args;
|
|
||||||
// const defaultParams = getDefaultParams(server);
|
|
||||||
|
|
||||||
// const searchParams = {
|
|
||||||
// ...defaultParams,
|
|
||||||
// };
|
|
||||||
// const data = await api
|
|
||||||
// .get('rest/getAlbumList2.view', {
|
|
||||||
// prefixUrl: server?.url,
|
|
||||||
// searchParams: parseSearchParams(searchParams),
|
|
||||||
// signal,
|
|
||||||
// })
|
|
||||||
// .json<SSAlbumListResponse>();
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// items: data.albumList2.album,
|
|
||||||
// startIndex: query.startIndex,
|
|
||||||
// totalRecordCount: null,
|
|
||||||
// };
|
|
||||||
// };
|
|
||||||
|
|||||||
@@ -156,12 +156,13 @@ const albumListParameters = z.object({
|
|||||||
const albumList = z.array(album.omit({ song: true }));
|
const albumList = z.array(album.omit({ song: true }));
|
||||||
|
|
||||||
const albumArtist = z.object({
|
const albumArtist = z.object({
|
||||||
album: z.array(album),
|
album: z.array(album).optional(),
|
||||||
albumCount: z.string(),
|
albumCount: z.string(),
|
||||||
artistImageUrl: z.string().optional(),
|
artistImageUrl: z.string().optional(),
|
||||||
coverArt: z.string().optional(),
|
coverArt: z.string().optional(),
|
||||||
id,
|
id,
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
|
roles: z.array(z.string()).optional(),
|
||||||
starred: z.string().optional(),
|
starred: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -175,6 +176,7 @@ const artistListEntry = albumArtist.pick({
|
|||||||
coverArt: true,
|
coverArt: true,
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
|
roles: true,
|
||||||
starred: true,
|
starred: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -494,6 +494,7 @@ export interface SongListQuery extends BaseQuery<SongListSort> {
|
|||||||
jellyfin?: Partial<z.infer<typeof jfType._parameters.songList>>;
|
jellyfin?: Partial<z.infer<typeof jfType._parameters.songList>>;
|
||||||
navidrome?: Partial<z.infer<typeof ndType._parameters.songList>>;
|
navidrome?: Partial<z.infer<typeof ndType._parameters.songList>>;
|
||||||
};
|
};
|
||||||
|
albumArtistIds?: string[];
|
||||||
albumIds?: string[];
|
albumIds?: string[];
|
||||||
artistIds?: string[];
|
artistIds?: string[];
|
||||||
favorite?: boolean;
|
favorite?: boolean;
|
||||||
@@ -503,6 +504,7 @@ export interface SongListQuery extends BaseQuery<SongListSort> {
|
|||||||
maxYear?: number;
|
maxYear?: number;
|
||||||
minYear?: number;
|
minYear?: number;
|
||||||
musicFolderId?: string;
|
musicFolderId?: string;
|
||||||
|
role?: string;
|
||||||
searchTerm?: string;
|
searchTerm?: string;
|
||||||
startIndex: number;
|
startIndex: number;
|
||||||
}
|
}
|
||||||
@@ -672,7 +674,7 @@ export type AlbumArtistDetailQuery = { id: string };
|
|||||||
export type AlbumArtistDetailArgs = { query: AlbumArtistDetailQuery } & BaseEndpointArgs;
|
export type AlbumArtistDetailArgs = { query: AlbumArtistDetailQuery } & BaseEndpointArgs;
|
||||||
|
|
||||||
// Artist List
|
// Artist List
|
||||||
export type ArtistListResponse = BasePaginatedResponse<Artist[]> | null | undefined;
|
export type ArtistListResponse = BasePaginatedResponse<AlbumArtist[]> | null | undefined;
|
||||||
|
|
||||||
export enum ArtistListSort {
|
export enum ArtistListSort {
|
||||||
ALBUM = 'album',
|
ALBUM = 'album',
|
||||||
@@ -695,6 +697,8 @@ export interface ArtistListQuery extends BaseQuery<ArtistListSort> {
|
|||||||
};
|
};
|
||||||
limit?: number;
|
limit?: number;
|
||||||
musicFolderId?: string;
|
musicFolderId?: string;
|
||||||
|
role?: string;
|
||||||
|
searchTerm?: string;
|
||||||
startIndex: number;
|
startIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1245,7 +1249,8 @@ export type ControllerEndpoint = {
|
|||||||
getAlbumList: (args: AlbumListArgs) => Promise<AlbumListResponse>;
|
getAlbumList: (args: AlbumListArgs) => Promise<AlbumListResponse>;
|
||||||
getAlbumListCount: (args: AlbumListArgs) => Promise<number>;
|
getAlbumListCount: (args: AlbumListArgs) => Promise<number>;
|
||||||
// getArtistInfo?: (args: any) => void;
|
// getArtistInfo?: (args: any) => void;
|
||||||
// getArtistList?: (args: ArtistListArgs) => Promise<ArtistListResponse>;
|
getArtistList: (args: ArtistListArgs) => Promise<ArtistListResponse>;
|
||||||
|
getArtistListCount: (args: ArtistListArgs) => Promise<number>;
|
||||||
getDownloadUrl: (args: DownloadArgs) => string;
|
getDownloadUrl: (args: DownloadArgs) => string;
|
||||||
getGenreList: (args: GenreListArgs) => Promise<GenreListResponse>;
|
getGenreList: (args: GenreListArgs) => Promise<GenreListResponse>;
|
||||||
getLyrics?: (args: LyricsArgs) => Promise<LyricsResponse>;
|
getLyrics?: (args: LyricsArgs) => Promise<LyricsResponse>;
|
||||||
@@ -1255,6 +1260,7 @@ export type ControllerEndpoint = {
|
|||||||
getPlaylistListCount: (args: PlaylistListArgs) => Promise<number>;
|
getPlaylistListCount: (args: PlaylistListArgs) => Promise<number>;
|
||||||
getPlaylistSongList: (args: PlaylistSongListArgs) => Promise<SongListResponse>;
|
getPlaylistSongList: (args: PlaylistSongListArgs) => Promise<SongListResponse>;
|
||||||
getRandomSongList: (args: RandomSongListArgs) => Promise<SongListResponse>;
|
getRandomSongList: (args: RandomSongListArgs) => Promise<SongListResponse>;
|
||||||
|
getRoles: (args: BaseEndpointArgs) => Promise<Array<string | { label: string; value: string }>>;
|
||||||
getServerInfo: (args: ServerInfoArgs) => Promise<ServerInfo>;
|
getServerInfo: (args: ServerInfoArgs) => Promise<ServerInfo>;
|
||||||
getSimilarSongs: (args: SimilarSongsArgs) => Promise<Song[]>;
|
getSimilarSongs: (args: SimilarSongsArgs) => Promise<Song[]>;
|
||||||
getSongDetail: (args: SongDetailArgs) => Promise<SongDetailResponse>;
|
getSongDetail: (args: SongDetailArgs) => Promise<SongDetailResponse>;
|
||||||
@@ -1417,7 +1423,7 @@ export const sortSongList = (songs: QueueSong[], sortBy: SongListSort, sortOrder
|
|||||||
|
|
||||||
export const sortAlbumArtistList = (
|
export const sortAlbumArtistList = (
|
||||||
artists: AlbumArtist[],
|
artists: AlbumArtist[],
|
||||||
sortBy: AlbumArtistListSort,
|
sortBy: AlbumArtistListSort | ArtistListSort,
|
||||||
sortOrder: SortOrder,
|
sortOrder: SortOrder,
|
||||||
) => {
|
) => {
|
||||||
const order = sortOrder === SortOrder.ASC ? 'asc' : 'desc';
|
const order = sortOrder === SortOrder.ASC ? 'asc' : 'desc';
|
||||||
|
|||||||
@@ -207,11 +207,14 @@ export const FeatureCarousel = ({ data }: FeatureCarouselProps) => {
|
|||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
<Badge size="lg">{currentItem?.releaseYear}</Badge>
|
<Badge size="lg">{currentItem?.releaseYear}</Badge>
|
||||||
<Badge size="lg">
|
{currentItem?.songCount !== null &&
|
||||||
{t('entity.trackWithCount', {
|
currentItem?.songCount !== undefined && (
|
||||||
count: currentItem?.songCount || 0,
|
<Badge size="lg">
|
||||||
})}
|
{t('entity.trackWithCount', {
|
||||||
</Badge>
|
count: currentItem?.songCount || 0,
|
||||||
|
})}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
<Group position="apart">
|
<Group position="apart">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -32,12 +32,24 @@ export const useFixedTableHeader = ({ enabled }: { enabled: boolean }) => {
|
|||||||
if (!isTableHeaderInView && isTableInView) {
|
if (!isTableHeaderInView && isTableInView) {
|
||||||
header?.classList.add('ag-header-fixed');
|
header?.classList.add('ag-header-fixed');
|
||||||
root?.classList.add('ag-header-fixed-margin');
|
root?.classList.add('ag-header-fixed-margin');
|
||||||
|
|
||||||
|
if (windowBarStyle === Platform.WINDOWS || windowBarStyle === Platform.MACOS) {
|
||||||
|
header?.classList.add('ag-header-window-bar');
|
||||||
|
}
|
||||||
} else if (!isTableInView) {
|
} else if (!isTableInView) {
|
||||||
header?.classList.remove('ag-header-fixed');
|
header?.classList.remove('ag-header-fixed');
|
||||||
root?.classList.remove('ag-header-fixed-margin');
|
root?.classList.remove('ag-header-fixed-margin');
|
||||||
|
|
||||||
|
if (windowBarStyle === Platform.WINDOWS || windowBarStyle === Platform.MACOS) {
|
||||||
|
header?.classList.remove('ag-header-window-bar');
|
||||||
|
}
|
||||||
} else if (isTableHeaderInView) {
|
} else if (isTableHeaderInView) {
|
||||||
header?.classList.remove('ag-header-fixed');
|
header?.classList.remove('ag-header-fixed');
|
||||||
root?.classList.remove('ag-header-fixed-margin');
|
root?.classList.remove('ag-header-fixed-margin');
|
||||||
|
|
||||||
|
if (windowBarStyle === Platform.WINDOWS || windowBarStyle === Platform.MACOS) {
|
||||||
|
header?.classList.remove('ag-header-window-bar');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [enabled, isTableHeaderInView, isTableInView, windowBarStyle]);
|
}, [enabled, isTableHeaderInView, isTableInView, windowBarStyle]);
|
||||||
|
|
||||||
|
|||||||
@@ -105,44 +105,42 @@ export const useVirtualTable = <TFilter extends BaseQuery<any>>({
|
|||||||
const queryKeyFn:
|
const queryKeyFn:
|
||||||
| ((serverId: string, query: Record<any, any>, pagination: QueryPagination) => QueryKey)
|
| ((serverId: string, query: Record<any, any>, pagination: QueryPagination) => QueryKey)
|
||||||
| null = useMemo(() => {
|
| null = useMemo(() => {
|
||||||
if (itemType === LibraryItem.ALBUM) {
|
switch (itemType) {
|
||||||
return queryKeys.albums.list;
|
case LibraryItem.ALBUM:
|
||||||
|
return queryKeys.albums.list;
|
||||||
|
case LibraryItem.ALBUM_ARTIST:
|
||||||
|
return queryKeys.albumArtists.list;
|
||||||
|
case LibraryItem.ARTIST:
|
||||||
|
return queryKeys.artists.list;
|
||||||
|
case LibraryItem.GENRE:
|
||||||
|
return queryKeys.genres.list;
|
||||||
|
case LibraryItem.PLAYLIST:
|
||||||
|
return queryKeys.playlists.list;
|
||||||
|
case LibraryItem.SONG:
|
||||||
|
return queryKeys.songs.list;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
if (itemType === LibraryItem.ALBUM_ARTIST) {
|
|
||||||
return queryKeys.albumArtists.list;
|
|
||||||
}
|
|
||||||
if (itemType === LibraryItem.PLAYLIST) {
|
|
||||||
return queryKeys.playlists.list;
|
|
||||||
}
|
|
||||||
if (itemType === LibraryItem.SONG) {
|
|
||||||
return queryKeys.songs.list;
|
|
||||||
}
|
|
||||||
if (itemType === LibraryItem.GENRE) {
|
|
||||||
return queryKeys.genres.list;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}, [itemType]);
|
}, [itemType]);
|
||||||
|
|
||||||
const queryFn: ((args: any) => Promise<BasePaginatedResponse<any> | null | undefined>) | null =
|
const queryFn: ((args: any) => Promise<BasePaginatedResponse<any> | null | undefined>) | null =
|
||||||
useMemo(() => {
|
useMemo(() => {
|
||||||
if (itemType === LibraryItem.ALBUM) {
|
switch (itemType) {
|
||||||
return api.controller.getAlbumList;
|
case LibraryItem.ALBUM:
|
||||||
|
return api.controller.getAlbumList;
|
||||||
|
case LibraryItem.ALBUM_ARTIST:
|
||||||
|
return api.controller.getAlbumArtistList;
|
||||||
|
case LibraryItem.ARTIST:
|
||||||
|
return api.controller.getArtistList;
|
||||||
|
case LibraryItem.GENRE:
|
||||||
|
return api.controller.getGenreList;
|
||||||
|
case LibraryItem.PLAYLIST:
|
||||||
|
return api.controller.getPlaylistList;
|
||||||
|
case LibraryItem.SONG:
|
||||||
|
return api.controller.getSongList;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
if (itemType === LibraryItem.ALBUM_ARTIST) {
|
|
||||||
return api.controller.getAlbumArtistList;
|
|
||||||
}
|
|
||||||
if (itemType === LibraryItem.PLAYLIST) {
|
|
||||||
return api.controller.getPlaylistList;
|
|
||||||
}
|
|
||||||
if (itemType === LibraryItem.SONG) {
|
|
||||||
return api.controller.getSongList;
|
|
||||||
}
|
|
||||||
if (itemType === LibraryItem.GENRE) {
|
|
||||||
return api.controller.getGenreList;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}, [itemType]);
|
}, [itemType]);
|
||||||
|
|
||||||
const onGridReady = useCallback(
|
const onGridReady = useCallback(
|
||||||
@@ -390,7 +388,9 @@ export const useVirtualTable = <TFilter extends BaseQuery<any>>({
|
|||||||
break;
|
break;
|
||||||
case LibraryItem.ARTIST:
|
case LibraryItem.ARTIST:
|
||||||
navigate(
|
navigate(
|
||||||
generatePath(AppRoute.LIBRARY_ARTISTS_DETAIL, { artistId: e.data.id }),
|
generatePath(AppRoute.LIBRARY_ARTISTS_DETAIL, {
|
||||||
|
artistId: e.data.id,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case LibraryItem.PLAYLIST:
|
case LibraryItem.PLAYLIST:
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ const AlbumListRoute = () => {
|
|||||||
|
|
||||||
const artist = searchParams.get('artistName');
|
const artist = searchParams.get('artistName');
|
||||||
const title = artist
|
const title = artist
|
||||||
? t('page.albumList.artistAlbums', { artist })
|
? t('page.albumList.artistAlbums', { artist, postProcess: 'sentenceCase' })
|
||||||
: genreId
|
: genreId
|
||||||
? t('page.albumList.genreAlbums', { genre: titleCase(genreTitle) })
|
? t('page.albumList.genreAlbums', { genre: titleCase(genreTitle) })
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|||||||
@@ -66,7 +66,11 @@ interface AlbumArtistDetailContentProps {
|
|||||||
export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailContentProps) => {
|
export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailContentProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { artistItems, externalLinks } = useGeneralSettings();
|
const { artistItems, externalLinks } = useGeneralSettings();
|
||||||
const { albumArtistId } = useParams() as { albumArtistId: string };
|
const { albumArtistId, artistId } = useParams() as {
|
||||||
|
albumArtistId?: string;
|
||||||
|
artistId?: string;
|
||||||
|
};
|
||||||
|
const routeId = (artistId || albumArtistId) as string;
|
||||||
const cq = useContainerQuery();
|
const cq = useContainerQuery();
|
||||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
@@ -85,24 +89,24 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
|||||||
}, [artistItems]);
|
}, [artistItems]);
|
||||||
|
|
||||||
const detailQuery = useAlbumArtistDetail({
|
const detailQuery = useAlbumArtistDetail({
|
||||||
query: { id: albumArtistId },
|
query: { id: routeId },
|
||||||
serverId: server?.id,
|
serverId: server?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const artistDiscographyLink = `${generatePath(
|
const artistDiscographyLink = `${generatePath(
|
||||||
AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_DISCOGRAPHY,
|
AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_DISCOGRAPHY,
|
||||||
{
|
{
|
||||||
albumArtistId,
|
albumArtistId: routeId,
|
||||||
},
|
},
|
||||||
)}?${createSearchParams({
|
)}?${createSearchParams({
|
||||||
artistId: albumArtistId,
|
artistId: routeId,
|
||||||
artistName: detailQuery?.data?.name || '',
|
artistName: detailQuery?.data?.name || '',
|
||||||
})}`;
|
})}`;
|
||||||
|
|
||||||
const artistSongsLink = `${generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_SONGS, {
|
const artistSongsLink = `${generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_SONGS, {
|
||||||
albumArtistId,
|
albumArtistId: routeId,
|
||||||
})}?${createSearchParams({
|
})}?${createSearchParams({
|
||||||
artistId: albumArtistId,
|
artistId: routeId,
|
||||||
artistName: detailQuery?.data?.name || '',
|
artistName: detailQuery?.data?.name || '',
|
||||||
})}`;
|
})}`;
|
||||||
|
|
||||||
@@ -111,7 +115,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
|||||||
enabled: enabledItem.recentAlbums,
|
enabled: enabledItem.recentAlbums,
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
artistIds: [albumArtistId],
|
artistIds: [routeId],
|
||||||
limit: 15,
|
limit: 15,
|
||||||
sortBy: AlbumListSort.RELEASE_DATE,
|
sortBy: AlbumListSort.RELEASE_DATE,
|
||||||
sortOrder: SortOrder.DESC,
|
sortOrder: SortOrder.DESC,
|
||||||
@@ -125,7 +129,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
|||||||
enabled: enabledItem.compilations && server?.type !== ServerType.SUBSONIC,
|
enabled: enabledItem.compilations && server?.type !== ServerType.SUBSONIC,
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
artistIds: [albumArtistId],
|
artistIds: [routeId],
|
||||||
compilation: true,
|
compilation: true,
|
||||||
limit: 15,
|
limit: 15,
|
||||||
sortBy: AlbumListSort.RELEASE_DATE,
|
sortBy: AlbumListSort.RELEASE_DATE,
|
||||||
@@ -141,7 +145,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
|||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
artist: detailQuery?.data?.name || '',
|
artist: detailQuery?.data?.name || '',
|
||||||
artistId: albumArtistId,
|
artistId: routeId,
|
||||||
},
|
},
|
||||||
serverId: server?.id,
|
serverId: server?.id,
|
||||||
});
|
});
|
||||||
@@ -292,8 +296,8 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
|||||||
const handlePlay = async (playType?: Play) => {
|
const handlePlay = async (playType?: Play) => {
|
||||||
handlePlayQueueAdd?.({
|
handlePlayQueueAdd?.({
|
||||||
byItemType: {
|
byItemType: {
|
||||||
id: [albumArtistId],
|
id: [routeId],
|
||||||
type: LibraryItem.ALBUM_ARTIST,
|
type: albumArtistId ? LibraryItem.ALBUM_ARTIST : LibraryItem.ARTIST,
|
||||||
},
|
},
|
||||||
playType: playType || playButtonBehavior,
|
playType: playType || playButtonBehavior,
|
||||||
});
|
});
|
||||||
@@ -336,9 +340,15 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const albumCount = detailQuery?.data?.albumCount;
|
||||||
|
const artistContextItems =
|
||||||
|
(albumCount ?? 1) > 0
|
||||||
|
? ARTIST_CONTEXT_MENU_ITEMS
|
||||||
|
: ARTIST_CONTEXT_MENU_ITEMS.filter((item) => !item.id.toLowerCase().includes('play'));
|
||||||
|
|
||||||
const handleGeneralContextMenu = useHandleGeneralContextMenu(
|
const handleGeneralContextMenu = useHandleGeneralContextMenu(
|
||||||
LibraryItem.ALBUM_ARTIST,
|
LibraryItem.ALBUM_ARTIST,
|
||||||
ARTIST_CONTEXT_MENU_ITEMS,
|
artistContextItems,
|
||||||
);
|
);
|
||||||
|
|
||||||
const topSongs = topSongsQuery?.data?.items?.slice(0, 10);
|
const topSongs = topSongsQuery?.data?.items?.slice(0, 10);
|
||||||
@@ -365,7 +375,10 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
|||||||
<LibraryBackgroundOverlay $backgroundColor={background} />
|
<LibraryBackgroundOverlay $backgroundColor={background} />
|
||||||
<DetailContainer>
|
<DetailContainer>
|
||||||
<Group spacing="md">
|
<Group spacing="md">
|
||||||
<PlayButton onClick={() => handlePlay(playButtonBehavior)} />
|
<PlayButton
|
||||||
|
disabled={albumCount === 0}
|
||||||
|
onClick={() => handlePlay(playButtonBehavior)}
|
||||||
|
/>
|
||||||
<Group spacing="xs">
|
<Group spacing="xs">
|
||||||
<Button
|
<Button
|
||||||
compact
|
compact
|
||||||
@@ -528,7 +541,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
|||||||
to={generatePath(
|
to={generatePath(
|
||||||
AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_TOP_SONGS,
|
AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_TOP_SONGS,
|
||||||
{
|
{
|
||||||
albumArtistId,
|
albumArtistId: routeId,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
|
|||||||
@@ -16,33 +16,41 @@ interface AlbumArtistDetailHeaderProps {
|
|||||||
|
|
||||||
export const AlbumArtistDetailHeader = forwardRef(
|
export const AlbumArtistDetailHeader = forwardRef(
|
||||||
({ background }: AlbumArtistDetailHeaderProps, ref: Ref<HTMLDivElement>) => {
|
({ background }: AlbumArtistDetailHeaderProps, ref: Ref<HTMLDivElement>) => {
|
||||||
const { albumArtistId } = useParams() as { albumArtistId: string };
|
const { albumArtistId, artistId } = useParams() as {
|
||||||
|
albumArtistId?: string;
|
||||||
|
artistId?: string;
|
||||||
|
};
|
||||||
|
const routeId = (artistId || albumArtistId) as string;
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const detailQuery = useAlbumArtistDetail({
|
const detailQuery = useAlbumArtistDetail({
|
||||||
query: { id: albumArtistId },
|
query: { id: routeId },
|
||||||
serverId: server?.id,
|
serverId: server?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const albumCount = detailQuery?.data?.albumCount;
|
||||||
|
const songCount = detailQuery?.data?.songCount;
|
||||||
|
const duration = detailQuery?.data?.duration;
|
||||||
|
const durationEnabled = duration !== null && duration !== undefined;
|
||||||
|
|
||||||
const metadataItems = [
|
const metadataItems = [
|
||||||
{
|
{
|
||||||
enabled: detailQuery?.data?.albumCount,
|
enabled: albumCount !== null && albumCount !== undefined,
|
||||||
id: 'albumCount',
|
id: 'albumCount',
|
||||||
secondary: false,
|
secondary: false,
|
||||||
value: t('entity.albumWithCount', { count: detailQuery?.data?.albumCount || 0 }),
|
value: t('entity.albumWithCount', { count: albumCount || 0 }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: detailQuery?.data?.songCount,
|
enabled: songCount !== null && songCount !== undefined,
|
||||||
id: 'songCount',
|
id: 'songCount',
|
||||||
secondary: false,
|
secondary: false,
|
||||||
value: t('entity.trackWithCount', { count: detailQuery?.data?.songCount || 0 }),
|
value: t('entity.trackWithCount', { count: songCount || 0 }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: detailQuery.data?.duration,
|
enabled: durationEnabled,
|
||||||
id: 'duration',
|
id: 'duration',
|
||||||
secondary: true,
|
secondary: true,
|
||||||
value:
|
value: durationEnabled && formatDurationString(duration),
|
||||||
detailQuery?.data?.duration && formatDurationString(detailQuery.data.duration),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||||
|
import { lazy, MutableRefObject, Suspense } from 'react';
|
||||||
|
import { Spinner } from '/@/renderer/components';
|
||||||
|
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||||
|
import { ListDisplayType } from '/@/renderer/types';
|
||||||
|
import { useListStoreByKey } from '../../../store/list.store';
|
||||||
|
import { useListContext } from '/@/renderer/context/list-context';
|
||||||
|
|
||||||
|
const ArtistListGridView = lazy(() =>
|
||||||
|
import('/@/renderer/features/artists/components/artist-list-grid-view').then((module) => ({
|
||||||
|
default: module.ArtistListGridView,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const ArtistListTableView = lazy(() =>
|
||||||
|
import('/@/renderer/features/artists/components/artist-list-table-view').then((module) => ({
|
||||||
|
default: module.ArtistListTableView,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
interface ArtistListContentProps {
|
||||||
|
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
|
||||||
|
itemCount?: number;
|
||||||
|
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ArtistListContent = ({ itemCount, gridRef, tableRef }: ArtistListContentProps) => {
|
||||||
|
const { pageKey } = useListContext();
|
||||||
|
const { display } = useListStoreByKey({ key: pageKey });
|
||||||
|
const isGrid = display === ListDisplayType.CARD || display === ListDisplayType.POSTER;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<Spinner container />}>
|
||||||
|
{isGrid ? (
|
||||||
|
<ArtistListGridView
|
||||||
|
gridRef={gridRef}
|
||||||
|
itemCount={itemCount}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ArtistListTableView
|
||||||
|
itemCount={itemCount}
|
||||||
|
tableRef={tableRef}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import { QueryKey, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { MutableRefObject, useCallback, useMemo } from 'react';
|
||||||
|
import AutoSizer, { Size } from 'react-virtualized-auto-sizer';
|
||||||
|
import { ListOnScrollProps } from 'react-window';
|
||||||
|
import { VirtualGridAutoSizerContainer } from '../../../components/virtual-grid/virtual-grid-wrapper';
|
||||||
|
import { useListStoreByKey } from '../../../store/list.store';
|
||||||
|
import { api } from '/@/renderer/api';
|
||||||
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
|
import {
|
||||||
|
AlbumArtist,
|
||||||
|
ArtistListQuery,
|
||||||
|
ArtistListResponse,
|
||||||
|
ArtistListSort,
|
||||||
|
LibraryItem,
|
||||||
|
} from '/@/renderer/api/types';
|
||||||
|
import { ALBUMARTIST_CARD_ROWS } from '/@/renderer/components';
|
||||||
|
import { VirtualInfiniteGrid, VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||||
|
import { useListContext } from '/@/renderer/context/list-context';
|
||||||
|
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
||||||
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
|
import { useCurrentServer, useListStoreActions } from '/@/renderer/store';
|
||||||
|
import { CardRow, ListDisplayType } from '/@/renderer/types';
|
||||||
|
import { useHandleFavorite } from '/@/renderer/features/shared/hooks/use-handle-favorite';
|
||||||
|
|
||||||
|
interface ArtistListGridViewProps {
|
||||||
|
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
|
||||||
|
itemCount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ArtistListGridView = ({ itemCount, gridRef }: ArtistListGridViewProps) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const server = useCurrentServer();
|
||||||
|
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||||
|
|
||||||
|
const { pageKey } = useListContext();
|
||||||
|
const { grid, display, filter } = useListStoreByKey<ArtistListQuery>({ key: pageKey });
|
||||||
|
const { setGrid } = useListStoreActions();
|
||||||
|
const handleFavorite = useHandleFavorite({ gridRef, server });
|
||||||
|
|
||||||
|
const fetchInitialData = useCallback(() => {
|
||||||
|
const query: Omit<ArtistListQuery, 'startIndex' | 'limit'> = {
|
||||||
|
...filter,
|
||||||
|
};
|
||||||
|
|
||||||
|
const queriesFromCache: [QueryKey, ArtistListResponse][] = queryClient.getQueriesData({
|
||||||
|
exact: false,
|
||||||
|
fetchStatus: 'idle',
|
||||||
|
queryKey: queryKeys.artists.list(server?.id || '', query),
|
||||||
|
stale: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const itemData = [];
|
||||||
|
|
||||||
|
for (const [, data] of queriesFromCache) {
|
||||||
|
const { items, startIndex } = data || {};
|
||||||
|
|
||||||
|
if (items && items.length !== 1 && startIndex !== undefined) {
|
||||||
|
let itemIndex = 0;
|
||||||
|
for (
|
||||||
|
let rowIndex = startIndex;
|
||||||
|
rowIndex < startIndex + items.length;
|
||||||
|
rowIndex += 1
|
||||||
|
) {
|
||||||
|
itemData[rowIndex] = items[itemIndex];
|
||||||
|
itemIndex += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return itemData;
|
||||||
|
}, [filter, queryClient, server?.id]);
|
||||||
|
|
||||||
|
const fetch = useCallback(
|
||||||
|
async ({ skip: startIndex, take: limit }: { skip: number; take: number }) => {
|
||||||
|
const query: ArtistListQuery = {
|
||||||
|
...filter,
|
||||||
|
limit,
|
||||||
|
startIndex,
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryKey = queryKeys.artists.list(server?.id || '', query);
|
||||||
|
|
||||||
|
const artistsRes = await queryClient.fetchQuery(
|
||||||
|
queryKey,
|
||||||
|
async ({ signal }) =>
|
||||||
|
api.controller.getArtistList({
|
||||||
|
apiClientProps: {
|
||||||
|
server,
|
||||||
|
signal,
|
||||||
|
},
|
||||||
|
query,
|
||||||
|
}),
|
||||||
|
{ cacheTime: 1000 * 60 * 1 },
|
||||||
|
);
|
||||||
|
|
||||||
|
return artistsRes;
|
||||||
|
},
|
||||||
|
[filter, queryClient, server],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleGridScroll = useCallback(
|
||||||
|
(e: ListOnScrollProps) => {
|
||||||
|
setGrid({ data: { scrollOffset: e.scrollOffset }, key: pageKey });
|
||||||
|
},
|
||||||
|
[pageKey, setGrid],
|
||||||
|
);
|
||||||
|
|
||||||
|
const cardRows = useMemo(() => {
|
||||||
|
const rows: CardRow<AlbumArtist>[] = [ALBUMARTIST_CARD_ROWS.name];
|
||||||
|
|
||||||
|
switch (filter.sortBy) {
|
||||||
|
case ArtistListSort.DURATION:
|
||||||
|
rows.push(ALBUMARTIST_CARD_ROWS.duration);
|
||||||
|
break;
|
||||||
|
case ArtistListSort.FAVORITED:
|
||||||
|
break;
|
||||||
|
case ArtistListSort.NAME:
|
||||||
|
break;
|
||||||
|
case ArtistListSort.ALBUM_COUNT:
|
||||||
|
rows.push(ALBUMARTIST_CARD_ROWS.albumCount);
|
||||||
|
break;
|
||||||
|
case ArtistListSort.PLAY_COUNT:
|
||||||
|
rows.push(ALBUMARTIST_CARD_ROWS.playCount);
|
||||||
|
break;
|
||||||
|
case ArtistListSort.RANDOM:
|
||||||
|
break;
|
||||||
|
case ArtistListSort.RATING:
|
||||||
|
rows.push(ALBUMARTIST_CARD_ROWS.rating);
|
||||||
|
break;
|
||||||
|
case ArtistListSort.RECENTLY_ADDED:
|
||||||
|
break;
|
||||||
|
case ArtistListSort.SONG_COUNT:
|
||||||
|
rows.push(ALBUMARTIST_CARD_ROWS.songCount);
|
||||||
|
break;
|
||||||
|
case ArtistListSort.RELEASE_DATE:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
}, [filter.sortBy]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VirtualGridAutoSizerContainer>
|
||||||
|
<AutoSizer>
|
||||||
|
{({ height, width }: Size) => (
|
||||||
|
<VirtualInfiniteGrid
|
||||||
|
ref={gridRef}
|
||||||
|
cardRows={cardRows}
|
||||||
|
display={display || ListDisplayType.CARD}
|
||||||
|
fetchFn={fetch}
|
||||||
|
fetchInitialData={fetchInitialData}
|
||||||
|
handleFavorite={handleFavorite}
|
||||||
|
handlePlayQueueAdd={handlePlayQueueAdd}
|
||||||
|
height={height}
|
||||||
|
initialScrollOffset={grid?.scrollOffset || 0}
|
||||||
|
itemCount={itemCount || 0}
|
||||||
|
itemGap={grid?.itemGap ?? 10}
|
||||||
|
itemSize={grid?.itemSize || 200}
|
||||||
|
itemType={LibraryItem.ARTIST}
|
||||||
|
loading={itemCount === undefined || itemCount === null}
|
||||||
|
minimumBatchSize={40}
|
||||||
|
route={{
|
||||||
|
route: AppRoute.LIBRARY_ARTISTS_DETAIL,
|
||||||
|
slugs: [{ idProperty: 'id', slugProperty: 'artistId' }],
|
||||||
|
}}
|
||||||
|
width={width}
|
||||||
|
onScroll={handleGridScroll}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AutoSizer>
|
||||||
|
</VirtualGridAutoSizerContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,619 @@
|
|||||||
|
import { ChangeEvent, MouseEvent, MutableRefObject, useCallback } 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 { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { RiFolder2Line, RiMoreFill, RiRefreshLine, RiSettings3Fill } from 'react-icons/ri';
|
||||||
|
import { useListContext } from '../../../context/list-context';
|
||||||
|
import { api } from '/@/renderer/api';
|
||||||
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
|
import {
|
||||||
|
ArtistListQuery,
|
||||||
|
ArtistListSort,
|
||||||
|
LibraryItem,
|
||||||
|
ServerType,
|
||||||
|
SortOrder,
|
||||||
|
} from '/@/renderer/api/types';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DropdownMenu,
|
||||||
|
MultiSelect,
|
||||||
|
Select,
|
||||||
|
Slider,
|
||||||
|
Switch,
|
||||||
|
Text,
|
||||||
|
} from '/@/renderer/components';
|
||||||
|
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||||
|
import { ALBUMARTIST_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
|
||||||
|
import { OrderToggleButton, useMusicFolders } from '/@/renderer/features/shared';
|
||||||
|
import { useContainerQuery } from '/@/renderer/hooks';
|
||||||
|
import {
|
||||||
|
ArtistListFilter,
|
||||||
|
useCurrentServer,
|
||||||
|
useListStoreActions,
|
||||||
|
useListStoreByKey,
|
||||||
|
} from '/@/renderer/store';
|
||||||
|
import { ListDisplayType, TableColumn } from '/@/renderer/types';
|
||||||
|
import i18n from '/@/i18n/i18n';
|
||||||
|
import { useRoles } from '/@/renderer/features/artists/queries/roles-query';
|
||||||
|
|
||||||
|
const FILTERS = {
|
||||||
|
jellyfin: [
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.ASC,
|
||||||
|
name: i18n.t('filter.album', { postProcess: 'titleCase' }),
|
||||||
|
value: ArtistListSort.ALBUM,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: i18n.t('filter.duration', { postProcess: 'titleCase' }),
|
||||||
|
value: ArtistListSort.DURATION,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.ASC,
|
||||||
|
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
|
||||||
|
value: ArtistListSort.NAME,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.ASC,
|
||||||
|
name: i18n.t('filter.random', { postProcess: 'titleCase' }),
|
||||||
|
value: ArtistListSort.RANDOM,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }),
|
||||||
|
value: ArtistListSort.RECENTLY_ADDED,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
navidrome: [
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: i18n.t('filter.albumCount', { postProcess: 'titleCase' }),
|
||||||
|
value: ArtistListSort.ALBUM_COUNT,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: i18n.t('filter.isFavorited', { postProcess: 'titleCase' }),
|
||||||
|
value: ArtistListSort.FAVORITED,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: i18n.t('filter.mostPlayed', { postProcess: 'titleCase' }),
|
||||||
|
value: ArtistListSort.PLAY_COUNT,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.ASC,
|
||||||
|
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
|
||||||
|
value: ArtistListSort.NAME,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: i18n.t('filter.rating', { postProcess: 'titleCase' }),
|
||||||
|
value: ArtistListSort.RATING,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: i18n.t('filter.songCount', { postProcess: 'titleCase' }),
|
||||||
|
value: ArtistListSort.SONG_COUNT,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
subsonic: [
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: i18n.t('filter.albumCount', { postProcess: 'titleCase' }),
|
||||||
|
value: ArtistListSort.ALBUM_COUNT,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: i18n.t('filter.isFavorited', { postProcess: 'titleCase' }),
|
||||||
|
value: ArtistListSort.FAVORITED,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.ASC,
|
||||||
|
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
|
||||||
|
value: ArtistListSort.NAME,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: i18n.t('filter.rating', { postProcess: 'titleCase' }),
|
||||||
|
value: ArtistListSort.RATING,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ArtistListHeaderFiltersProps {
|
||||||
|
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
|
||||||
|
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ArtistListHeaderFilters = ({ gridRef, tableRef }: ArtistListHeaderFiltersProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const server = useCurrentServer();
|
||||||
|
const { pageKey } = useListContext();
|
||||||
|
const { display, table, grid, filter } = useListStoreByKey<ArtistListQuery>({
|
||||||
|
key: pageKey,
|
||||||
|
});
|
||||||
|
const { setFilter, setTable, setTablePagination, setDisplayType, setGrid } =
|
||||||
|
useListStoreActions();
|
||||||
|
const cq = useContainerQuery();
|
||||||
|
const roles = useRoles({
|
||||||
|
options: {
|
||||||
|
cacheTime: 1000 * 60 * 60 * 2,
|
||||||
|
staleTime: 1000 * 60 * 60 * 2,
|
||||||
|
},
|
||||||
|
query: {},
|
||||||
|
serverId: server?.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isGrid = display === ListDisplayType.CARD || display === ListDisplayType.POSTER;
|
||||||
|
const musicFoldersQuery = useMusicFolders({ query: null, serverId: server?.id });
|
||||||
|
|
||||||
|
const sortByLabel =
|
||||||
|
(server?.type &&
|
||||||
|
FILTERS[server.type as keyof typeof FILTERS].find((f) => f.value === filter.sortBy)
|
||||||
|
?.name) ||
|
||||||
|
t('common.unknown', { postProcess: 'titleCase' });
|
||||||
|
|
||||||
|
const handleItemSize = (e: number) => {
|
||||||
|
if (display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) {
|
||||||
|
setTable({ data: { rowHeight: e }, key: pageKey });
|
||||||
|
} else {
|
||||||
|
setGrid({ data: { itemSize: e }, key: pageKey });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleItemGap = (e: number) => {
|
||||||
|
setGrid({ data: { itemGap: e }, key: pageKey });
|
||||||
|
};
|
||||||
|
|
||||||
|
const debouncedHandleItemSize = debounce(handleItemSize, 20);
|
||||||
|
|
||||||
|
const fetch = useCallback(
|
||||||
|
async (startIndex: number, limit: number, filters: ArtistListFilter) => {
|
||||||
|
const queryKey = queryKeys.artists.list(server?.id || '', {
|
||||||
|
limit,
|
||||||
|
startIndex,
|
||||||
|
...filters,
|
||||||
|
});
|
||||||
|
|
||||||
|
const albums = await queryClient.fetchQuery(
|
||||||
|
queryKey,
|
||||||
|
async ({ signal }) =>
|
||||||
|
api.controller.getArtistList({
|
||||||
|
apiClientProps: {
|
||||||
|
server,
|
||||||
|
signal,
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
limit,
|
||||||
|
startIndex,
|
||||||
|
...filters,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{ cacheTime: 1000 * 60 * 1 },
|
||||||
|
);
|
||||||
|
|
||||||
|
return albums;
|
||||||
|
},
|
||||||
|
[queryClient, server],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFilterChange = useCallback(
|
||||||
|
async (filters: ArtistListFilter) => {
|
||||||
|
if (display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) {
|
||||||
|
const dataSource: IDatasource = {
|
||||||
|
getRows: async (params) => {
|
||||||
|
const limit = params.endRow - params.startRow;
|
||||||
|
const startIndex = params.startRow;
|
||||||
|
|
||||||
|
const queryKey = queryKeys.artists.list(server?.id || '', {
|
||||||
|
limit,
|
||||||
|
startIndex,
|
||||||
|
...filters,
|
||||||
|
});
|
||||||
|
|
||||||
|
const artistsRes = await queryClient.fetchQuery(
|
||||||
|
queryKey,
|
||||||
|
async ({ signal }) =>
|
||||||
|
api.controller.getArtistList({
|
||||||
|
apiClientProps: {
|
||||||
|
server,
|
||||||
|
signal,
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
limit,
|
||||||
|
startIndex,
|
||||||
|
...filters,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{ cacheTime: 1000 * 60 * 1 },
|
||||||
|
);
|
||||||
|
|
||||||
|
params.successCallback(
|
||||||
|
artistsRes?.items || [],
|
||||||
|
artistsRes?.totalRecordCount || 0,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
rowCount: undefined,
|
||||||
|
};
|
||||||
|
tableRef.current?.api.setDatasource(dataSource);
|
||||||
|
tableRef.current?.api.purgeInfiniteCache();
|
||||||
|
tableRef.current?.api.ensureIndexVisible(0, 'top');
|
||||||
|
|
||||||
|
if (display === ListDisplayType.TABLE_PAGINATED) {
|
||||||
|
setTablePagination({ data: { currentPage: 0 }, key: pageKey });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
gridRef.current?.scrollTo(0);
|
||||||
|
gridRef.current?.resetLoadMoreItemsCache();
|
||||||
|
|
||||||
|
// Refetching within the virtualized grid may be inconsistent due to it refetching
|
||||||
|
// using an outdated set of filters. To avoid this, we fetch using the updated filters
|
||||||
|
// and then set the grid's data here.
|
||||||
|
const data = await fetch(0, 200, filters);
|
||||||
|
|
||||||
|
if (!data?.items) return;
|
||||||
|
gridRef.current?.setItemData(data.items);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[display, tableRef, server, queryClient, setTablePagination, pageKey, gridRef, fetch],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSetSortBy = useCallback(
|
||||||
|
(e: MouseEvent<HTMLButtonElement>) => {
|
||||||
|
if (!e.currentTarget?.value || !server?.type) return;
|
||||||
|
|
||||||
|
const sortOrder = FILTERS[server.type as keyof typeof FILTERS].find(
|
||||||
|
(f) => f.value === e.currentTarget.value,
|
||||||
|
)?.defaultOrder;
|
||||||
|
|
||||||
|
const updatedFilters = setFilter({
|
||||||
|
data: {
|
||||||
|
sortBy: e.currentTarget.value as ArtistListSort,
|
||||||
|
sortOrder: sortOrder || SortOrder.ASC,
|
||||||
|
},
|
||||||
|
itemType: LibraryItem.ARTIST,
|
||||||
|
key: pageKey,
|
||||||
|
}) as ArtistListFilter;
|
||||||
|
|
||||||
|
handleFilterChange(updatedFilters);
|
||||||
|
},
|
||||||
|
[handleFilterChange, pageKey, server?.type, setFilter],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSetMusicFolder = useCallback(
|
||||||
|
(e: MouseEvent<HTMLButtonElement>) => {
|
||||||
|
if (!e.currentTarget?.value) return;
|
||||||
|
|
||||||
|
let updatedFilters = null;
|
||||||
|
if (e.currentTarget.value === String(filter.musicFolderId)) {
|
||||||
|
updatedFilters = setFilter({
|
||||||
|
data: { musicFolderId: undefined },
|
||||||
|
itemType: LibraryItem.ARTIST,
|
||||||
|
key: pageKey,
|
||||||
|
}) as ArtistListFilter;
|
||||||
|
} else {
|
||||||
|
updatedFilters = setFilter({
|
||||||
|
data: { musicFolderId: e.currentTarget.value },
|
||||||
|
itemType: LibraryItem.ARTIST,
|
||||||
|
key: pageKey,
|
||||||
|
}) as ArtistListFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFilterChange(updatedFilters);
|
||||||
|
},
|
||||||
|
[filter.musicFolderId, handleFilterChange, setFilter, pageKey],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleToggleSortOrder = useCallback(() => {
|
||||||
|
const newSortOrder = filter.sortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC;
|
||||||
|
const updatedFilters = setFilter({
|
||||||
|
data: { sortOrder: newSortOrder },
|
||||||
|
itemType: LibraryItem.ARTIST,
|
||||||
|
key: pageKey,
|
||||||
|
}) as ArtistListFilter;
|
||||||
|
handleFilterChange(updatedFilters);
|
||||||
|
}, [filter.sortOrder, handleFilterChange, pageKey, setFilter]);
|
||||||
|
|
||||||
|
const handleSetViewType = useCallback(
|
||||||
|
(e: MouseEvent<HTMLButtonElement>) => {
|
||||||
|
if (!e.currentTarget?.value) return;
|
||||||
|
|
||||||
|
setDisplayType({ data: e.currentTarget.value as ListDisplayType, key: pageKey });
|
||||||
|
},
|
||||||
|
[pageKey, setDisplayType],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleTableColumns = (values: TableColumn[]) => {
|
||||||
|
const existingColumns = table.columns;
|
||||||
|
|
||||||
|
if (values.length === 0) {
|
||||||
|
return setTable({
|
||||||
|
data: {
|
||||||
|
columns: [],
|
||||||
|
},
|
||||||
|
key: pageKey,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If adding a column
|
||||||
|
if (values.length > existingColumns.length) {
|
||||||
|
const newColumn = { column: values[values.length - 1], width: 100 };
|
||||||
|
|
||||||
|
setTable({ data: { columns: [...existingColumns, newColumn] }, key: pageKey });
|
||||||
|
} else {
|
||||||
|
// If removing a column
|
||||||
|
const removed = existingColumns.filter((column) => !values.includes(column.column));
|
||||||
|
const newColumns = existingColumns.filter((column) => !removed.includes(column));
|
||||||
|
|
||||||
|
setTable({ data: { columns: newColumns }, key: pageKey });
|
||||||
|
}
|
||||||
|
|
||||||
|
return tableRef.current?.api.sizeColumnsToFit();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAutoFitColumns = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setTable({ data: { autoFit: e.currentTarget.checked }, key: pageKey });
|
||||||
|
|
||||||
|
if (e.currentTarget.checked) {
|
||||||
|
tableRef.current?.api.sizeColumnsToFit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRefresh = useCallback(() => {
|
||||||
|
queryClient.invalidateQueries(queryKeys.artists.list(server?.id || ''));
|
||||||
|
handleFilterChange(filter);
|
||||||
|
}, [filter, handleFilterChange, queryClient, server?.id]);
|
||||||
|
|
||||||
|
const handleSetRole = useCallback(
|
||||||
|
(e: string | null) => {
|
||||||
|
const updatedFilters = setFilter({
|
||||||
|
data: {
|
||||||
|
role: e || '',
|
||||||
|
},
|
||||||
|
itemType: LibraryItem.ARTIST,
|
||||||
|
key: pageKey,
|
||||||
|
}) as ArtistListFilter;
|
||||||
|
handleFilterChange(updatedFilters);
|
||||||
|
},
|
||||||
|
[handleFilterChange, pageKey, setFilter],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex justify="space-between">
|
||||||
|
<Group
|
||||||
|
ref={cq.ref}
|
||||||
|
spacing="sm"
|
||||||
|
w="100%"
|
||||||
|
>
|
||||||
|
<DropdownMenu position="bottom-start">
|
||||||
|
<DropdownMenu.Target>
|
||||||
|
<Button
|
||||||
|
compact
|
||||||
|
fw="600"
|
||||||
|
size="md"
|
||||||
|
variant="subtle"
|
||||||
|
>
|
||||||
|
{sortByLabel}
|
||||||
|
</Button>
|
||||||
|
</DropdownMenu.Target>
|
||||||
|
<DropdownMenu.Dropdown>
|
||||||
|
{FILTERS[server?.type as keyof typeof FILTERS].map((f) => (
|
||||||
|
<DropdownMenu.Item
|
||||||
|
key={`filter-${f.name}`}
|
||||||
|
$isActive={f.value === filter.sortBy}
|
||||||
|
value={f.value}
|
||||||
|
onClick={handleSetSortBy}
|
||||||
|
>
|
||||||
|
{f.name}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
))}
|
||||||
|
</DropdownMenu.Dropdown>
|
||||||
|
</DropdownMenu>
|
||||||
|
<Divider orientation="vertical" />
|
||||||
|
<OrderToggleButton
|
||||||
|
sortOrder={filter.sortOrder}
|
||||||
|
onToggle={handleToggleSortOrder}
|
||||||
|
/>
|
||||||
|
{server?.type === ServerType.JELLYFIN && (
|
||||||
|
<>
|
||||||
|
<Divider orientation="vertical" />
|
||||||
|
<DropdownMenu position="bottom-start">
|
||||||
|
<DropdownMenu.Target>
|
||||||
|
<Button
|
||||||
|
compact
|
||||||
|
fw="600"
|
||||||
|
size="md"
|
||||||
|
variant="subtle"
|
||||||
|
>
|
||||||
|
{cq.isMd ? 'Folder' : <RiFolder2Line size={15} />}
|
||||||
|
</Button>
|
||||||
|
</DropdownMenu.Target>
|
||||||
|
<DropdownMenu.Dropdown>
|
||||||
|
{musicFoldersQuery.data?.items.map((folder) => (
|
||||||
|
<DropdownMenu.Item
|
||||||
|
key={`musicFolder-${folder.id}`}
|
||||||
|
$isActive={filter.musicFolderId === folder.id}
|
||||||
|
value={folder.id}
|
||||||
|
onClick={handleSetMusicFolder}
|
||||||
|
>
|
||||||
|
{folder.name}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
))}
|
||||||
|
</DropdownMenu.Dropdown>
|
||||||
|
</DropdownMenu>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{roles.data?.length && (
|
||||||
|
<>
|
||||||
|
<Divider orientation="vertical" />
|
||||||
|
<Select
|
||||||
|
data={roles.data}
|
||||||
|
value={filter.role}
|
||||||
|
onChange={handleSetRole}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Divider orientation="vertical" />
|
||||||
|
<Button
|
||||||
|
compact
|
||||||
|
size="md"
|
||||||
|
tooltip={{ label: t('common.refresh', { postProcess: 'titleCase' }) }}
|
||||||
|
variant="subtle"
|
||||||
|
onClick={handleRefresh}
|
||||||
|
>
|
||||||
|
<RiRefreshLine size="1.3rem" />
|
||||||
|
</Button>
|
||||||
|
<Divider orientation="vertical" />
|
||||||
|
<DropdownMenu position="bottom-start">
|
||||||
|
<DropdownMenu.Target>
|
||||||
|
<Button
|
||||||
|
compact
|
||||||
|
size="md"
|
||||||
|
variant="subtle"
|
||||||
|
>
|
||||||
|
<RiMoreFill size={15} />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenu.Target>
|
||||||
|
<DropdownMenu.Dropdown>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
icon={<RiRefreshLine />}
|
||||||
|
onClick={handleRefresh}
|
||||||
|
>
|
||||||
|
{t('common.refresh', {
|
||||||
|
postProcess: 'titleCase',
|
||||||
|
})}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Dropdown>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Group>
|
||||||
|
<Group>
|
||||||
|
<DropdownMenu
|
||||||
|
position="bottom-end"
|
||||||
|
width={425}
|
||||||
|
>
|
||||||
|
<DropdownMenu.Target>
|
||||||
|
<Button
|
||||||
|
compact
|
||||||
|
size="md"
|
||||||
|
variant="subtle"
|
||||||
|
>
|
||||||
|
<RiSettings3Fill size="1.3rem" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenu.Target>
|
||||||
|
<DropdownMenu.Dropdown>
|
||||||
|
<DropdownMenu.Label>
|
||||||
|
{t('table.config.general.displayType', { postProcess: 'sentenceCase' })}
|
||||||
|
</DropdownMenu.Label>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
$isActive={display === ListDisplayType.CARD}
|
||||||
|
value={ListDisplayType.CARD}
|
||||||
|
onClick={handleSetViewType}
|
||||||
|
>
|
||||||
|
{t('table.config.view.card', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
})}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
$isActive={display === ListDisplayType.POSTER}
|
||||||
|
value={ListDisplayType.POSTER}
|
||||||
|
onClick={handleSetViewType}
|
||||||
|
>
|
||||||
|
{t('table.config.view.poster', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
})}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
$isActive={display === ListDisplayType.TABLE}
|
||||||
|
value={ListDisplayType.TABLE}
|
||||||
|
onClick={handleSetViewType}
|
||||||
|
>
|
||||||
|
{t('table.config.view.table', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
})}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Divider />
|
||||||
|
<DropdownMenu.Label>
|
||||||
|
{t('table.config.general.itemSize', { postProcess: 'sentenceCase' })}
|
||||||
|
</DropdownMenu.Label>
|
||||||
|
<DropdownMenu.Item closeMenuOnClick={false}>
|
||||||
|
{display === ListDisplayType.CARD ||
|
||||||
|
display === ListDisplayType.POSTER ? (
|
||||||
|
<Slider
|
||||||
|
defaultValue={grid?.itemSize}
|
||||||
|
max={300}
|
||||||
|
min={150}
|
||||||
|
onChange={debouncedHandleItemSize}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Slider
|
||||||
|
defaultValue={table.rowHeight}
|
||||||
|
max={100}
|
||||||
|
min={30}
|
||||||
|
onChange={debouncedHandleItemSize}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
{isGrid && (
|
||||||
|
<>
|
||||||
|
<DropdownMenu.Label>
|
||||||
|
{t('table.config.general.itemGap', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
})}
|
||||||
|
</DropdownMenu.Label>
|
||||||
|
<DropdownMenu.Item closeMenuOnClick={false}>
|
||||||
|
<Slider
|
||||||
|
defaultValue={grid?.itemGap || 0}
|
||||||
|
max={30}
|
||||||
|
min={0}
|
||||||
|
onChangeEnd={handleItemGap}
|
||||||
|
/>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!isGrid && (
|
||||||
|
<>
|
||||||
|
<DropdownMenu.Label>
|
||||||
|
{t('table.config.general.tableColumns', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
})}
|
||||||
|
</DropdownMenu.Label>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
closeMenuOnClick={false}
|
||||||
|
component="div"
|
||||||
|
sx={{ cursor: 'default' }}
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
<MultiSelect
|
||||||
|
clearable
|
||||||
|
data={ALBUMARTIST_TABLE_COLUMNS}
|
||||||
|
defaultValue={table?.columns.map(
|
||||||
|
(column) => column.column,
|
||||||
|
)}
|
||||||
|
width={300}
|
||||||
|
onChange={handleTableColumns}
|
||||||
|
/>
|
||||||
|
<Group position="apart">
|
||||||
|
<Text>
|
||||||
|
{t('table.config.general.autoFitColumns', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
<Switch
|
||||||
|
defaultChecked={table.autoFit}
|
||||||
|
onChange={handleAutoFitColumns}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DropdownMenu.Dropdown>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Group>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import type { ChangeEvent, MutableRefObject } from 'react';
|
||||||
|
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||||
|
import { Flex, Group, Stack } from '@mantine/core';
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { FilterBar } from '../../shared/components/filter-bar';
|
||||||
|
import { ArtistListQuery, LibraryItem } from '/@/renderer/api/types';
|
||||||
|
import { PageHeader, SearchInput } from '/@/renderer/components';
|
||||||
|
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||||
|
import { LibraryHeaderBar } from '/@/renderer/features/shared';
|
||||||
|
import { useContainerQuery } from '/@/renderer/hooks';
|
||||||
|
import { ArtistListFilter, useCurrentServer } from '/@/renderer/store';
|
||||||
|
import { useDisplayRefresh } from '/@/renderer/hooks/use-display-refresh';
|
||||||
|
import { ArtistListHeaderFilters } from '/@/renderer/features/artists/components/artist-list-header-filters';
|
||||||
|
|
||||||
|
interface ArtistListHeaderProps {
|
||||||
|
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
|
||||||
|
itemCount?: number;
|
||||||
|
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ArtistListHeader = ({ itemCount, gridRef, tableRef }: ArtistListHeaderProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const server = useCurrentServer();
|
||||||
|
const cq = useContainerQuery();
|
||||||
|
|
||||||
|
const { filter, refresh, search } = useDisplayRefresh<ArtistListQuery>({
|
||||||
|
gridRef,
|
||||||
|
itemCount,
|
||||||
|
itemType: LibraryItem.ARTIST,
|
||||||
|
server,
|
||||||
|
tableRef,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSearch = debounce((e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const updatedFilters = search(e) as ArtistListFilter;
|
||||||
|
refresh(updatedFilters);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
ref={cq.ref}
|
||||||
|
spacing={0}
|
||||||
|
>
|
||||||
|
<PageHeader backgroundColor="var(--titlebar-bg)">
|
||||||
|
<Flex
|
||||||
|
justify="space-between"
|
||||||
|
w="100%"
|
||||||
|
>
|
||||||
|
<LibraryHeaderBar>
|
||||||
|
<LibraryHeaderBar.Title>
|
||||||
|
{t('entity.artist_other', { postProcess: 'titleCase' })}
|
||||||
|
</LibraryHeaderBar.Title>
|
||||||
|
<LibraryHeaderBar.Badge
|
||||||
|
isLoading={itemCount === null || itemCount === undefined}
|
||||||
|
>
|
||||||
|
{itemCount}
|
||||||
|
</LibraryHeaderBar.Badge>
|
||||||
|
</LibraryHeaderBar>
|
||||||
|
<Group>
|
||||||
|
<SearchInput
|
||||||
|
defaultValue={filter.searchTerm}
|
||||||
|
openedWidth={cq.isMd ? 250 : cq.isSm ? 200 : 150}
|
||||||
|
onChange={handleSearch}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Flex>
|
||||||
|
</PageHeader>
|
||||||
|
<FilterBar>
|
||||||
|
<ArtistListHeaderFilters
|
||||||
|
gridRef={gridRef}
|
||||||
|
tableRef={tableRef}
|
||||||
|
/>
|
||||||
|
</FilterBar>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||||
|
import { MutableRefObject } from 'react';
|
||||||
|
import { useListContext } from '../../../context/list-context';
|
||||||
|
import { ARTIST_CONTEXT_MENU_ITEMS } from '../../context-menu/context-menu-items';
|
||||||
|
import { LibraryItem } from '/@/renderer/api/types';
|
||||||
|
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid';
|
||||||
|
import { VirtualTable } from '/@/renderer/components/virtual-table';
|
||||||
|
import { useVirtualTable } from '/@/renderer/components/virtual-table/hooks/use-virtual-table';
|
||||||
|
import { useCurrentServer } from '/@/renderer/store';
|
||||||
|
|
||||||
|
interface ArtistListTableViewProps {
|
||||||
|
itemCount?: number;
|
||||||
|
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ArtistListTableView = ({ itemCount, tableRef }: ArtistListTableViewProps) => {
|
||||||
|
const server = useCurrentServer();
|
||||||
|
const { pageKey } = useListContext();
|
||||||
|
|
||||||
|
const tableProps = useVirtualTable({
|
||||||
|
contextMenu: ARTIST_CONTEXT_MENU_ITEMS,
|
||||||
|
itemCount,
|
||||||
|
itemType: LibraryItem.ARTIST,
|
||||||
|
pageKey,
|
||||||
|
server,
|
||||||
|
tableRef,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VirtualGridAutoSizerContainer>
|
||||||
|
<VirtualTable
|
||||||
|
// https://github.com/ag-grid/ag-grid/issues/5284
|
||||||
|
// Key is used to force remount of table when display, rowHeight, or server changes
|
||||||
|
key={`table-${tableProps.rowHeight}-${server?.id}`}
|
||||||
|
ref={tableRef}
|
||||||
|
{...tableProps}
|
||||||
|
/>
|
||||||
|
</VirtualGridAutoSizerContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { api } from '/@/renderer/api';
|
||||||
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
|
import { ArtistListQuery } from '/@/renderer/api/types';
|
||||||
|
import { QueryHookArgs } from '/@/renderer/lib/react-query';
|
||||||
|
import { getServerById } from '/@/renderer/store';
|
||||||
|
|
||||||
|
export const useArtistListCount = (args: QueryHookArgs<ArtistListQuery>) => {
|
||||||
|
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.getArtistListCount({
|
||||||
|
apiClientProps: {
|
||||||
|
server,
|
||||||
|
signal,
|
||||||
|
},
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
queryKey: queryKeys.albumArtists.count(
|
||||||
|
serverId || '',
|
||||||
|
Object.keys(query).length === 0 ? undefined : query,
|
||||||
|
),
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { api } from '/@/renderer/api';
|
||||||
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
|
import { QueryHookArgs } from '/@/renderer/lib/react-query';
|
||||||
|
import { getServerById } from '/@/renderer/store';
|
||||||
|
|
||||||
|
export const useRoles = (args: QueryHookArgs<{}>) => {
|
||||||
|
const { options, serverId } = args;
|
||||||
|
const server = getServerById(serverId);
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
enabled: !!serverId,
|
||||||
|
queryFn: ({ signal }) => {
|
||||||
|
if (!server) throw new Error('Server not found');
|
||||||
|
return api.controller.getRoles({
|
||||||
|
apiClientProps: {
|
||||||
|
server,
|
||||||
|
signal,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
queryKey: queryKeys.roles.list(serverId || ''),
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -16,15 +16,21 @@ const AlbumArtistDetailRoute = () => {
|
|||||||
const headerRef = useRef<HTMLDivElement>(null);
|
const headerRef = useRef<HTMLDivElement>(null);
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
|
|
||||||
const { albumArtistId } = useParams() as { albumArtistId: string };
|
const { albumArtistId, artistId } = useParams() as {
|
||||||
|
albumArtistId?: string;
|
||||||
|
artistId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const routeId = (artistId || albumArtistId) as string;
|
||||||
|
|
||||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||||
const playButtonBehavior = usePlayButtonBehavior();
|
const playButtonBehavior = usePlayButtonBehavior();
|
||||||
const detailQuery = useAlbumArtistDetail({
|
const detailQuery = useAlbumArtistDetail({
|
||||||
query: { id: albumArtistId },
|
query: { id: routeId },
|
||||||
serverId: server?.id,
|
serverId: server?.id,
|
||||||
});
|
});
|
||||||
const { color: background, colorId } = useFastAverageColor({
|
const { color: background, colorId } = useFastAverageColor({
|
||||||
id: albumArtistId,
|
id: routeId,
|
||||||
src: detailQuery.data?.imageUrl,
|
src: detailQuery.data?.imageUrl,
|
||||||
srcLoaded: !detailQuery.isLoading,
|
srcLoaded: !detailQuery.isLoading,
|
||||||
});
|
});
|
||||||
@@ -32,19 +38,19 @@ const AlbumArtistDetailRoute = () => {
|
|||||||
const handlePlay = () => {
|
const handlePlay = () => {
|
||||||
handlePlayQueueAdd?.({
|
handlePlayQueueAdd?.({
|
||||||
byItemType: {
|
byItemType: {
|
||||||
id: [albumArtistId],
|
id: [routeId],
|
||||||
type: LibraryItem.ALBUM_ARTIST,
|
type: LibraryItem.ALBUM_ARTIST,
|
||||||
},
|
},
|
||||||
playType: playButtonBehavior,
|
playType: playButtonBehavior,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!background || colorId !== albumArtistId) {
|
if (!background || colorId !== routeId) {
|
||||||
return <Spinner container />;
|
return <Spinner container />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatedPage key={`album-artist-detail-${albumArtistId}`}>
|
<AnimatedPage key={`album-artist-detail-${routeId}`}>
|
||||||
<NativeScrollArea
|
<NativeScrollArea
|
||||||
ref={scrollAreaRef}
|
ref={scrollAreaRef}
|
||||||
pageHeaderProps={{
|
pageHeaderProps={{
|
||||||
|
|||||||
@@ -12,18 +12,22 @@ import { ListContext } from '/@/renderer/context/list-context';
|
|||||||
|
|
||||||
const AlbumArtistDetailTopSongsListRoute = () => {
|
const AlbumArtistDetailTopSongsListRoute = () => {
|
||||||
const tableRef = useRef<AgGridReactType | null>(null);
|
const tableRef = useRef<AgGridReactType | null>(null);
|
||||||
const { albumArtistId } = useParams() as { albumArtistId: string };
|
const { albumArtistId, artistId } = useParams() as {
|
||||||
|
albumArtistId?: string;
|
||||||
|
artistId?: string;
|
||||||
|
};
|
||||||
|
const routeId = (artistId || albumArtistId) as string;
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const pageKey = LibraryItem.SONG;
|
const pageKey = LibraryItem.SONG;
|
||||||
|
|
||||||
const detailQuery = useAlbumArtistDetail({
|
const detailQuery = useAlbumArtistDetail({
|
||||||
query: { id: albumArtistId },
|
query: { id: routeId },
|
||||||
serverId: server?.id,
|
serverId: server?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const topSongsQuery = useTopSongsList({
|
const topSongsQuery = useTopSongsList({
|
||||||
options: { enabled: !!detailQuery?.data?.name },
|
options: { enabled: !!detailQuery?.data?.name },
|
||||||
query: { artist: detailQuery?.data?.name || '', artistId: albumArtistId },
|
query: { artist: detailQuery?.data?.name || '', artistId: routeId },
|
||||||
serverId: server?.id,
|
serverId: server?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -31,10 +35,10 @@ const AlbumArtistDetailTopSongsListRoute = () => {
|
|||||||
|
|
||||||
const providerValue = useMemo(() => {
|
const providerValue = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
id: albumArtistId,
|
id: routeId,
|
||||||
pageKey,
|
pageKey,
|
||||||
};
|
};
|
||||||
}, [albumArtistId, pageKey]);
|
}, [routeId, pageKey]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatedPage>
|
<AnimatedPage>
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||||
|
import { useMemo, useRef } from 'react';
|
||||||
|
import { useCurrentServer } from '../../../store/auth.store';
|
||||||
|
import { useListFilterByKey } from '../../../store/list.store';
|
||||||
|
import { ArtistListQuery, LibraryItem } from '/@/renderer/api/types';
|
||||||
|
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||||
|
import { ListContext } from '/@/renderer/context/list-context';
|
||||||
|
import { AnimatedPage } from '/@/renderer/features/shared';
|
||||||
|
import { useArtistListCount } from '/@/renderer/features/artists/queries/artist-list-count-query';
|
||||||
|
import { ArtistListHeader } from '/@/renderer/features/artists/components/artist-list-header';
|
||||||
|
import { ArtistListContent } from '/@/renderer/features/artists/components/artist-list-content';
|
||||||
|
|
||||||
|
const ArtistListRoute = () => {
|
||||||
|
const gridRef = useRef<VirtualInfiniteGridRef | null>(null);
|
||||||
|
const tableRef = useRef<AgGridReactType | null>(null);
|
||||||
|
const pageKey = LibraryItem.ARTIST;
|
||||||
|
const server = useCurrentServer();
|
||||||
|
|
||||||
|
const artistListFilter = useListFilterByKey<ArtistListQuery>({ key: pageKey });
|
||||||
|
|
||||||
|
const itemCountCheck = useArtistListCount({
|
||||||
|
options: {
|
||||||
|
cacheTime: 1000 * 60,
|
||||||
|
staleTime: 1000 * 60,
|
||||||
|
},
|
||||||
|
query: artistListFilter,
|
||||||
|
serverId: server?.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const itemCount = itemCountCheck.data === null ? undefined : itemCountCheck.data;
|
||||||
|
|
||||||
|
const providerValue = useMemo(() => {
|
||||||
|
return {
|
||||||
|
id: undefined,
|
||||||
|
pageKey,
|
||||||
|
};
|
||||||
|
}, [pageKey]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatedPage>
|
||||||
|
<ListContext.Provider value={providerValue}>
|
||||||
|
<ArtistListHeader
|
||||||
|
gridRef={gridRef}
|
||||||
|
itemCount={itemCount}
|
||||||
|
tableRef={tableRef}
|
||||||
|
/>
|
||||||
|
<ArtistListContent
|
||||||
|
gridRef={gridRef}
|
||||||
|
itemCount={itemCount}
|
||||||
|
tableRef={tableRef}
|
||||||
|
/>
|
||||||
|
</ListContext.Provider>
|
||||||
|
</AnimatedPage>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ArtistListRoute;
|
||||||
@@ -97,6 +97,18 @@ const JELLYFIN_IGNORED_MENU_ITEMS: ContextMenuItemType[] = ['setRating', 'shareI
|
|||||||
|
|
||||||
const utils = isElectron() ? window.electron.utils : null;
|
const utils = isElectron() ? window.electron.utils : null;
|
||||||
|
|
||||||
|
function RatingIcon({ rating }: { rating: number }) {
|
||||||
|
return (
|
||||||
|
<Rating
|
||||||
|
readOnly
|
||||||
|
style={{
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}}
|
||||||
|
value={rating}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export interface ContextMenuProviderProps {
|
export interface ContextMenuProviderProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
@@ -835,62 +847,32 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: 'zeroStar',
|
id: 'zeroStar',
|
||||||
label: (
|
label: <RatingIcon rating={0} />,
|
||||||
<Rating
|
|
||||||
readOnly
|
|
||||||
value={0}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
onClick: () => handleUpdateRating(0),
|
onClick: () => handleUpdateRating(0),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'oneStar',
|
id: 'oneStar',
|
||||||
label: (
|
label: <RatingIcon rating={1} />,
|
||||||
<Rating
|
|
||||||
readOnly
|
|
||||||
value={1}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
onClick: () => handleUpdateRating(1),
|
onClick: () => handleUpdateRating(1),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'twoStar',
|
id: 'twoStar',
|
||||||
label: (
|
label: <RatingIcon rating={2} />,
|
||||||
<Rating
|
|
||||||
readOnly
|
|
||||||
value={2}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
onClick: () => handleUpdateRating(2),
|
onClick: () => handleUpdateRating(2),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'threeStar',
|
id: 'threeStar',
|
||||||
label: (
|
label: <RatingIcon rating={3} />,
|
||||||
<Rating
|
|
||||||
readOnly
|
|
||||||
value={3}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
onClick: () => handleUpdateRating(3),
|
onClick: () => handleUpdateRating(3),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'fourStar',
|
id: 'fourStar',
|
||||||
label: (
|
label: <RatingIcon rating={4} />,
|
||||||
<Rating
|
|
||||||
readOnly
|
|
||||||
value={4}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
onClick: () => handleUpdateRating(4),
|
onClick: () => handleUpdateRating(4),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'fiveStar',
|
id: 'fiveStar',
|
||||||
label: (
|
label: <RatingIcon rating={5} />,
|
||||||
<Rating
|
|
||||||
readOnly
|
|
||||||
value={5}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
onClick: () => handleUpdateRating(5),
|
onClick: () => handleUpdateRating(5),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
getAlbumArtistSongsById,
|
getAlbumArtistSongsById,
|
||||||
getSongsByQuery,
|
getSongsByQuery,
|
||||||
getGenreSongsById,
|
getGenreSongsById,
|
||||||
|
getArtistSongsById,
|
||||||
} from '/@/renderer/features/player/utils';
|
} from '/@/renderer/features/player/utils';
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -119,6 +120,13 @@ export const useHandlePlayQueueAdd = () => {
|
|||||||
queryClient,
|
queryClient,
|
||||||
server,
|
server,
|
||||||
});
|
});
|
||||||
|
} else if (itemType === LibraryItem.ARTIST) {
|
||||||
|
songList = await getArtistSongsById({
|
||||||
|
id,
|
||||||
|
query,
|
||||||
|
queryClient,
|
||||||
|
server,
|
||||||
|
});
|
||||||
} else if (itemType === LibraryItem.GENRE) {
|
} else if (itemType === LibraryItem.GENRE) {
|
||||||
songList = await getGenreSongsById({ id, query, queryClient, server });
|
songList = await getGenreSongsById({ id, query, queryClient, server });
|
||||||
} else if (itemType === LibraryItem.SONG) {
|
} else if (itemType === LibraryItem.SONG) {
|
||||||
@@ -162,7 +170,7 @@ export const useHandlePlayQueueAdd = () => {
|
|||||||
if (!songs || songs?.length === 0)
|
if (!songs || songs?.length === 0)
|
||||||
return toast.warn({
|
return toast.warn({
|
||||||
message: t('common.noResultsFromQuery', { postProcess: 'sentenceCase' }),
|
message: t('common.noResultsFromQuery', { postProcess: 'sentenceCase' }),
|
||||||
title: t('player.playbackFetchNoResults'),
|
title: t('player.playbackFetchNoResults', { postProcess: 'sentenceCase' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (initialIndex) {
|
if (initialIndex) {
|
||||||
|
|||||||
@@ -342,8 +342,12 @@ export const useScrobble = () => {
|
|||||||
// a single track on repeat one, or one track added to the queue
|
// a single track on repeat one, or one track added to the queue
|
||||||
// multiple times in a row and playback goes normally (no next/previous)
|
// multiple times in a row and playback goes normally (no next/previous)
|
||||||
equalityFn: (a, b) =>
|
equalityFn: (a, b) =>
|
||||||
|
// compute whether the song changed
|
||||||
(a[0] as QueueSong)?.uniqueId === (b[0] as QueueSong)?.uniqueId &&
|
(a[0] as QueueSong)?.uniqueId === (b[0] as QueueSong)?.uniqueId &&
|
||||||
a[2] === b[2],
|
// compute whether the position changed. This should imply 1
|
||||||
|
a[2] === b[2] &&
|
||||||
|
// compute whether the same player: relevant for repeat one and repeat all (one track)
|
||||||
|
a[3] === b[3],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ export const getAlbumArtistSongsById = async (args: {
|
|||||||
const { id, queryClient, server, query } = args;
|
const { id, queryClient, server, query } = args;
|
||||||
|
|
||||||
const queryFilter: SongListQuery = {
|
const queryFilter: SongListQuery = {
|
||||||
artistIds: id || [],
|
albumArtistIds: id || [],
|
||||||
sortBy: SongListSort.ALBUM_ARTIST,
|
sortBy: SongListSort.ALBUM_ARTIST,
|
||||||
sortOrder: SortOrder.ASC,
|
sortOrder: SortOrder.ASC,
|
||||||
startIndex: 0,
|
startIndex: 0,
|
||||||
@@ -174,6 +174,43 @@ export const getAlbumArtistSongsById = async (args: {
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getArtistSongsById = async (args: {
|
||||||
|
id: string[];
|
||||||
|
query?: Partial<SongListQuery>;
|
||||||
|
queryClient: QueryClient;
|
||||||
|
server: ServerListItem;
|
||||||
|
}) => {
|
||||||
|
const { id, queryClient, server, query } = args;
|
||||||
|
|
||||||
|
const queryFilter: SongListQuery = {
|
||||||
|
artistIds: id,
|
||||||
|
sortBy: SongListSort.ALBUM,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
|
startIndex: 0,
|
||||||
|
...query,
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryKey = queryKeys.songs.list(server?.id, queryFilter);
|
||||||
|
|
||||||
|
const res = await queryClient.fetchQuery(
|
||||||
|
queryKey,
|
||||||
|
async ({ signal }) =>
|
||||||
|
api.controller.getSongList({
|
||||||
|
apiClientProps: {
|
||||||
|
server,
|
||||||
|
signal,
|
||||||
|
},
|
||||||
|
query: queryFilter,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
cacheTime: 1000 * 60,
|
||||||
|
staleTime: 1000 * 60,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
export const getSongsByQuery = async (args: {
|
export const getSongsByQuery = async (args: {
|
||||||
query?: Partial<SongListQuery>;
|
query?: Partial<SongListQuery>;
|
||||||
queryClient: QueryClient;
|
queryClient: QueryClient;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { useCallback, useState, Fragment, useRef } from 'react';
|
import { useCallback, useState, Fragment, useRef } from 'react';
|
||||||
import { ActionIcon, Group, Kbd, ScrollArea } from '@mantine/core';
|
import { ActionIcon, Group, Kbd, ScrollArea } from '@mantine/core';
|
||||||
import { useDisclosure, useDebouncedValue } from '@mantine/hooks';
|
import { useDisclosure, useDebouncedValue } from '@mantine/hooks';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { RiSearchLine, RiCloseFill } from 'react-icons/ri';
|
import { RiSearchLine, RiCloseFill } from 'react-icons/ri';
|
||||||
import { generatePath, useNavigate } from 'react-router';
|
import { generatePath, useNavigate } from 'react-router';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
@@ -37,6 +38,7 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
|
|||||||
const activePage = pages[pages.length - 1];
|
const activePage = pages[pages.length - 1];
|
||||||
const isHome = activePage === CommandPalettePages.HOME;
|
const isHome = activePage === CommandPalettePages.HOME;
|
||||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const popPage = useCallback(() => {
|
const popPage = useCallback(() => {
|
||||||
setPages((pages) => {
|
setPages((pages) => {
|
||||||
@@ -187,13 +189,17 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LibraryCommandItem
|
<LibraryCommandItem
|
||||||
|
disabled={artist?.albumCount === 0}
|
||||||
handlePlayQueueAdd={handlePlayQueueAdd}
|
handlePlayQueueAdd={handlePlayQueueAdd}
|
||||||
id={artist.id}
|
id={artist.id}
|
||||||
imageUrl={artist.imageUrl}
|
imageUrl={artist.imageUrl}
|
||||||
itemType={LibraryItem.ALBUM_ARTIST}
|
itemType={LibraryItem.ALBUM_ARTIST}
|
||||||
subtitle={
|
subtitle={
|
||||||
(artist?.albumCount || 0) > 0
|
artist?.albumCount !== undefined &&
|
||||||
? `${artist.albumCount} albums`
|
artist?.albumCount !== null
|
||||||
|
? t('entity.albumWithCount', {
|
||||||
|
count: artist.albumCount,
|
||||||
|
})
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
title={artist.name}
|
title={artist.name}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ const StyledImage = styled.img`
|
|||||||
const ActionsContainer = styled(Flex)``;
|
const ActionsContainer = styled(Flex)``;
|
||||||
|
|
||||||
interface LibraryCommandItemProps {
|
interface LibraryCommandItemProps {
|
||||||
|
disabled?: boolean;
|
||||||
handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
|
handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
|
||||||
id: string;
|
id: string;
|
||||||
imageUrl: string | null;
|
imageUrl: string | null;
|
||||||
@@ -62,6 +63,7 @@ interface LibraryCommandItemProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const LibraryCommandItem = ({
|
export const LibraryCommandItem = ({
|
||||||
|
disabled,
|
||||||
id,
|
id,
|
||||||
imageUrl,
|
imageUrl,
|
||||||
subtitle,
|
subtitle,
|
||||||
@@ -154,6 +156,7 @@ export const LibraryCommandItem = ({
|
|||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
compact
|
compact
|
||||||
|
disabled={disabled}
|
||||||
size="md"
|
size="md"
|
||||||
tooltip={{
|
tooltip={{
|
||||||
label: t('player.play', { postProcess: 'sentenceCase' }),
|
label: t('player.play', { postProcess: 'sentenceCase' }),
|
||||||
@@ -166,6 +169,7 @@ export const LibraryCommandItem = ({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
compact
|
compact
|
||||||
|
disabled={disabled}
|
||||||
size="md"
|
size="md"
|
||||||
tooltip={{
|
tooltip={{
|
||||||
label: t('player.addLast', { postProcess: 'sentenceCase' }),
|
label: t('player.addLast', { postProcess: 'sentenceCase' }),
|
||||||
@@ -179,6 +183,7 @@ export const LibraryCommandItem = ({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
compact
|
compact
|
||||||
|
disabled={disabled}
|
||||||
size="md"
|
size="md"
|
||||||
tooltip={{
|
tooltip={{
|
||||||
label: t('player.addNext', { postProcess: 'sentenceCase' }),
|
label: t('player.addNext', { postProcess: 'sentenceCase' }),
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { MutableRefObject } from 'react';
|
|||||||
import { generatePath, useNavigate } from 'react-router';
|
import { generatePath, useNavigate } from 'react-router';
|
||||||
import { useParams, useSearchParams } from 'react-router-dom';
|
import { useParams, useSearchParams } from 'react-router-dom';
|
||||||
import { AppRoute } from '../../../router/routes';
|
import { AppRoute } from '../../../router/routes';
|
||||||
import { LibraryItem, QueueSong } from '/@/renderer/api/types';
|
import { LibraryItem, QueueSong, SongListQuery } from '/@/renderer/api/types';
|
||||||
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid';
|
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid';
|
||||||
import { VirtualTable } from '/@/renderer/components/virtual-table';
|
import { VirtualTable } from '/@/renderer/components/virtual-table';
|
||||||
import { useCurrentSongRowStyles } from '/@/renderer/components/virtual-table/hooks/use-current-song-row-styles';
|
import { useCurrentSongRowStyles } from '/@/renderer/components/virtual-table/hooks/use-current-song-row-styles';
|
||||||
@@ -80,7 +80,7 @@ export const SearchContent = ({ tableRef }: SearchContentProps) => {
|
|||||||
|
|
||||||
const { rowClassRules } = useCurrentSongRowStyles({ tableRef });
|
const { rowClassRules } = useCurrentSongRowStyles({ tableRef });
|
||||||
|
|
||||||
const tableProps = useVirtualTable({
|
const tableProps = useVirtualTable<SongListQuery>({
|
||||||
contextMenu: contextMenuItems(),
|
contextMenu: contextMenuItems(),
|
||||||
customFilters: filter,
|
customFilters: filter,
|
||||||
itemType,
|
itemType,
|
||||||
@@ -96,6 +96,7 @@ export const SearchContent = ({ tableRef }: SearchContentProps) => {
|
|||||||
key={`table-${itemType}-${tableProps.rowHeight}-${server?.id}`}
|
key={`table-${itemType}-${tableProps.rowHeight}-${server?.id}`}
|
||||||
ref={tableRef}
|
ref={tableRef}
|
||||||
context={{
|
context={{
|
||||||
|
itemType,
|
||||||
query: searchParams.get('query'),
|
query: searchParams.get('query'),
|
||||||
}}
|
}}
|
||||||
getRowId={(data) => data.data.id}
|
getRowId={(data) => data.data.id}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { useGeneralSettings, useSettingsStoreActions } from '/@/renderer/store';
|
|||||||
|
|
||||||
const SIDEBAR_ITEMS: Array<[string, string]> = [
|
const SIDEBAR_ITEMS: Array<[string, string]> = [
|
||||||
['Albums', 'page.sidebar.albums'],
|
['Albums', 'page.sidebar.albums'],
|
||||||
['Artists', 'page.sidebar.artists'],
|
['Artists', 'page.sidebar.albumArtists'],
|
||||||
['Folders', 'page.sidebar.folders'],
|
['Artists-all', 'page.sidebar.artists'],
|
||||||
['Genres', 'page.sidebar.genres'],
|
['Genres', 'page.sidebar.genres'],
|
||||||
['Home', 'page.sidebar.home'],
|
['Home', 'page.sidebar.home'],
|
||||||
['Now Playing', 'page.sidebar.nowPlaying'],
|
['Now Playing', 'page.sidebar.nowPlaying'],
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const MotionButton = styled(UnstyledButton)`
|
|||||||
fill: var(--btn-filled-fg);
|
fill: var(--btn-filled-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover:not([disabled]) {
|
||||||
background: var(--btn-filled-bg);
|
background: var(--btn-filled-bg);
|
||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
|
|
||||||
@@ -28,6 +28,10 @@ const MotionButton = styled(UnstyledButton)`
|
|||||||
transform: scale(0.95);
|
transform: scale(0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
transition: background-color 0.2s ease-in-out;
|
transition: background-color 0.2s ease-in-out;
|
||||||
transition: transform 0.2s ease-in-out;
|
transition: transform 0.2s ease-in-out;
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ const TextWrapper = styled.div`
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: pre-line;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ActiveTabIndicator = styled(motion.div)`
|
const ActiveTabIndicator = styled(motion.div)`
|
||||||
@@ -90,7 +90,6 @@ const _CollapsedSidebarItem = forwardRef<HTMLDivElement, CollapsedSidebarItemPro
|
|||||||
<Text
|
<Text
|
||||||
$secondary={!isMatch}
|
$secondary={!isMatch}
|
||||||
fw="600"
|
fw="600"
|
||||||
overflow="hidden"
|
|
||||||
size="xs"
|
size="xs"
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
|
|||||||
@@ -33,7 +33,11 @@ export const CollapsedSidebar = () => {
|
|||||||
const translatedSidebarItemMap = useMemo(
|
const translatedSidebarItemMap = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
Albums: t('page.sidebar.albums', { postProcess: 'titleCase' }),
|
Albums: t('page.sidebar.albums', { postProcess: 'titleCase' }),
|
||||||
Artists: t('page.sidebar.artists', { postProcess: 'titleCase' }),
|
Artists: t('page.sidebar.albumArtists', { postProcess: 'titleCase' }).replace(
|
||||||
|
' ',
|
||||||
|
'\n',
|
||||||
|
),
|
||||||
|
'Artists-all': t('page.sidebar.artists', { postProcess: 'titleCase' }),
|
||||||
Folders: t('page.sidebar.folders', { postProcess: 'titleCase' }),
|
Folders: t('page.sidebar.folders', { postProcess: 'titleCase' }),
|
||||||
Genres: t('page.sidebar.genres', { postProcess: 'titleCase' }),
|
Genres: t('page.sidebar.genres', { postProcess: 'titleCase' }),
|
||||||
Home: t('page.sidebar.home', { postProcess: 'titleCase' }),
|
Home: t('page.sidebar.home', { postProcess: 'titleCase' }),
|
||||||
|
|||||||
@@ -81,8 +81,8 @@ export const Sidebar = () => {
|
|||||||
const translatedSidebarItemMap = useMemo(
|
const translatedSidebarItemMap = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
Albums: t('page.sidebar.albums', { postProcess: 'titleCase' }),
|
Albums: t('page.sidebar.albums', { postProcess: 'titleCase' }),
|
||||||
Artists: t('page.sidebar.artists', { postProcess: 'titleCase' }),
|
Artists: t('page.sidebar.albumArtists', { postProcess: 'titleCase' }),
|
||||||
Folders: t('page.sidebar.folders', { postProcess: 'titleCase' }),
|
'Artists-all': t('page.sidebar.artists', { postProcess: 'titleCase' }),
|
||||||
Genres: t('page.sidebar.genres', { postProcess: 'titleCase' }),
|
Genres: t('page.sidebar.genres', { postProcess: 'titleCase' }),
|
||||||
Home: t('page.sidebar.home', { postProcess: 'titleCase' }),
|
Home: t('page.sidebar.home', { postProcess: 'titleCase' }),
|
||||||
'Now Playing': t('page.sidebar.nowPlaying', { postProcess: 'titleCase' }),
|
'Now Playing': t('page.sidebar.nowPlaying', { postProcess: 'titleCase' }),
|
||||||
|
|||||||
@@ -122,9 +122,12 @@ const TrackListRoute = () => {
|
|||||||
|
|
||||||
const artist = searchParams.get('artistName');
|
const artist = searchParams.get('artistName');
|
||||||
const title = artist
|
const title = artist
|
||||||
? t('page.trackList.artistTracks', { artist })
|
? t('page.trackList.artistTracks', { artist, postProcess: 'sentenceCase' })
|
||||||
: genreId
|
: genreId
|
||||||
? t('page.trackList.genreTracks', { genre: titleCase(genreTitle) })
|
? t('page.trackList.genreTracks', {
|
||||||
|
genre: titleCase(genreTitle),
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
})
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -27,44 +27,42 @@ export const useListFilterRefresh = ({
|
|||||||
|
|
||||||
const queryKeyFn: ((serverId: string, query: Record<any, any>) => QueryKey) | null =
|
const queryKeyFn: ((serverId: string, query: Record<any, any>) => QueryKey) | null =
|
||||||
useMemo(() => {
|
useMemo(() => {
|
||||||
if (itemType === LibraryItem.ALBUM) {
|
switch (itemType) {
|
||||||
return queryKeys.albums.list;
|
case LibraryItem.ALBUM:
|
||||||
|
return queryKeys.albums.list;
|
||||||
|
case LibraryItem.ALBUM_ARTIST:
|
||||||
|
return queryKeys.albumArtists.list;
|
||||||
|
case LibraryItem.ARTIST:
|
||||||
|
return queryKeys.artists.list;
|
||||||
|
case LibraryItem.GENRE:
|
||||||
|
return queryKeys.genres.list;
|
||||||
|
case LibraryItem.PLAYLIST:
|
||||||
|
return queryKeys.playlists.list;
|
||||||
|
case LibraryItem.SONG:
|
||||||
|
return queryKeys.songs.list;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
if (itemType === LibraryItem.ALBUM_ARTIST) {
|
|
||||||
return queryKeys.albumArtists.list;
|
|
||||||
}
|
|
||||||
if (itemType === LibraryItem.PLAYLIST) {
|
|
||||||
return queryKeys.playlists.list;
|
|
||||||
}
|
|
||||||
if (itemType === LibraryItem.SONG) {
|
|
||||||
return queryKeys.songs.list;
|
|
||||||
}
|
|
||||||
if (itemType === LibraryItem.GENRE) {
|
|
||||||
return queryKeys.genres.list;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}, [itemType]);
|
}, [itemType]);
|
||||||
|
|
||||||
const queryFn: ((args: any) => Promise<BasePaginatedResponse<any> | null | undefined>) | null =
|
const queryFn: ((args: any) => Promise<BasePaginatedResponse<any> | null | undefined>) | null =
|
||||||
useMemo(() => {
|
useMemo(() => {
|
||||||
if (itemType === LibraryItem.ALBUM) {
|
switch (itemType) {
|
||||||
return api.controller.getAlbumList;
|
case LibraryItem.ALBUM:
|
||||||
|
return api.controller.getAlbumList;
|
||||||
|
case LibraryItem.ALBUM_ARTIST:
|
||||||
|
return api.controller.getAlbumArtistList;
|
||||||
|
case LibraryItem.ARTIST:
|
||||||
|
return api.controller.getArtistList;
|
||||||
|
case LibraryItem.GENRE:
|
||||||
|
return api.controller.getGenreList;
|
||||||
|
case LibraryItem.PLAYLIST:
|
||||||
|
return api.controller.getPlaylistList;
|
||||||
|
case LibraryItem.SONG:
|
||||||
|
return api.controller.getSongList;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
if (itemType === LibraryItem.ALBUM_ARTIST) {
|
|
||||||
return api.controller.getAlbumArtistList;
|
|
||||||
}
|
|
||||||
if (itemType === LibraryItem.PLAYLIST) {
|
|
||||||
return api.controller.getPlaylistList;
|
|
||||||
}
|
|
||||||
if (itemType === LibraryItem.SONG) {
|
|
||||||
return api.controller.getSongList;
|
|
||||||
}
|
|
||||||
if (itemType === LibraryItem.GENRE) {
|
|
||||||
return api.controller.getGenreList;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}, [itemType]);
|
}, [itemType]);
|
||||||
|
|
||||||
const handleRefreshTable = useCallback(
|
const handleRefreshTable = useCallback(
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ root.render(
|
|||||||
<Notifications
|
<Notifications
|
||||||
containerWidth="300px"
|
containerWidth="300px"
|
||||||
position="bottom-center"
|
position="bottom-center"
|
||||||
|
zIndex={5}
|
||||||
/>
|
/>
|
||||||
<App />
|
<App />
|
||||||
</PersistQueryClientProvider>,
|
</PersistQueryClientProvider>,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { ModalsProvider } from '@mantine/modals';
|
|||||||
import { BaseContextModal } from '/@/renderer/components';
|
import { BaseContextModal } from '/@/renderer/components';
|
||||||
import { AddToPlaylistContextModal } from '/@/renderer/features/playlists';
|
import { AddToPlaylistContextModal } from '/@/renderer/features/playlists';
|
||||||
import { ShareItemContextModal } from '/@/renderer/features/sharing';
|
import { ShareItemContextModal } from '/@/renderer/features/sharing';
|
||||||
|
import ArtistListRoute from '/@/renderer/features/artists/routes/artist-list-route';
|
||||||
|
|
||||||
const NowPlayingRoute = lazy(
|
const NowPlayingRoute = lazy(
|
||||||
() => import('/@/renderer/features/now-playing/routes/now-playing-route'),
|
() => import('/@/renderer/features/now-playing/routes/now-playing-route'),
|
||||||
@@ -144,6 +145,29 @@ export const AppRouter = () => {
|
|||||||
errorElement={<RouteErrorBoundary />}
|
errorElement={<RouteErrorBoundary />}
|
||||||
path={AppRoute.LIBRARY_ALBUMS_DETAIL}
|
path={AppRoute.LIBRARY_ALBUMS_DETAIL}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
element={<ArtistListRoute />}
|
||||||
|
errorElement={<RouteErrorBoundary />}
|
||||||
|
path={AppRoute.LIBRARY_ARTISTS}
|
||||||
|
/>
|
||||||
|
<Route path={AppRoute.LIBRARY_ARTISTS_DETAIL}>
|
||||||
|
<Route
|
||||||
|
index
|
||||||
|
element={<AlbumArtistDetailRoute />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
element={<AlbumListRoute />}
|
||||||
|
path={AppRoute.LIBRARY_ARTISTS_DETAIL_DISCOGRAPHY}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
element={<SongListRoute />}
|
||||||
|
path={AppRoute.LIBRARY_ARTISTS_DETAIL_SONGS}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
element={<AlbumArtistDetailTopSongsListRoute />}
|
||||||
|
path={AppRoute.LIBRARY_ARTISTS_DETAIL_TOP_SONGS}
|
||||||
|
/>
|
||||||
|
</Route>
|
||||||
<Route
|
<Route
|
||||||
element={<DummyAlbumDetailRoute />}
|
element={<DummyAlbumDetailRoute />}
|
||||||
errorElement={<RouteErrorBoundary />}
|
errorElement={<RouteErrorBoundary />}
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ export enum AppRoute {
|
|||||||
LIBRARY_ALBUM_ARTISTS_DETAIL_TOP_SONGS = '/library/album-artists/:albumArtistId/top-songs',
|
LIBRARY_ALBUM_ARTISTS_DETAIL_TOP_SONGS = '/library/album-artists/:albumArtistId/top-songs',
|
||||||
LIBRARY_ARTISTS = '/library/artists',
|
LIBRARY_ARTISTS = '/library/artists',
|
||||||
LIBRARY_ARTISTS_DETAIL = '/library/artists/:artistId',
|
LIBRARY_ARTISTS_DETAIL = '/library/artists/:artistId',
|
||||||
|
LIBRARY_ARTISTS_DETAIL_DISCOGRAPHY = '/library/artists/:artistId/discography',
|
||||||
|
LIBRARY_ARTISTS_DETAIL_SONGS = '/library/artists/:artistId/songs',
|
||||||
|
LIBRARY_ARTISTS_DETAIL_TOP_SONGS = '/library/artists/:artistId/top-songs',
|
||||||
LIBRARY_FOLDERS = '/library/folders',
|
LIBRARY_FOLDERS = '/library/folders',
|
||||||
LIBRARY_GENRES = '/library/genres',
|
LIBRARY_GENRES = '/library/genres',
|
||||||
LIBRARY_GENRES_ALBUMS = '/library/genres/:genreId/albums',
|
LIBRARY_GENRES_ALBUMS = '/library/genres/:genreId/albums',
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
AlbumArtistListSort,
|
AlbumArtistListSort,
|
||||||
AlbumListArgs,
|
AlbumListArgs,
|
||||||
AlbumListSort,
|
AlbumListSort,
|
||||||
|
ArtistListArgs,
|
||||||
GenreListArgs,
|
GenreListArgs,
|
||||||
GenreListSort,
|
GenreListSort,
|
||||||
LibraryItem,
|
LibraryItem,
|
||||||
@@ -27,6 +28,7 @@ export const generatePageKey = (page: string, id?: string) => {
|
|||||||
export type AlbumListFilter = Omit<AlbumListArgs['query'], 'startIndex' | 'limit'>;
|
export type AlbumListFilter = Omit<AlbumListArgs['query'], 'startIndex' | 'limit'>;
|
||||||
export type SongListFilter = Omit<SongListArgs['query'], 'startIndex' | 'limit'>;
|
export type SongListFilter = Omit<SongListArgs['query'], 'startIndex' | 'limit'>;
|
||||||
export type AlbumArtistListFilter = Omit<AlbumArtistListArgs['query'], 'startIndex' | 'limit'>;
|
export type AlbumArtistListFilter = Omit<AlbumArtistListArgs['query'], 'startIndex' | 'limit'>;
|
||||||
|
export type ArtistListFilter = Omit<ArtistListArgs['query'], 'startIndex' | 'limit'>;
|
||||||
export type PlaylistListFilter = Omit<PlaylistListArgs['query'], 'startIndex' | 'limit'>;
|
export type PlaylistListFilter = Omit<PlaylistListArgs['query'], 'startIndex' | 'limit'>;
|
||||||
export type GenreListFilter = Omit<GenreListArgs['query'], 'startIndex' | 'limit'>;
|
export type GenreListFilter = Omit<GenreListArgs['query'], 'startIndex' | 'limit'>;
|
||||||
|
|
||||||
@@ -34,6 +36,7 @@ type FilterType =
|
|||||||
| AlbumListFilter
|
| AlbumListFilter
|
||||||
| SongListFilter
|
| SongListFilter
|
||||||
| AlbumArtistListFilter
|
| AlbumArtistListFilter
|
||||||
|
| ArtistListFilter
|
||||||
| PlaylistListFilter
|
| PlaylistListFilter
|
||||||
| GenreListFilter;
|
| GenreListFilter;
|
||||||
|
|
||||||
@@ -509,6 +512,36 @@ export const useListStore = create<ListSlice>()(
|
|||||||
scrollOffset: 0,
|
scrollOffset: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
artist: {
|
||||||
|
display: ListDisplayType.POSTER,
|
||||||
|
filter: {
|
||||||
|
role: '',
|
||||||
|
sortBy: AlbumArtistListSort.NAME,
|
||||||
|
sortOrder: SortOrder.DESC,
|
||||||
|
},
|
||||||
|
grid: { itemGap: 10, itemSize: 200, scrollOffset: 0 },
|
||||||
|
table: {
|
||||||
|
autoFit: true,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
column: TableColumn.ROW_INDEX,
|
||||||
|
width: 50,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
column: TableColumn.TITLE_COMBINED,
|
||||||
|
width: 500,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
pagination: {
|
||||||
|
currentPage: 1,
|
||||||
|
itemsPerPage: 100,
|
||||||
|
totalItems: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
},
|
||||||
|
rowHeight: 60,
|
||||||
|
scrollOffset: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
genre: {
|
genre: {
|
||||||
display: ListDisplayType.TABLE,
|
display: ListDisplayType.TABLE,
|
||||||
filter: {
|
filter: {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export type SidebarItemType = {
|
|||||||
route: AppRoute | string;
|
route: AppRoute | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sidebarItems = [
|
export const sidebarItems: SidebarItemType[] = [
|
||||||
{
|
{
|
||||||
disabled: true,
|
disabled: true,
|
||||||
id: 'Now Playing',
|
id: 'Now Playing',
|
||||||
@@ -64,21 +64,21 @@ export const sidebarItems = [
|
|||||||
{
|
{
|
||||||
disabled: false,
|
disabled: false,
|
||||||
id: 'Artists',
|
id: 'Artists',
|
||||||
label: i18n.t('page.sidebar.artists'),
|
label: i18n.t('page.sidebar.albumArtists'),
|
||||||
route: AppRoute.LIBRARY_ALBUM_ARTISTS,
|
route: AppRoute.LIBRARY_ALBUM_ARTISTS,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
disabled: false,
|
||||||
|
id: 'Artists-all',
|
||||||
|
label: i18n.t('page.sidebar.artists'),
|
||||||
|
route: AppRoute.LIBRARY_ARTISTS,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
disabled: false,
|
disabled: false,
|
||||||
id: 'Genres',
|
id: 'Genres',
|
||||||
label: i18n.t('page.sidebar.genres'),
|
label: i18n.t('page.sidebar.genres'),
|
||||||
route: AppRoute.LIBRARY_GENRES,
|
route: AppRoute.LIBRARY_GENRES,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
disabled: true,
|
|
||||||
id: 'Folders',
|
|
||||||
label: i18n.t('page.sidebar.folders'),
|
|
||||||
route: AppRoute.LIBRARY_FOLDERS,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
disabled: true,
|
disabled: true,
|
||||||
id: 'Playlists',
|
id: 'Playlists',
|
||||||
@@ -734,8 +734,24 @@ export const useSettingsStore = create<SettingsSlice>()(
|
|||||||
),
|
),
|
||||||
{
|
{
|
||||||
merge: mergeOverridingColumns,
|
merge: mergeOverridingColumns,
|
||||||
|
migrate(persistedState, version) {
|
||||||
|
if (version === 8) {
|
||||||
|
const state = persistedState as SettingsSlice;
|
||||||
|
state.general.sidebarItems = state.general.sidebarItems.filter(
|
||||||
|
(item) => item.id !== 'Folders',
|
||||||
|
);
|
||||||
|
state.general.sidebarItems.push({
|
||||||
|
disabled: false,
|
||||||
|
id: 'Artists-all',
|
||||||
|
label: i18n.t('page.sidebar.artists'),
|
||||||
|
route: AppRoute.LIBRARY_ARTISTS,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return persistedState;
|
||||||
|
},
|
||||||
name: 'store_settings',
|
name: 'store_settings',
|
||||||
version: 8,
|
version: 9,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,6 +10,10 @@
|
|||||||
transition: position 0.2s ease-in-out;
|
transition: position 0.2s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ag-header-window-bar {
|
||||||
|
top: 95px;
|
||||||
|
}
|
||||||
|
|
||||||
.ag-header {
|
.ag-header {
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user