Compare commits

..

30 Commits

Author SHA1 Message Date
jeffvli 3ad5447871 enable album play buttons if yt enabled 2026-02-07 14:44:24 -08:00
jeffvli 3a50dee7a2 revert settings migrations, lint 2026-02-07 13:40:05 -08:00
jeffvli c4ecfeedec add automatic country prioritization based on existing releases 2026-02-07 13:36:17 -08:00
jeffvli f43655ed5a refactor mbz country priority to be multiselect 2026-02-07 13:24:32 -08:00
jeffvli fddda70190 handle release group image for song normalization 2026-02-07 13:14:02 -08:00
jeffvli a5992943d0 fetch image by release group instead of release 2026-02-07 13:12:19 -08:00
jeffvli 56e2611992 serve electron renderer via express to allow yt iframe playback 2026-02-07 12:59:59 -08:00
jeffvli c8ae128ac4 handle imageUrl in drag preview and context menu 2026-02-07 02:33:31 -08:00
jeffvli ba56ab8844 redesign external song indicator on itemcard 2026-02-07 02:30:41 -08:00
jeffvli 7cb7dfb62b add external song indicator for queue 2026-02-07 02:14:24 -08:00
jeffvli 86537a8d1e increase cache time for youtube queries 2026-02-07 01:53:19 -08:00
jeffvli 3b3e77b672 handle external imageUrl 2026-02-07 01:52:58 -08:00
jeffvli bec6464a44 move external playback fetch to context 2026-02-07 01:26:04 -08:00
jeffvli 812ca5302a fix audio context breaking on source change 2026-02-07 01:01:59 -08:00
jeffvli 1824083b99 adjust yt search query format 2026-02-07 00:41:55 -08:00
jeffvli f46ca8cd35 handle playback from ItemCard 2026-02-07 00:41:36 -08:00
jeffvli f04ea3bca0 fix artist name joining from mbz 2026-02-06 22:29:16 -08:00
jeffvli a547be1577 add settings configuration for integrations 2026-02-06 22:19:42 -08:00
jeffvli 8ae29407ec support ytmusic controls on web/mpv players 2026-02-06 21:38:05 -08:00
jeffvli 8e603871b7 add experimental ytmusic playback for external songs 2026-02-06 20:47:27 -08:00
jeffvli 40ec16e191 support mbz album detail view 2026-02-06 20:13:58 -08:00
jeffvli 0bb30ab0da decouple internal and external album count in releasetype sections 2026-02-06 14:47:02 -08:00
jeffvli 9919ff9626 improve card styling on external items 2026-02-06 14:40:15 -08:00
jeffvli f6cec17710 progress 2026-02-06 13:02:44 -08:00
jeffvli 03b01472f8 remove duplicate ServerType enum 2026-02-06 13:02:44 -08:00
jeffvli 3550177f67 ignore external albums in album section playback handler 2026-02-06 05:23:33 -08:00
jeffvli 82914c27f0 add missing releases from musicbrainz to artist page 2026-02-06 05:23:33 -08:00
jeffvli 10d02087d0 add selector to convert musicbrainz releases to Album type 2026-02-06 05:23:33 -08:00
jeffvli 4b509951a5 add musicbrainz artist query 2026-02-06 05:23:33 -08:00
jeffvli 2869aab728 add musicbrainz-api package 2026-02-06 05:23:33 -08:00
247 changed files with 3879 additions and 9687 deletions
+2 -1
View File
@@ -16,5 +16,6 @@ jobs:
- uses: vedantmgoyal9/winget-releaser@main
with:
identifier: jeffvli.Feishin
installers-regex: 'Feishin-*-win-(x64|arm64)\.exe'
installers-regex: 'Feishin-*-win-x64\.exe'
token: ${{ secrets.WINGET_ACC_TOKEN }}
Binary file not shown.
+3 -9
View File
@@ -13,15 +13,9 @@ asarUnpack:
- resources/**
win:
target:
- target: zip
arch:
- x64
- arm64
- target: nsis
arch:
- x64
- arm64
icon: assets/icons/icon.ico
- zip
- nsis
icon: assets/icons/icon.png
nsis:
allowToChangeInstallationDirectory: true
+3 -9
View File
@@ -13,15 +13,9 @@ asarUnpack:
- resources/**
win:
target:
- target: zip
arch:
- x64
- arm64
- target: nsis
arch:
- x64
- arm64
icon: assets/icons/icon.ico
- zip
- nsis
icon: assets/icons/icon.png
nsis:
allowToChangeInstallationDirectory: true
+2 -8
View File
@@ -13,14 +13,8 @@ asarUnpack:
- resources/**
win:
target:
- target: zip
arch:
- x64
- arm64
- target: nsis
arch:
- x64
- arm64
- zip
- nsis
icon: assets/icons/icon.ico
nsis:
+4 -5
View File
@@ -1,6 +1,6 @@
{
"name": "feishin",
"version": "1.5.0",
"version": "1.4.2",
"description": "A modern self-hosted music player.",
"keywords": [
"subsonic",
@@ -44,7 +44,6 @@
"package:mac": "pnpm run build && electron-builder --mac",
"package:mac:pr": "pnpm run build && electron-builder --mac --publish never",
"package:win": "pnpm run build && electron-builder --win",
"package:win-arm64:pr": "pnpm run build && electron-builder --win --arm64 --publish never",
"package:win:pr": "pnpm run build && electron-builder --win --publish never",
"publish:linux": "pnpm run build && electron-builder --publish always --linux",
"publish:linux-arm64": "pnpm run build && electron-builder --publish always --linux --arm64",
@@ -56,9 +55,6 @@
"publish:mac:alpha": "pnpm run build && electron-builder --config electron-builder-alpha.yml --publish always --mac",
"publish:mac:beta": "pnpm run build && electron-builder --config electron-builder-beta.yml --publish always --mac",
"publish:win": "pnpm run build && electron-builder --publish always --win",
"publish:win-arm64": "pnpm run build && electron-builder --publish always --win --arm64",
"publish:win-arm64:alpha": "pnpm run build && electron-builder --config electron-builder-alpha.yml --publish always --win --arm64",
"publish:win-arm64:beta": "pnpm run build && electron-builder --config electron-builder-beta.yml --publish always --win --arm64",
"publish:win:alpha": "pnpm run build && electron-builder --config electron-builder-alpha.yml --publish always --win",
"publish:win:beta": "pnpm run build && electron-builder --config electron-builder-beta.yml --publish always --win",
"start": "electron-vite preview",
@@ -102,6 +98,7 @@
"electron-log": "^5.4.3",
"electron-store": "^8.2.0",
"electron-updater": "^6.6.2",
"express": "^5.2.1",
"fast-average-color": "^9.5.0",
"fast-xml-parser": "^5.3.2",
"format-duration": "^3.0.2",
@@ -115,6 +112,7 @@
"md5": "^2.3.0",
"motion": "^12.23.24",
"mpris-service": "^2.1.2",
"musicbrainz-api": "^0.27.1",
"nanoid": "^3.3.11",
"node-mpv": "github:jeffvli/Node-MPV#32b4d64395289ad710c41d481d2707a7acfc228f",
"nuqs": "^2.7.1",
@@ -138,6 +136,7 @@
"string-to-color": "^2.2.2",
"wavesurfer.js": "^7.11.1",
"ws": "^8.18.2",
"ytmusic-api": "^5.3.0",
"zod": "^3.22.3",
"zustand": "^5.0.5"
},
+652 -3
View File
File diff suppressed because it is too large Load Diff
+12 -56
View File
@@ -15,8 +15,7 @@
"nowPlaying": "ara sona",
"shared": "$t(entity.playlist, {\"count\": 2}) compartides",
"favorites": "$t(entity.favorite, {\"count\": 2})",
"radio": "$t(entity.radioStation, {\"count\": 2})",
"collections": "col·leccions"
"radio": "$t(entity.radioStation, {\"count\": 2})"
},
"albumArtistDetail": {
"relatedArtists": "$t(entity.artist, {\"count\": 2}) similars",
@@ -29,11 +28,7 @@
"topSongsFrom": "les millors cançons de {{title}}",
"viewAll": "mostra-ho tot",
"groupingTypeAll": "tots els tipus de llançaments",
"groupingTypePrimary": "tipus principals de llançament",
"favoriteSongs": "Cançons preferides",
"topSongsCommunity": "comunitat",
"topSongsPersonal": "personal",
"favoriteSongsFrom": "cançons preferides de {{title}}"
"groupingTypePrimary": "tipus principals de llançament"
},
"albumArtistList": {
"title": "$t(entity.albumArtist, {\"count\": 2})"
@@ -196,19 +191,6 @@
},
"radioList": {
"title": "emissores de ràdio"
},
"windowBar": {
"paused": "(en pausa) ",
"privateMode": "(mode privat)"
},
"collections": {
"overrideExisting": "sobreescriu existents",
"saveAsCollection": "desa com a col·lecció"
},
"releasenotes": {
"commitsSinceStable": "commits des de {{stable}}",
"noNewCommits": "no hi ha hagut commits en aquest període",
"noStableReleaseToCompare": "no hi ha actualitzacions disponibles amb les quals comparar"
}
},
"common": {
@@ -307,8 +289,8 @@
"restartRequired": "cal reiniciar",
"sampleRate": "freqüència de mostreig",
"setting_one": "configuració",
"setting_many": "configuracions",
"setting_other": "configuracions",
"setting_many": "",
"setting_other": "",
"trackGain": "guany de pista",
"trackPeak": "pic de pista",
"gap": "espera",
@@ -334,8 +316,7 @@
"example": "exemple",
"mood": "estat d'ànim",
"filter_single": "senzill",
"filter_multiple": "multi",
"rename": "reanomena"
"filter_multiple": "multi"
},
"entity": {
"album_one": "àlbum",
@@ -559,6 +540,7 @@
"fontType_optionBuiltIn": "tipus de lletra integrats",
"fontType_optionCustom": "tipus de lletra personalitzats",
"fontType_optionSystem": "tipus de lletra del sistema",
"disableAutomaticUpdates": "desactivar les actualitzacions automàtiques",
"disableLibraryUpdateOnStartup": "desactiva la comprovació de noves versions a l'inici",
"homeConfiguration": "configuració de la pàgina d'inici",
"sidebarConfiguration": "configuració de la barra lateral",
@@ -788,7 +770,7 @@
"releaseChannel_optionLatest": "última versió",
"releaseChannel_optionBeta": "beta",
"releaseChannel": "canal de versions",
"releaseChannel_description": "trieu entre versions estables i beta o alfa (diàries) per les actualitzacions automàtiques",
"releaseChannel_description": "tria entre versions estables i versions beta per les actualitzacions automàtiques",
"mediaSession": "activa Media Session",
"mediaSession_description": "activa la integració amb Media Session per mostrar els controls multimèdia i les metadades a l'indicador de volum del sistema i la pantalla de bloqueig",
"crossfadeStyle": "estil de fosa encadenada",
@@ -880,21 +862,7 @@
"homeFeatureStyle_description": "controla l'estil del carrusel de destacats de l'inici",
"homeFeatureStyle": "estil del carrusel de destacats de l'inici",
"homeFeatureStyle_optionMultiple": "múltiple",
"homeFeatureStyle_optionSingle": "simple",
"enableGridMultiSelect": "activa la selecció múltiple de quadrícula",
"enableGridMultiSelect_description": "quan està activada, podeu seleccionar més d'un element en la vista de quadrícula; si feu clic en la imatge d'un element de la quadrícula, accedireu a la pàgina de l'element",
"sidebarPlaylistSorting_description": "permet ordenar manualment les llistes de reproducció a la barra lateral arrossegant amb el ratolí en comptes de seguir l'ordre predeterminat del servidor",
"sidebarPlaylistSorting": "ordenació de llistes de reproducció de la barra lateral",
"sidebarPlaylistListFilterRegex_description": "amaga les llistes de reproducció de la barra lateral que coincideixin amb aquesta expressió regular",
"sidebarPlaylistListFilterRegex_placeholder": "ex. ^Mescla diària.*",
"sidebarPlaylistListFilterRegex": "regex pel filtre de llistes",
"analyticsEnable": "envia analítiques basades en l'ús",
"analyticsEnable_description": "s'envien dades d'ús anonimitzades al desenvolupar per ajudar a millorar l'aplicació",
"automaticUpdates": "actualitzacions automàtiques",
"automaticUpdates_description": "cerca i instal·la actualitzacions automàticament",
"releaseChannel_optionAlpha": "alfa (diària)",
"blurExplicitImages": "desenfoca imatges explícites",
"blurExplicitImages_description": "les caràtules d'àlbums i cançons marcades com a explícites quedaran desenfocades"
"homeFeatureStyle_optionSingle": "simple"
},
"table": {
"column": {
@@ -956,8 +924,7 @@
"alternateRowColors": "colors de fila alternants",
"horizontalBorders": "vores de fila",
"rowHoverHighlight": "ressalta en passar el cursor per la fila",
"verticalBorders": "vores de columna",
"showHeader": "mostra l'encapçalament"
"verticalBorders": "vores de columna"
},
"label": {
"actions": "$t(common.action, {\"count\": 2})",
@@ -999,8 +966,7 @@
"view": {
"table": "taula",
"grid": "quadrícula",
"list": "llista",
"detail": "detall"
"list": "llista"
}
}
},
@@ -1047,8 +1013,7 @@
"lastPlayed": "última reproducció",
"path": "ruta",
"songCount": "nombre de cançons",
"explicitStatus": "$t(common.explicitStatus)",
"sortName": "ordena per nom"
"explicitStatus": "$t(common.explicitStatus)"
},
"player": {
"muted": "silenciat",
@@ -1089,16 +1054,7 @@
"restoreQueueFromServer": "restaura la cua del servidor",
"saveQueueToServer": "desa la cua al servidor",
"artistRadio": "ràdio de l'artista",
"trackRadio": "ràdio de la pista",
"sleepTimer": "temporitzador d'adormir",
"sleepTimer_endOfSong": "final de la cançó actual",
"sleepTimer_minutes": "{{count}} min",
"sleepTimer_hours": "{{count}} h",
"sleepTimer_custom": "personalitzat",
"sleepTimer_off": "apagat",
"sleepTimer_timeRemaining": "queden {{time}}",
"sleepTimer_setCustom": "configura el temporitzador",
"sleepTimer_cancel": "cancel·la el temporitzador"
"trackRadio": "ràdio de la pista"
},
"error": {
"credentialsRequired": "credencials requerides",
+8 -38
View File
@@ -38,16 +38,7 @@
"restoreQueueFromServer": "obnovit frontu ze serveru",
"saveQueueToServer": "uložit frontu na server",
"artistRadio": "rádio umělce",
"trackRadio": "rádio skladby",
"sleepTimer": "časovač spánku",
"sleepTimer_endOfSong": "konec aktuální skladby",
"sleepTimer_minutes": "{{count}} min.",
"sleepTimer_hours": "{{count}} hod.",
"sleepTimer_custom": "vlastní",
"sleepTimer_off": "vypnuto",
"sleepTimer_timeRemaining": "zbývá {{time}}",
"sleepTimer_setCustom": "nastavit časovač",
"sleepTimer_cancel": "zrušit časovač"
"trackRadio": "rádio skladby"
},
"setting": {
"crossfadeStyle_description": "vyberte způsob prolnutí u přehrávače zvuku",
@@ -55,7 +46,7 @@
"hotkey_skipBackward": "přeskočení zpět",
"replayGainMode_description": "úprava zesílení hlasitosti podle hodnot {{ReplayGain}} uložených v metadatech souborů",
"volumeWheelStep_description": "počet procent, o které má být hlasitost posunuta při přejetí kolečkem myši na posuvníku hlasitosti",
"audioDevice_description": "vyberte zvukové zařízení k přehrávání",
"audioDevice_description": "vyberte zvukové zařízení k přehrávání (pouze webový přehrávač)",
"theme_description": "nastavení motivu použitého v aplikaci",
"hotkey_playbackPause": "pozastavení",
"replayGainFallback": "fallback {{ReplayGain}}",
@@ -107,6 +98,7 @@
"hotkey_globalSearch": "globální vyhledávání",
"gaplessAudio_description": "nastavení přehrávače mpv pro přehrávání bez mezer",
"remoteUsername_description": "nastavení uživatelského jména pro server vzdáleného ovládání. pokud je jméno i heslo prázdné, bude autentifikace zakázána",
"disableAutomaticUpdates": "vypnout automatické aktualizace",
"exitToTray_description": "ukončit aplikaci do systémové lišty",
"followLyric_description": "přesouvat texty s aktuální pozicí přehrávání",
"hotkey_favoritePreviousSong": "oblíbit $t(common.previousSong)",
@@ -270,7 +262,7 @@
"neteaseTranslation_description": "Pokud je povoleno, načte a zobrazí přeložené texty ze služby NetEase, pokud jsou dostupné",
"preferLocalLyrics": "preferovat místní texty",
"preferLocalLyrics_description": "preferovat místní texty před vzdálenými, pokud jsou dostupné",
"discordPausedStatus": "zobrazit stav při pozastavení",
"discordPausedStatus": "zobrazit rich presence při pozastavení",
"discordPausedStatus_description": "pokud je povoleno, bude při pozastavení přehrávače zobrazen stav",
"preservePitch": "zachovat výšku",
"preservePitch_description": "zachová výšku při úpravě rychlosti přehrávání",
@@ -292,7 +284,7 @@
"releaseChannel_optionLatest": "nejnovější",
"releaseChannel_optionBeta": "beta",
"releaseChannel": "kanál vydání",
"releaseChannel_description": "vyberte si mezi stabilními, beta nebo alpha (nočními) vydáními pro automatické aktualizace",
"releaseChannel_description": "vyberte si mezi stabilními vydáními nebo beta vydáními pro automatické aktualizace",
"mediaSession": "povolit relaci médií",
"mediaSession_description": "povolí integraci do služby Media Session, což zobrazí ovládání a metadata médií v překrytí systémové hlasitosti a na zamykací obrazovce",
"exportImportSettings_control_description": "exportovat a importovat nastavení pomocí souboru JSON",
@@ -388,19 +380,7 @@
"enableGridMultiSelect": "povolit vícenásobný výběr v mřížce",
"enableGridMultiSelect_description": "pokud je povoleno, umožňuje vybrat několik položek v zobrazení mřížky. pokud je zakázáno, kliknutím na obrázek položky mřížky přejdete na stránku položky",
"sidebarPlaylistSorting_description": "umožňuje ruční řazení seznamů skladeb v postranní liště pomocí přetažení namísto výchozího pořadí serveru",
"sidebarPlaylistSorting": "řazení seznamů skladeb v postranní liště",
"blurExplicitImages": "rozostřit explicitní obrázky",
"blurExplicitImages_description": "obaly alb a skladeb označené jako explicitní budou rozostřeny",
"sidebarPlaylistListFilterRegex_description": "v postranní liště skrýt seznamy skladeb, které odpovídají tomuto regulárnímu výrazu",
"sidebarPlaylistListFilterRegex_placeholder": "např. ^Denní mix.*",
"sidebarPlaylistListFilterRegex": "regulární výraz filtru seznamů skladeb",
"releaseChannel_optionAlpha": "alpha (noční)",
"analyticsEnable": "Posílat analytiku založenou na využití",
"analyticsEnable_description": "Anonymizovaná data o používání jsou odesílána vývojáři za účelem zlepšení aplikace",
"automaticUpdates": "Automatické aktualizace",
"automaticUpdates_description": "Kontrolovat a automaticky instalovat aktualizace",
"discordStateIcon": "zobrazit ikonu přehrávání",
"discordStateIcon_description": "zobrazit malou ikonu přehrávání ve stavu na Discordu. ikona pozastavení bude zobrazena vždy, když je povolena možnost „Zobrazit stav při pozastavení“"
"sidebarPlaylistSorting": "řazení seznamů skladeb v postranní liště"
},
"action": {
"editPlaylist": "upravit $t(entity.playlist, {\"count\": 1})",
@@ -573,8 +553,7 @@
"view": {
"table": "tabulka",
"list": "seznam",
"grid": "mřížka",
"detail": "podrobnosti"
"grid": "mřížka"
},
"general": {
"displayType": "typ zobrazení",
@@ -920,11 +899,7 @@
"viewAllTracks": "zobrazit všechny $t(entity.track, {\"count\": 2})",
"viewAll": "zobrazit vše",
"groupingTypeAll": "všechny typy vydání",
"groupingTypePrimary": "primární typy vydání",
"favoriteSongs": "oblíbené skladby",
"topSongsCommunity": "komunita",
"topSongsPersonal": "osobní",
"favoriteSongsFrom": "oblíbené skladby od umělce {{title}}"
"groupingTypePrimary": "primární typy vydání"
},
"itemDetail": {
"copiedPath": "cesta úspěšně zkopírována",
@@ -958,11 +933,6 @@
"collections": {
"overrideExisting": "nahradit existující",
"saveAsCollection": "uložit jako sbírku"
},
"releasenotes": {
"commitsSinceStable": "revize od {{stable}}",
"noNewCommits": "žádné nové revize v tomto období",
"noStableReleaseToCompare": "není dostupné žádné stabilní vydání k porovnání"
}
},
"form": {
+2 -1299
View File
File diff suppressed because it is too large Load Diff
+6 -22
View File
@@ -33,8 +33,7 @@
"createRadioStation": "$t(entity.radioStation, {\"count\": 1}) erstellen",
"deleteRadioStation": "$t(entity.radioStation, {\"count\": 1}) löschen",
"selectAll": "alle auswählen",
"openApplicationDirectory": "Anwendungsverzeichnis öffnen",
"addOrRemoveFromSelection": "Zur Auswahl hinzufügen oder entfernen"
"openApplicationDirectory": "Anwendungsverzeichnis öffnen"
},
"common": {
"backward": "zurück",
@@ -572,8 +571,7 @@
"shared": "$t(entity.playlist, {\"count\": 2}) geteilt",
"myLibrary": "meine bibliothek",
"favorites": "$t(entity.favorite, {\"count\": 2})",
"radio": "$t(entity.radioStation, {\"count\": 2})",
"collections": "Sammlungen"
"radio": "$t(entity.radioStation, {\"count\": 2})"
},
"setting": {
"playbackTab": "Wiedergabe",
@@ -631,9 +629,7 @@
"topSongs": "Toplieder",
"relatedArtists": "ähnliche $t(entity.artist, {\"count\": 2})",
"groupingTypeAll": "alle Veröffentlichungsformate",
"groupingTypePrimary": "primäre Veröffentlichungsformate",
"favoriteSongs": "Lieblingssongs",
"favoriteSongsFrom": "Liebslingssongs von {{title}}"
"groupingTypePrimary": "primäre Veröffentlichungsformate"
},
"manageServers": {
"title": "Servers verwalten",
@@ -659,13 +655,6 @@
},
"radioList": {
"title": "Radiosender"
},
"windowBar": {
"paused": "(Pausiert) ",
"privateMode": "(Privater Modus)"
},
"collections": {
"saveAsCollection": "Als Sammlung speichern"
}
},
"player": {
@@ -704,8 +693,7 @@
"addNextShuffled": "als Nächstes (zufällige Wiedergabe)",
"holdToShuffle": "Halten für Zufallswiedergabe",
"restoreQueueFromServer": "Wiedergabeliste von Server wiederherstellen",
"saveQueueToServer": "Wiedergabeliste auf Server speichern",
"lyrics": "Songtexte"
"saveQueueToServer": "Wiedergabeliste auf Server speichern"
},
"setting": {
"audioDevice_description": "wählen Sie das Audiogerät aus, das für die Wiedergabe verwendet werden soll (nur Webplayer)",
@@ -721,6 +709,7 @@
"disableLibraryUpdateOnStartup": "beim Start nicht nach neuen Versionen suchen",
"discordApplicationId_description": "die Application-ID für {{discord}} Rich Presence (Standard: {{defaultId}})",
"audioPlayer_description": "Wählen Sie den Audioplayer aus, der für die Wiedergabe verwendet werden soll",
"disableAutomaticUpdates": "Automatische Updates deaktivieren",
"crossfadeDuration_description": "Legt die Dauer der Überblendung fest",
"customFontPath": "Benutzerdefinierter Pfad für Schriftarten",
"crossfadeDuration": "Dauer der Überblendung",
@@ -985,12 +974,7 @@
"translationTargetLanguage_description": "die gewünschte Sprache der Übersetzung",
"translationTargetLanguage": "Zielsprache der Übersetzung",
"queryBuilderCustomFields": "benutzerdefiniertes Feld",
"queryBuilderCustomFields_inputTag": "Tag",
"homeFeatureStyle_optionMultiple": "mehrere",
"imageResolution": "Bildauflösung",
"imageResolution_optionTable": "Tabelle",
"imageResolution_optionSidebar": "Seitenleiste",
"preservePitch": "Tonhöhe erhalten"
"queryBuilderCustomFields_inputTag": "Tag"
},
"dragDropZone": {
"error_oneFileOnly": "Bitte wähle nur 1 Datei",
Executable → Regular
+16 -20
View File
@@ -79,6 +79,7 @@
"dismiss": "dismiss",
"doNotShowAgain": "do not show this again",
"duration": "duration",
"external": "external",
"view": "view",
"edit": "edit",
"enable": "enable",
@@ -152,6 +153,7 @@
"trackPeak": "track peak",
"translation": "translation",
"unknown": "unknown",
"unavailable": "unavailable",
"version": "version",
"year": "year",
"yes": "yes",
@@ -236,8 +238,6 @@
"filter": {
"album": "$t(entity.album, {\"count\": 1})",
"albumArtist": "$t(entity.albumArtist, {\"count\": 1})",
"matchAnd": "and",
"matchOr": "or",
"albumCount": "$t(entity.album, {\"count\": 2}) count",
"artist": "$t(entity.artist, {\"count\": 1})",
"biography": "biography",
@@ -584,6 +584,7 @@
"analytics": "analytics",
"generalTab": "general",
"hotkeysTab": "hotkeys",
"integrationsTab": "integrations",
"playbackTab": "playback",
"windowTab": "window",
"updates": "update",
@@ -669,16 +670,7 @@
"trackRadio": "track radio",
"unfavorite": "unfavorite",
"pause": "pause",
"viewQueue": "view queue",
"sleepTimer": "sleep timer",
"sleepTimer_endOfSong": "end of current song",
"sleepTimer_minutes": "{{count}} min",
"sleepTimer_hours": "{{count}} hr",
"sleepTimer_custom": "custom",
"sleepTimer_off": "off",
"sleepTimer_timeRemaining": "{{time}} remaining",
"sleepTimer_setCustom": "set timer",
"sleepTimer_cancel": "cancel timer"
"viewQueue": "view queue"
},
"queryBuilder": {
"standardTags": "standard tags",
@@ -724,8 +716,6 @@
"albumBackgroundBlur": "album background image blur size",
"analyticsDisable": "Opt-out of usage based analytics",
"analyticsDisable_description": "Anonymized usage data is sent to the developer to help improve the application",
"analyticsEnable": "Send usage-based analytics",
"analyticsEnable_description": "Anonymized usage data is sent to the developer to help improve the application",
"applicationHotkeys_description": "configure application hotkeys. toggle the checkbox to set as a global hotkey (desktop only)",
"applicationHotkeys": "application hotkeys",
"artistBackground": "artist background image",
@@ -736,7 +726,7 @@
"artistConfiguration_description": "configure what items are shown, and in what order, on the album artist page",
"artistReleaseTypeConfiguration": "artist release type configuration",
"artistReleaseTypeConfiguration_description": "configure what release types are shown, and in what order, on the album artist page",
"audioDevice_description": "select the audio device to use for playback",
"audioDevice_description": "select the audio device to use for playback (web player only)",
"audioDevice": "audio device",
"audioExclusiveMode_description": "enable exclusive output mode. In this mode, the system is usually locked out, and only mpv will be able to output audio",
"audioExclusiveMode": "audio exclusive mode",
@@ -762,8 +752,7 @@
"customCssNotice": "Warning: while there is some sanitization (disallowing url() and content:), using custom css can still pose risks by changing the interface",
"customFontPath_description": "sets the path to the custom font to use for the application",
"customFontPath": "custom font path",
"automaticUpdates": "Automatic updates",
"automaticUpdates_description": "Check for and install updates automatically",
"disableAutomaticUpdates": "disable automatic updates",
"releaseChannel_optionAlpha": "alpha (nightly)",
"releaseChannel_optionBeta": "beta",
"releaseChannel_optionLatest": "latest",
@@ -790,8 +779,6 @@
"discordRichPresence_description": "enable playback status in {{discord}} rich presence. Image keys are: {{icon}}, {{playing}}, and {{paused}}",
"discordServeImage": "serve {{discord}} images from server",
"discordServeImage_description": "share cover art for {{discord}} rich presence from server itself, only available for Jellyfin and Navidrome. {{discord}} uses a bot to fetch images, so your server must be reachable from the public internet",
"discordStateIcon": "show playing icon",
"discordStateIcon_description": "show a small playing icon in the rich presence status. the paused icon is always shown when \"Show rich presence when paused\" is enabled",
"discordUpdateInterval": "{{discord}} rich presence update interval",
"discordUpdateInterval_description": "the time in seconds between each update (minimum 15 seconds)",
"enableAutoTranslation_description": "enable translation automatically when lyrics are loaded",
@@ -908,6 +895,16 @@
"mpvExtraParameters_help": "one per line",
"musicbrainz_description": "show links to MusicBrainz on artist/album pages, where MusicBrainz ID exists",
"musicbrainz": "show MusicBrainz links",
"musicBrainzQueries": "enable MusicBrainz integration",
"musicBrainzQueries_description": "the integration will query MusicBrainz for missing artist releases and other miscellaneous data",
"musicbrainzExcludeReleaseTypes": "MusicBrainz release type exclusion",
"musicbrainzExcludeReleaseTypes_description": "release types to exclude when loading MusicBrainz artist releases",
"musicbrainzPrioritizeCountries": "MusicBrainz country priority",
"musicbrainzPrioritizeCountries_description": "countries to prioritize when ordering MusicBrainz releases (first in list has highest priority)",
"musicbrainzAutoCountryPriority": "automatic country priority",
"musicbrainzAutoCountryPriority_description": "derive country priority from the artist's MusicBrainz releases (countries with more releases are ranked higher)",
"youtube": "enable YouTube playback",
"youtube_description": "external songs will attempt to use YouTube to resolve stream URLs (desktop only)",
"neteaseTranslation_description": "When enabled, fetches and displays translated lyrics from NetEase if available",
"neteaseTranslation": "Enable NetEase translations",
"notify": "enable song notifications",
@@ -1161,7 +1158,6 @@
"year": "$t(common.year)"
},
"view": {
"detail": "detail",
"grid": "grid",
"list": "list",
"table": "table"
+24 -54
View File
@@ -32,22 +32,13 @@
"playSimilarSongs": "Reproducir canciones similares",
"viewQueue": "ver cola",
"addLastShuffled": "Al final (mezclado)",
"addNextShuffled": "Siguiente (mezclado)",
"addNextShuffled": "Al siguiente (mezclado)",
"holdToShuffle": "Mantener para mezclar",
"lyrics": "Letras",
"restoreQueueFromServer": "Restaurar cola del servidor",
"saveQueueToServer": "Guardar cola en el servidor",
"artistRadio": "Radio de artista",
"trackRadio": "Radio de pista",
"sleepTimer_minutes": "{{count}} min",
"sleepTimer_hours": "{{count}} h",
"sleepTimer_custom": "Personalizado",
"sleepTimer_setCustom": "Configurar temporizador",
"sleepTimer_cancel": "Cancelar temporizador",
"sleepTimer_timeRemaining": "{{time}} restante",
"sleepTimer_off": "Apagado",
"sleepTimer_endOfSong": "Fin de la canción actual",
"sleepTimer": "Temporizador de apagado"
"trackRadio": "Radio de pista"
},
"setting": {
"crossfadeStyle_description": "selecciona el estilo de crossfade a usar por el reproductor de audio",
@@ -104,6 +95,7 @@
"hotkey_globalSearch": "búsqueda global",
"gaplessAudio_description": "establece la configuración de audio sin pausas para mpv",
"remoteUsername_description": "establece el nombre de usuario para el control remoto del servidor. si el usuario y la contraseña están vacíos, la autenticación será deshabilitada",
"disableAutomaticUpdates": "desactiva las actualizaciones automáticas",
"exitToTray_description": "sale de la aplicación a la bandeja del sistema",
"followLyric_description": "desplaza la letra a la posición de reproducción actual",
"hotkey_favoritePreviousSong": "$t(common.previousSong) favorita",
@@ -168,7 +160,7 @@
"customFontPath": "ruta de fuente personalizada",
"followLyric": "seguir la letra actual",
"crossfadeDuration": "duración del crossfade",
"discordIdleStatus": "mostrar estado inactivo en el estado de actividad",
"discordIdleStatus": "mostrar el estado inactivo en el estado de actividad",
"sidePlayQueueStyle_optionDetached": "separada",
"audioPlayer": "reproductor de audio",
"hotkey_zoomOut": "reducir",
@@ -291,7 +283,7 @@
"releaseChannel_optionLatest": "Última versión",
"releaseChannel_optionBeta": "Beta",
"releaseChannel": "Canal de lanzamiento",
"releaseChannel_description": "Elige entre lanzamientos estables, beta, o alpha (nightly) para las actualizaciones automáticas",
"releaseChannel_description": "Elige entre lanzamientos estables o beta para las actualizaciones automáticas",
"artistBackground_description": "Añade una imagen de fondo para las páginas de artista que contienen el arte del artista",
"mediaSession": "Activar sesión de medios",
"mediaSession_description": "Activa la integración de la sesión de medios, mostrando los controles de medios y los metadatos en la superposición del volumen del sistema y en la pantalla de bloqueo",
@@ -327,8 +319,8 @@
"playerbarWaveformRadius": "Radio de la forma de onda",
"showLyricsInSidebar_description": "Se añadirá un panel a la cola de reproducción acoplada que muestra las letras",
"showLyricsInSidebar": "Mostrar letras en la barra lateral del reproductor",
"showVisualizerInSidebar_description": "Se añadirá un panel a la barra lateral del reproductor que muestra el visualizador",
"showVisualizerInSidebar": "Mostrar visualizador en la barra lateral del reproductor",
"showVisualizerInSidebar_description": "Se añadirá un panel a la barra lateral de reproducción que muestra el visualizador",
"showVisualizerInSidebar": "Mostrar visualizador en la barra lateral de reproducción",
"queryBuilder": "Generador de consultas",
"queryBuilderCustomFields_inputTag": "Etiqueta",
"queryBuilderCustomFields": "Campos personalizados",
@@ -388,19 +380,7 @@
"enableGridMultiSelect": "Activar selección múltiple de rejilla",
"enableGridMultiSelect_description": "Cuando está activo, permite seleccionar múltiples elementos en las vistas de rejilla. Cuando está desactivado, hacer clic en las imágenes de los elementos de la rejilla navegará a la página del elemento",
"sidebarPlaylistSorting": "Ordenación de la lista de reproducción de la barra lateral",
"sidebarPlaylistSorting_description": "Permite la ordenación manual de la lista de reproducción en la barra lateral usando arrastrar y soltar en lugar del orden predeterminado del servidor",
"sidebarPlaylistListFilterRegex": "Expresión regular de filtrado de listas de reproducción",
"sidebarPlaylistListFilterRegex_description": "Esconde las listas de reproducción en la barra lateral que coincidan con esta expresión regular",
"sidebarPlaylistListFilterRegex_placeholder": "p. ej. ^Mezcla diaria.*",
"blurExplicitImages": "Desenfocar imágenes explícitas",
"blurExplicitImages_description": "El álbum y la carátula de la canción etiquetados como explícitos serán desenfocados",
"releaseChannel_optionAlpha": "Alpha (nightly)",
"analyticsEnable": "Enviar analíticas basadas en el uso",
"analyticsEnable_description": "Se envían datos de uso anonimizados al desarrollador para ayudar a mejorar la aplicación",
"automaticUpdates": "Actualizaciones automáticas",
"automaticUpdates_description": "Busca e instala actualizaciones automáticamente",
"discordStateIcon": "Mostrar icono de reproducción",
"discordStateIcon_description": "Muestra un icono pequeño de reproducción en el estado de actividad. El icono de pausa se muestra siempre cuando \"Mostrar estado de actividad cuando esté en pausa\" esté activado"
"sidebarPlaylistSorting_description": "Permite la ordenación manual de la lista de reproducción en la barra lateral usando arrastrar y soltar en lugar del orden predeterminado del servidor"
},
"action": {
"editPlaylist": "editar $t(entity.playlist, {\"count\": 1})",
@@ -446,7 +426,7 @@
"backward": "hacia atrás",
"increase": "aumentar",
"rating": "calificación",
"bpm": "bpm",
"bpm": "lpm",
"refresh": "actualizar",
"unknown": "desconocido",
"areYouSure": "seguro?",
@@ -458,7 +438,7 @@
"currentSong": "$t(entity.track, {\"count\": 1}) actual",
"collapse": "contraer",
"trackNumber": "pista",
"descending": "descendente",
"descending": "descendiente",
"add": "añadir",
"ascending": "ascendente",
"dismiss": "descartar",
@@ -485,8 +465,8 @@
"cancel": "cancelar",
"forceRestartRequired": "reiniciar para aplicar cambios... cerrar la notificación para reiniciar",
"setting_one": "configuración",
"setting_many": "configuración",
"setting_other": "configuración",
"setting_many": "configuraciones",
"setting_other": "configuraciones",
"version": "versión",
"title": "título",
"filters": "filtros",
@@ -600,10 +580,10 @@
"noNetworkDescription": "No se pudo conectar a este servidor"
},
"filter": {
"mostPlayed": "más reproducidos",
"mostPlayed": "más reproducido",
"isCompilation": "es una compilación",
"recentlyPlayed": "recientemente reproducido",
"isRated": "Está calificado",
"isRated": "es clasificado",
"title": "título",
"rating": "calificación",
"search": "buscar",
@@ -619,7 +599,7 @@
"albumArtist": "$t(entity.albumArtist, {\"count\": 1})",
"isRecentlyPlayed": "reproducido recientemente",
"isFavorited": "es favorito",
"bpm": "bpm",
"bpm": "lpm",
"releaseYear": "año de lanzamiento",
"disc": "disco",
"biography": "biografía",
@@ -638,10 +618,10 @@
"owner": "$t(common.owner)",
"genre": "$t(entity.genre, {\"count\": 1})",
"id": "id",
"songCount": "número de canciones",
"songCount": "número de canción",
"isPublic": "es público",
"album": "$t(entity.album, {\"count\": 1})",
"albumCount": "Número de $t(entity.album, {\"count\": 2})",
"albumCount": "Contar $t(entity.album, {\"count\": 2})",
"explicitStatus": "$t(common.explicitStatus)",
"sortName": "Ordenar por nombre"
},
@@ -812,11 +792,7 @@
"about": "Sobre {{artist}}",
"appearsOn": "Aparece en",
"groupingTypeAll": "Todos los tipos de lanzamiento",
"groupingTypePrimary": "Tipos de lanzamiento principales",
"favoriteSongs": "Canciones favoritas",
"favoriteSongsFrom": "Canciones favoritas de {{title}}",
"topSongsPersonal": "Personal",
"topSongsCommunity": "Comunidad"
"groupingTypePrimary": "Tipos de lanzamiento principales"
},
"itemDetail": {
"copiedPath": "Ruta copiada correctamente",
@@ -850,11 +826,6 @@
"collections": {
"overrideExisting": "Sobreescribir existente",
"saveAsCollection": "Guardar como colección"
},
"releasenotes": {
"commitsSinceStable": "Actualizaciones desde {{stable}}",
"noNewCommits": "Ninguna nueva actualización en este rango",
"noStableReleaseToCompare": "Ningún lanzamiento estable disponible con el que comparar"
}
},
"form": {
@@ -880,8 +851,8 @@
"input_name": "nombre del servidor",
"success": "servidor añadido correctamente",
"input_savePassword": "guardar contraseña",
"ignoreSsl": "Ignorar SSL ($t(common.restartRequired))",
"ignoreCors": "Ignorar CORS ($t(common.restartRequired))",
"ignoreSsl": "ignorar ssl ($t(common.restartRequired))",
"ignoreCors": "ignorar cors ($t(common.restartRequired))",
"error_savePassword": "un error ocurrió cuando se intentó guardar la contraseña",
"input_preferInstantMix": "Preferir mix instantáneo",
"input_preferInstantMixDescription": "Usa solo el mix instantáneo para obtener canciones similares. Útil si tienes complementos que modifican este comportamiento",
@@ -979,7 +950,7 @@
"releaseDate": "fecha de lanzamiento",
"bitrate": "tasa de bits",
"title": "título",
"bpm": "bpm",
"bpm": "lpm",
"dateAdded": "fecha de adición",
"artist": "$t(entity.artist, {\"count\": 1})",
"songCount": "$t(entity.track, {\"count\": 2})",
@@ -1044,8 +1015,8 @@
"followCurrentSong": "seguir la canción actual",
"advancedSettings": "Opciones avanzadas",
"autosize": "Autodimensionar",
"moveUp": "Subir",
"moveDown": "Bajar",
"moveUp": "Ascender",
"moveDown": "Descender",
"pinToLeft": "Anclar a la izquierda",
"pinToRight": "Anclar a la derecha",
"alignLeft": "Alinear a la izquierda",
@@ -1068,8 +1039,7 @@
"view": {
"table": "tabla",
"list": "Lista",
"grid": "Cuadrícula",
"detail": "Detalle"
"grid": "Cuadrícula"
}
}
},
+1
View File
@@ -419,6 +419,7 @@
"customCss": "css pertsonalizatua",
"customFontPath": "letra-tipo pertsonalizatuaren bidea",
"customFontPath_description": "aplikazioan erabiliko den letra-tipo pertsonalizatuaren bidea ezartzen du",
"disableAutomaticUpdates": "desgaitu eguneratze automatikoak",
"discordApplicationId": "{{discord}} aplikazioaren IDa",
"followLyric": "jarraitu uneko letra",
"font_description": "aplikazioan erabiliko den letra-tipoa ezartzen du",
+1
View File
@@ -76,6 +76,7 @@
"hotkey_volumeDown": "کم کردن صدا",
"audioPlayer_description": "پخش‌کنندهٔ صدا را برای پخش انتخاب کنید",
"hotkey_globalSearch": "جست و جوی سراسری",
"disableAutomaticUpdates": "غیرفعال کردن به‌‌روزرسانی خودکار",
"exitToTray_description": "خروج از اپلیکیشن به system tray",
"replayGainMode_optionAlbum": "$t(entity.album, {\"count\": 1})",
"discordUpdateInterval_description": "فاصلهٔ بین هر به روزرسانی به ثانیه (حداقل ۱۵ ثانیه)",
+1
View File
@@ -375,6 +375,7 @@
"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ä",
"discordUpdateInterval_description": "päivitysväli sekunnteina (vähintään 15 sekunttia)",
+11 -27
View File
@@ -125,7 +125,7 @@
"forceRestartRequired": "redémarrer pour appliquer les changements… fermer la notification pour redémarrer",
"setting": "paramètre",
"setting_one": "paramètre",
"setting_many": "paramètres",
"setting_many": "",
"setting_other": "paramètres",
"version": "version",
"title": "titre",
@@ -202,10 +202,7 @@
"countSelected": "{{count}} sélectionnée",
"example": "exemple",
"mood": "humeur",
"retry": "réessayer",
"filter_single": "unique",
"filter_multiple": "multiple",
"rename": "renommer"
"retry": "réessayer"
},
"error": {
"remotePortWarning": "redémarrer le serveur pour appliquer le nouveau port",
@@ -273,7 +270,7 @@
"albumArtist": "$t(entity.albumArtist, {\"count\": 1})",
"comment": "commentaire",
"recentlyUpdated": "mis à jour récemment",
"channels": "$t(common.channel, {\"count\": 2})",
"channels": "$t(common.channel_other)",
"owner": "$t(common.owner)",
"genre": "$t(entity.genre, {\"count\": 1})",
"albumCount": "$t(entity.album, {\"count\": 2}) total",
@@ -281,8 +278,7 @@
"artist": "$t(entity.artist, {\"count\": 1})",
"isPublic": "est public",
"album": "$t(entity.album, {\"count\": 1})",
"explicitStatus": "$t(common.explicitStatus)",
"sortName": "tri par nom"
"explicitStatus": "$t(common.explicitStatus)"
},
"page": {
"sidebar": {
@@ -429,7 +425,7 @@
"trackList": {
"title": "$t(entity.track, {\"count\": 2})",
"artistTracks": "pistes par {{artist}}",
"genreTracks": "\"{{genre}}\" $t(entity.track, {\"count\": 2})"
"genreTracks": "'{{genre}}' $t(entity.track, {\"count\": 2})"
},
"playlistList": {
"title": "$t(entity.playlist, {\"count\": 2})"
@@ -449,12 +445,7 @@
"viewDiscography": "voir la discographie",
"relatedArtists": "$t(entity.artist, {\"count\": 2}) similaires",
"topSongs": "meilleurs titres",
"groupingTypeAll": "toutes les types de sortie",
"favoriteSongs": "titres préférées",
"groupingTypePrimary": "types de parution principale",
"topSongsCommunity": "communauté",
"topSongsPersonal": "personnel",
"favoriteSongsFrom": "meilleurs titres de {{title}}"
"groupingTypeAll": "toutes les types de sortie"
},
"itemDetail": {
"copyPath": "copier le chemin dans le presse-papiers",
@@ -480,14 +471,6 @@
},
"radioList": {
"title": "stations radio"
},
"releasenotes": {
"commitsSinceStable": "commits depuis {{stable}}",
"noNewCommits": "pas de nouveaux commits dans cette plage"
},
"windowBar": {
"paused": "(Pause) ",
"privateMode": "(Mode Privé)"
}
},
"setting": {
@@ -504,6 +487,7 @@
"applicationHotkeys_description": "configurer les raccourcis clavier dapplication. activer la case à cocher pour définir comme raccourci clavier global (bureau uniquement)",
"crossfadeStyle_description": "sélectionnez le style du fondu enchaîné à utiliser pour le lecteur audio",
"customFontPath": "chemin de police personnalisé",
"disableAutomaticUpdates": "désactiver les mises à jour automatiques",
"customFontPath_description": "définit le chemin de police personnalisé pour l'application",
"remotePort_description": "définit le port du serveur de contrôle à distance",
"hotkey_skipBackward": "reculer",
@@ -733,7 +717,7 @@
"releaseChannel_optionLatest": "dernière",
"releaseChannel_optionBeta": "bêta",
"releaseChannel": "canal de diffusion",
"releaseChannel_description": "choisissez entre les versions stables, bêta, ou alpha (nightly) pour les mises à jour automatiques",
"releaseChannel_description": "choisissez entre les versions stables ou les versions bêta pour les mises à jour automatiques",
"mediaSession": "activer media session",
"mediaSession_description": "active l'intégration Media Session, affichant les commandes multimédias et les métadonnées dans la superposition du volume du système et l'écran de verrouillage",
"enableAutoTranslation_description": "activer la traduction automatiquement lorsque les paroles sont chargées",
@@ -1038,9 +1022,9 @@
"artist": "$t(entity.artist, {\"count\": 1})",
"album": "$t(entity.album, {\"count\": 1})",
"biography": "$t(common.biography)",
"channels": "$t(common.channel, {\"count\": 2})",
"channels": "$t(common.channel_other)",
"bitrate": "$t(common.bitrate)",
"actions": "$t(common.action, {\"count\": 2})",
"actions": "$t(common.action_other)",
"favorite": "$t(common.favorite)",
"albumArtist": "$t(entity.albumArtist, {\"count\": 1})",
"rating": "$t(common.rating)",
@@ -1082,7 +1066,7 @@
"artist": "$t(entity.artist, {\"count\": 1})",
"genre": "$t(entity.genre, {\"count\": 1})",
"songCount": "$t(entity.track, {\"count\": 2})",
"channels": "$t(common.channel, {\"count\": 2})",
"channels": "$t(common.channel_other)",
"size": "$t(common.size)",
"codec": "$t(common.codec)",
"owner": "propriétaire",
+1
View File
@@ -666,6 +666,7 @@
"customCss": "egyéni css",
"customCssEnable_description": "lehetővé teszi az egyéni css írását",
"customCssEnable": "egyéni css engedélyezése",
"disableAutomaticUpdates": "automatikus frissítés kikapcsolása",
"customFontPath": "egyéni betűtípus elérési út",
"customCss_description": "egyéni css tartalom. Megjegyzés: a tartalom és a távoli URL-ek nem megengedett tulajdonságok. A tartalom előnézete az alábbiakban látható. A tisztítás miatt további mezők is megjelennek, amelyeket te nem állítottál be",
"customCssNotice": "Figyelem: bár van némi tisztítás (az url() és a content: használata nem engedélyezett), az egyéni css használata továbbra is kockázatot jelenthet, mivel megváltoztatja a felületet",
+1
View File
@@ -648,6 +648,7 @@
"customCss_description": "konten CSS kustom. Catatan: properti content dan URL jarak jauh tidak diizinkan. Pratinjau konten Anda ditampilkan di bawah. Kolom tambahan yang tidak Anda atur ada karena sanitasi",
"customFontPath": "jalur font kustom",
"customFontPath_description": "tentukan jalur font kustom yang akan digunakan aplikasi",
"disableAutomaticUpdates": "nonaktifkan pembaruan otomatis",
"discordApplicationId": "ID aplikasi {{discord}}",
"discordApplicationId_description": "id aplikasi untuk rich presence {{discord}} (default: {{defaultId}})",
"discordIdleStatus": "tampilkan status tidak aktif dalam status aktivitas",
+1
View File
@@ -202,6 +202,7 @@
"hotkey_globalSearch": "ricerca globale",
"gaplessAudio_description": "imposta l'audio gapless per mpv",
"remoteUsername_description": "imposta l'username del server di controllo remoto. Se username e password sono vuoti, l'autenticazione sarà disattivata",
"disableAutomaticUpdates": "disabilita aggiornamenti automatici",
"exitToTray_description": "riduce a icona nella barra di sistema all'uscita",
"followLyric_description": "scorre il testo alla posizione di riproduzione corrente",
"hotkey_favoritePreviousSong": "$t(common.previousSong) preferita",
+5 -4
View File
@@ -95,6 +95,7 @@
"hotkey_globalSearch": "グローバル検索",
"gaplessAudio_description": "MPV 向けのギャップレス再生を設定します",
"remoteUsername_description": "リモートコントロール サーバーのユーザ名を設定します。 ユーザー名とパスワードの両方が空の場合、認証は無効になります",
"disableAutomaticUpdates": "自動更新を無効化",
"exitToTray_description": "アプリケーション終了ボタンが押された際、システムトレイに格納します",
"followLyric_description": "現在の再生位置に歌詞をスクロールします",
"hotkey_favoritePreviousSong": "$t(common.previousSong) をお気に入り",
@@ -630,10 +631,10 @@
"genericError": "エラーが発生しました",
"credentialsRequired": "ログイン情報が必要です",
"sessionExpiredError": "セッションの有効期限が切れました",
"remoteEnableError": "リモートサーバーを$t(common.enable)にする際にエラーが発生しました",
"remoteEnableError": "リモートサーバーを $t(common.enable) にする際にエラーが発生しました",
"localFontAccessDenied": "ローカルフォントへのアクセスが拒否されました",
"serverNotSelectedError": "サーバーが選択されていません",
"remoteDisableError": "リモートサーバーを$t(common.disable)にする際にエラーが発生しました",
"remoteDisableError": "リモートサーバーを $t(common.disable) にする際にエラーが発生しました",
"mpvRequired": "MPV が必要です",
"audioDeviceFetchError": "オーディオデバイスの取得時にエラーが発生しました",
"invalidServer": "無効なサーバー",
@@ -922,8 +923,8 @@
"input_name": "サーバー名",
"success": "サーバーが追加されました",
"input_savePassword": "パスワードを保存",
"ignoreSsl": "SSL を無視します ($t(common.restartRequired))",
"ignoreCors": "CORS を無視します ($t(common.restartRequired))",
"ignoreSsl": "SSL を無視 ($t(common.restartRequired))",
"ignoreCors": "CORSを無視 ($t(common.restartRequired))",
"error_savePassword": "パスワードを保存する際にエラーが発生しました",
"input_preferInstantMixDescription": "類似曲を取得するにはインスタントミックスのみを使用してください。この動作を変更するプラグインがある場合に役立ちます",
"input_preferInstantMix": "インスタントミックスを優先する",
+17 -348
View File
@@ -119,7 +119,7 @@
"size": "grootte",
"reload": "herlaad",
"setting_one": "instelling",
"setting_other": "instellingen",
"setting_other": "",
"close": "sluiten",
"additionalParticipants": "andere deelnemers",
"newVersion": "een nieuwe versie is geïnstalleerd ({{version}})",
@@ -157,9 +157,7 @@
"example": "voorbeeld",
"mood": "stemming",
"retry": "opnieuw proberen",
"filter_single": "single",
"rename": "hernoemen",
"filter_multiple": "meerdere"
"filter_single": "single"
},
"filter": {
"rating": "rating",
@@ -204,8 +202,7 @@
"songCount": "aantal nummers",
"toYear": "tot jaar",
"trackNumber": "track",
"explicitStatus": "$t(common.explicitStatus)",
"sortName": "sorteernaam"
"explicitStatus": "$t(common.explicitStatus)"
},
"page": {
"contextMenu": {
@@ -241,8 +238,8 @@
"version": "versie {{version}}",
"settings": "$t(common.setting, {\"count\": 2})",
"manageServers": "beheer servers",
"expandSidebar": "zijbalk uitklappen",
"collapseSidebar": "zijbalk inklappen",
"expandSidebar": "sidebar uitklappen",
"collapseSidebar": "sidebar inklappen",
"openBrowserDevtools": "open browser devtools",
"quit": "$t(common.quit)",
"goBack": "terug",
@@ -301,11 +298,7 @@
"viewAllTracks": "bekijk alle $t(entity.track, {\"count\": 2})",
"recentReleases": "recente uitgaven",
"groupingTypeAll": "alle soorten publicaties",
"groupingTypePrimary": "primaire publicatiesoorten",
"favoriteSongs": "favoriete nummers",
"topSongsCommunity": "community",
"topSongsPersonal": "persoonlijk",
"favoriteSongsFrom": "favoriete nummers van {{title}}"
"groupingTypePrimary": "primaire publicatiesoorten"
},
"manageServers": {
"title": "beheer servers",
@@ -391,8 +384,7 @@
"settings": "$t(common.setting, {\"count\": 2})",
"shared": "$t(entity.playlist, {\"count\": 2}) gedeeld",
"tracks": "$t(entity.track, {\"count\": 2})",
"radio": "$t(entity.radioStation, {\"count\": 2})",
"collections": "verzamelingen"
"radio": "$t(entity.radioStation, {\"count\": 2})"
},
"trackList": {
"artistTracks": "nummers van {{artist}}",
@@ -404,14 +396,6 @@
},
"folderList": {
"title": "$t(entity.folder, {\"count\": 2})"
},
"windowBar": {
"paused": "(Gepauzeerd) ",
"privateMode": "(Privémodus)"
},
"collections": {
"overrideExisting": "bestaande overschrijven",
"saveAsCollection": "sla op als verzameling"
}
},
"error": {
@@ -456,12 +440,12 @@
"artist_other": "artiesten",
"folderWithCount_one": "{{count}} folder",
"folderWithCount_other": "{{count}} folders",
"albumArtist_one": "albumartiest",
"albumArtist_other": "albumartiesten",
"albumArtist_one": "album artiest",
"albumArtist_other": "album artiesten",
"track_one": "track",
"track_other": "tracks",
"albumArtistCount_one": "{{count}} albumartiest",
"albumArtistCount_other": "{{count}} albumartiesten",
"albumArtistCount_one": "{{count}} album artiest",
"albumArtistCount_other": "{{count}} album artiesten",
"albumWithCount_one": "{{count}} album",
"albumWithCount_other": "{{count}} albums",
"favorite_one": "favoriet",
@@ -489,107 +473,11 @@
"table": {
"column": {
"rating": "rating",
"size": "$t(common.size)",
"albumArtist": "albumartiest",
"biography": "biografie",
"bitrate": "bitsnelheid",
"comment": "opmerking",
"dateAdded": "datum toegevoegd",
"favorite": "favoriet",
"discNumber": "disc",
"bpm": "bpm",
"album": "album",
"lastPlayed": "laatst gespeeld",
"path": "pad",
"playCount": "keren gespeeld",
"releaseDate": "uitgavedatum",
"releaseYear": "jaar",
"title": "titel",
"trackNumber": "nummer",
"owner": "eigenaar",
"albumCount": "$t(entity.album, {\"count\": 2})",
"artist": "$t(entity.artist, {\"count\": 1})",
"bitDepth": "$t(common.bitDepth)",
"channels": "$t(common.channel, {\"count\": 2})",
"codec": "$t(common.codec)",
"genre": "$t(entity.genre, {\"count\": 1})",
"sampleRate": "$t(common.sampleRate)",
"songCount": "$t(entity.track, {\"count\": 2})"
"size": "$t(common.size)"
},
"config": {
"label": {
"rating": "$t(common.rating)",
"composer": "componist",
"dateAdded": "datum toegevoegd",
"discNumber": "discnummer",
"image": "afbeelding",
"lastPlayed": "laatst gespeeld",
"playCount": "keren afgespeeld",
"releaseDate": "uitgavedatum",
"rowIndex": "rij-index",
"trackNumber": "nummer",
"actions": "$t(common.action, {\"count\": 2})",
"album": "$t(entity.album, {\"count\": 1})",
"albumCount": "$t(entity.album, {\"count\": 2})",
"albumArtist": "$t(entity.albumArtist, {\"count\": 1})",
"artist": "$t(entity.artist, {\"count\": 1})",
"biography": "$t(common.biography)",
"bitDepth": "$t(common.bitDepth)",
"bitrate": "$t(common.bitrate)",
"bpm": "$t(common.bpm)",
"channels": "$t(common.channel, {\"count\": 2})",
"codec": "$t(common.codec)",
"duration": "$t(common.duration)",
"favorite": "$t(common.favorite)",
"genre": "$t(entity.genre, {\"count\": 1})",
"genreBadge": "$t(entity.genre, {\"count\": 1}) (badges)",
"note": "$t(common.note)",
"owner": "$t(common.owner)",
"path": "$t(common.path)",
"sampleRate": "$t(common.sampleRate)",
"size": "$t(common.size)",
"songCount": "$t(entity.track, {\"count\": 2})",
"title": "$t(common.title)",
"titleArtist": "$t(common.title) (artiest)",
"titleCombined": "$t(common.title) (gecombineerd)",
"year": "$t(common.year)"
},
"general": {
"advancedSettings": "geavanceerde instellingen",
"autoFitColumns": "kolommen automatisch passend maken",
"autosize": "automatische afmetingen",
"moveUp": "omhoog verplaatsen",
"moveDown": "omlaag verplaatsen",
"pinToLeft": "links vastpinnen",
"pinToRight": "rechts vastpinnen",
"alignLeft": "links uitlijnen",
"alignCenter": "centreren",
"alignRight": "rechts uitlijnen",
"followCurrentSong": "huidige nummer volgen",
"displayType": "weergavesoort",
"itemGap": "ruimte tussen items (px)",
"itemSize": "grootte item (px)",
"itemsPerRow": "items per rij",
"size_default": "standaard",
"size_compact": "compact",
"size_large": "groot",
"tableColumns": "kolommen",
"pagination": "paginering",
"pagination_itemsPerPage": "items per pagina",
"pagination_infinite": "oneindig",
"pagination_paginate": "gepagineerd",
"alternateRowColors": "afwisselende rijkleuren",
"horizontalBorders": "randen om rijen",
"rowHoverHighlight": "oplichtende rijen bij zweven met de muis",
"showHeader": "toon kop",
"verticalBorders": "randen om kolommen",
"gap": "$t(common.gap)",
"size": "$t(common.size)"
},
"view": {
"grid": "grid",
"list": "lijst",
"table": "tabel"
"rating": "$t(common.rating)"
}
}
},
@@ -615,8 +503,8 @@
"exportImportSettings_importBtn": "importeer instellingen",
"exportImportSettings_importModalTitle": "importeer feishing-instellingen",
"exportImportSettings_importSuccess": "instellingen zijn succesvol geïmporteerd!",
"exportImportSettings_notValidJSON": "het ingevoerde bestand is geen geldige JSON",
"exportImportSettings_offendingKeyError": "\"{{offendingKey}}\" is onjuist - {{reason}}",
"exportImportSettings_notValidJSON": "het ingevoerde bestand is geen valide JSON",
"exportImportSettings_offendingKeyError": "\"{{offendingKey}}\" is incorrect - {{reason}}",
"externalLinks_description": "maakt het mogelijk om externe links (Last.fm, MusicBrainz) te tonen op artiesten-/albumpagina's",
"externalLinks": "toon externe links",
"followLyric_description": "scroll de songtekst naar de huidige positie",
@@ -685,6 +573,7 @@
"customCssNotice": "Waarschuwing: ondanks sanering (het niet toestaan van url() en content:) brengt aangepaste css nog steeds risico's met zich mee omdat de interface wordt gewijzigd",
"customFontPath_description": "bepaal het pad naar het aangepaste lettertype voor gebruik in de applicatie",
"customFontPath": "aangepaste lettertypelocatie",
"disableAutomaticUpdates": "automatische updates uitschakelen",
"releaseChannel_optionBeta": "beta",
"releaseChannel_optionLatest": "meest recente",
"releaseChannel": "releasekanaal",
@@ -871,73 +760,7 @@
"sidebarPlaylistList": "afspeellijsten zijbalk",
"sidePlayQueueStyle_description": "de stijl van de wachtrij aan de zijkant",
"sidePlayQueueStyle_optionAttached": "aangekoppeld",
"sidePlayQueueStyle_optionDetached": "afgekoppeld",
"homeFeatureStyle_description": "bepaalt de stijl van de uitgelicht-carrousel op de homepagina",
"homeFeatureStyle": "stijl uitgelicht-carrousel",
"homeFeatureStyle_optionMultiple": "meervoudig",
"homeFeatureStyle_optionSingle": "enkelvoudig",
"blurExplicitImages": "vervaag expliciete afbeeldingen",
"blurExplicitImages_description": "hoezen van albums en nummers die getagd zijn als expliciet zullen worden vervaagd",
"mediaSession": "mediasessie inschakelen",
"sidePlayQueueStyle": "stijl van zijwachtrij",
"skipDuration_description": "de tijdsduur die wordt doorgespoeld bij gebruik van de spoelknoppen in de afspeelbalk",
"skipDuration": "doorspoelduur",
"startMinimized_description": "start de applicatie in het systeemvak",
"startMinimized": "start geminimaliseerd",
"theme": "thema",
"theme_description": "het visuele thema dat de applicatie gebruikt",
"themeDark_description": "het donkere thema dat de applicatie gebruikt",
"themeDark": "thema (donker)",
"themeLight_description": "het lichte thema dat de applicatie gebruikt",
"themeLight": "thema (licht)",
"transcode": "transcoderen inschakelen",
"transcode_description": "schakel transcoderen naar andere formaten in",
"transcodeBitrate_description": "de bitsnelheid waarnaar wordt getranscodeerd. bij 0 bepaalt de server de waarde",
"transcodeBitrate": "transcodeerbitsnelheid",
"transcodeFormat_description": "het formaat waarnaar wordt getranscodeerd. laat leeg om de server te laten bepalen",
"transcodeFormat": "transcodeerformaat",
"translationApiKey_description": "api-sleutel voor vertaling (enkel globaal service-eindpunt)",
"translationApiKey": "vertalings-api-sleutel",
"translationApiProvider_description": "api-provider voor vertalingen",
"translationApiProvider": "vertalings-api-provider",
"translationTargetLanguage_description": "doeltaal voor vertalingen",
"translationTargetLanguage": "doeltaal vertaling",
"trayEnabled_description": "toon/verstop het systeemvakicoon/-menu. indien uitgeschakeld wordt het minimaliseren/sluiten naar het systeemvak ook uitgeschakeld",
"trayEnabled": "toon systeemvak",
"useSystemTheme_description": "volg de systeemvoorkeur voor licht of donker thema",
"useSystemTheme": "gebruik systeemthema",
"volumeWheelStep_description": "de hoeveelheid volume die gewijzigd wordt bij het scrollen met het muiswiel op de volumebalk",
"volumeWheelStep": "volumestap muiswiel",
"volumeWidth_description": "de breedte van de volumebalk",
"volumeWidth": "volumebalkbreedte",
"webAudio_description": "gebruik web-audio. dit schakeld geavanceerde mogelijkheden als replaygain in. schakel uit als dit niet werkt",
"webAudio": "gebruik web-audio",
"windowBarStyle": "vensterbalkstijl",
"windowBarStyle_description": "kies de stijl van de vensterbalk",
"zoom": "zoompercentage",
"zoom_description": "het zoompercentage van de applicatie",
"queryBuilder": "opdrachtbouwer",
"queryBuilderCustomFields": "aangepaste velden",
"queryBuilderCustomFields_description": "voeg aangepaste velden voor gebruik in opdrachtbouwers toe",
"enableGridMultiSelect": "meervoudig selecteren in grid",
"enableGridMultiSelect_description": "staat toe meerdere items in gridweergaven te selecteren. indien uitgeschakeld zal het klikken op een item in een gridweergave naar diens pagina navigeren",
"sidebarPlaylistSorting": "afspeellijstsortering in zijbalk",
"sidebarPlaylistSorting_description": "activeert het handmatig sorteren van de afspeellijst in de zijbalk door middel van slepen in plaats van het gebruiken van de servervolgorde",
"sidebarPlaylistListFilterRegex_description": "verberg afspeellijsten in de zijbalk die overeenkomen met deze reguliere expressie",
"sidebarPlaylistListFilterRegex_placeholder": "bijv. ^Daily Mix.*",
"sidebarPlaylistListFilterRegex": "regex afspeellijstfilter",
"mediaSession_description": "schakelt mediasessie-integratie in, waarbij mediabesturing en metadata in het volumeweergave en het lock-scherm worden weergegeven",
"skipPlaylistPage": "sla afspeellijstpagina over",
"skipPlaylistPage_description": "ga naar de nummerlijst in plaats van de standaard pagina bij het navigeren naar een afspeellijst",
"queryBuilderCustomFields_inputLabel": "label",
"queryBuilderCustomFields_inputTag": "tag",
"playButtonBehavior_optionAddLast": "$t(player.addLast)",
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
"playButtonBehavior_optionPlay": "$t(player.play)",
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
"replayGainMode_optionAlbum": "$t(entity.album, {\"count\": 1})",
"replayGainMode_optionNone": "$t(common.none)",
"replayGainMode_optionTrack": "$t(entity.track, {\"count\": 1})"
"sidePlayQueueStyle_optionDetached": "afgekoppeld"
},
"form": {
"addServer": {
@@ -1141,159 +964,5 @@
"soundtrack": "soundtrack",
"spokenWord": "gesproken woord"
}
},
"dragDropZone": {
"error_oneFileOnly": "Kies één bestand",
"error_readingFile": "probleem opgetreden bij het lezen van het bestand: {{errorMessage}}",
"mainText": "sleep hier een bestand naartoe"
},
"visualizer": {
"visualizerType": "Type Visualiseerder",
"cyclePresets": "Doorloop Voorinstellingen",
"cycleTime": "Cyclustijd (seconden)",
"ignoredPresets": "Genegeerde Voorinstellingen",
"selectedPresets": "Gekozen Voorinstellingen",
"includeAllPresets": "Alle Voorinstellingen Opnemen",
"randomizeNextPreset": "Willekeurige Volgende Voorinstelling",
"blendTime": "Mengtijd",
"presets": "Voorinstellingen",
"selectPreset": "Kies Voorinstelling",
"applyPreset": "Voorinstelling Toepassen",
"saveAsPreset": "Opslaan als Voorinstelling",
"updatePreset": "Voorinstelling Bijwerken",
"copyConfiguration": "Kopieer Configuratie",
"pasteConfiguration": "Plak Configuratie",
"pasteConfigurationPlaceholder": "Plak JSON-configuratie hier...",
"pasteFromClipboard": "Plakken vanaf Klembord",
"applyConfiguration": "Configuratie Toepassen",
"configCopied": "Configuratie gekopieerd naar het klembord",
"configCopyFailed": "Kopiëren van configuratie is mislukt",
"configPasted": "Configuratie succesvol toegepast",
"configPasteFailed": "Toepassen configuratie mislukt. Controleer het formaat.",
"configPasteReadFailed": "Lezen van het klembord mislukt",
"presetName": "Naam Voorinstelling",
"presetNamePlaceholder": "Voer de naam van de voorinstelling in",
"general": "Algemeen",
"mode": "Modus",
"mode1To8": "Modus 1-8",
"mode10": "Modus 10",
"barSpace": "Balkruimte",
"lineWidth": "Lijnbreedte",
"fillAlpha": "Alfavulling",
"channelLayout": "Kanaalindeling",
"maxFPS": "Max FPS",
"opacity": "Opaciteit",
"customGradients": "Aangepaste Kleurverlopen",
"addCustomGradient": "Voeg Aangepast Kleurverloop Toe",
"gradientName": "Naam Kleurverloop",
"gradientNamePlaceholder": "Naam Kleurverloop",
"vertical": "Verticaal",
"horizontal": "Horizontaal",
"colorStops": "Kleurstop",
"addColor": "Voeg Kleur Toe",
"position": "Positie",
"level": "Niveau",
"remove": "Verwijder",
"pasteGradient": "Plak Kleurverloop",
"pasteGradientPlaceholder": "Plak JSON van kleurverloop hier...",
"custom": "Aangepast",
"builtIn": "Ingebouwd",
"colors": "Kleuren",
"colorMode": "Kleurmodus",
"gradient": "Kleurverloop",
"gradientLeft": "Kleurverloop Links",
"gradientRight": "Kleurverloop Rechts",
"fft": "FFT",
"fftSize": "FFT-grootte",
"smoothing": "Gladstrijken",
"frequencyRangeAndScaling": "Frequentiebereik en -schaling",
"minimumFrequency": "Minimumfrequentie",
"maximumFrequency": "Maximumfrequentie",
"frequencyScale": "Frequentieschaal",
"sensitivity": "Gevoeligheid",
"weightingFilter": "Gewichtsfilter",
"minimumDecibels": "Minimum aantal decibel",
"maximumDecibels": "Maximum aantal decibel",
"linearAmplitude": "Lineaire Amplitude",
"linearBoost": "Lineaire Versterking",
"peakBehavior": "Piekgedrag",
"showPeaks": "Toon Pieken",
"fadePeaks": "Vervaag Pieken",
"peakLine": "Pieklijn",
"gravity": "Zwaartekracht",
"peakFadeTime": "Piekvervagingstijd (ms)",
"peakHoldTime": "Piekvasthoudtijd (ms)",
"radialSpectrum": "Radiaal Spectrum",
"radial": "Radiaal",
"radialInvert": "Geïnverteerde Radiaal",
"spinSpeed": "Draaisnelheid",
"radius": "Radius",
"reflexMirror": "Reflexspiegel",
"reflexRatio": "Reflexverhouding",
"reflexAlpha": "Reflex-alfa",
"reflexFit": "Reflex-inpassing",
"reflexBrightness": "Reflex-helderheid",
"mirror": "Spiegel",
"miscellaneousSettings": "Diverse Instellingen",
"alphaBars": "Alfabalken",
"ansiBands": "ANSI-banden",
"ledBars": "LED-balken",
"trueLeds": "Ware LEDs",
"lumiBars": "Lumi-balken",
"outlineBars": "Uitgelijnde balken",
"roundBars": "Ronde Balken",
"lowResolution": "Lage Resolutie",
"splitGradient": "Gescheiden Kleurverloop",
"showFPS": "Toon FPS",
"showScaleX": "Toon X-schaal",
"showScaleY": "Toon Y-schaal",
"noteLabels": "Nootlabels",
"options": {
"mode": {
"0": "[0] Discrete Frequenties",
"1": "[1] 1/24e octaaf / 240 bands",
"2": "[2] 1/12e octaaf / 120 bands",
"3": "[3] 1/8e octaaf / 80 bands",
"4": "[4] 1/6e octaaf / 60 bands",
"5": "[5] 1/4e octaaf / 40 bands",
"6": "[6] 1/3e octaaf / 30 bands",
"7": "[7] Half octaaf / 20 bands",
"8": "[8] Volledig octaaf / 10 bands",
"10": "[10] Lijn- / Gebiedsgrafiek"
},
"colorMode": {
"gradient": "Kleurverloop",
"barIndex": "Balk-index",
"barLevel": "Balkniveau"
},
"gradient": {
"classic": "Klassiek",
"prism": "Prisma",
"rainbow": "Regenboog",
"steelblue": "Staalblauw",
"orangered": "Oranjerood"
},
"channelLayout": {
"single": "Enkelvoudig",
"dualCombined": "Duaalgecombineerd",
"dualHorizontal": "Duaalhorizontaal",
"dualVertical": "Duaalvertikaal"
},
"frequencyScale": {
"none": "Geen",
"bark": "Bark-schaal",
"linear": "Lineaire Schaal",
"log": "Log-schaal",
"mel": "Mel-schaal"
},
"weightingFilter": {
"none": "Geen",
"a": "A",
"b": "B",
"c": "C",
"d": "D",
"z": "Z"
}
}
}
}
+13 -40
View File
@@ -73,9 +73,9 @@
"delete": "usuń",
"cancel": "anuluj",
"forceRestartRequired": "zrestartuj aby zastosować zmiany... zamknij powiadomienie aby zrestartować",
"setting_one": "ustawienie",
"setting_few": "ustawienia",
"setting_many": "ustawień",
"setting_one": "ustawienia",
"setting_few": "",
"setting_many": "",
"version": "wersja",
"title": "tytuł",
"filter_one": "filtr",
@@ -176,14 +176,14 @@
"playlist_few": "playlisty",
"playlist_many": "playlist",
"artist_one": "wykonawca",
"artist_few": "wykonawcy",
"artist_few": "wykonawców",
"artist_many": "wykonawców",
"folderWithCount_one": "{{count}} katalog",
"folderWithCount_few": "{{count}} katalogi",
"folderWithCount_many": "{{count}} katalogów",
"albumArtist_one": "wykonawca albumu",
"albumArtist_few": "wykonawcy albumu",
"albumArtist_many": "wykonawcy albumów",
"albumArtist_many": "wykonawców albumu",
"track_one": "utwór",
"track_few": "utwory",
"track_many": "utworów",
@@ -428,7 +428,7 @@
"dynamicIsImage": "włącz obraz w tle",
"lyricOffset": "opóźnienie tekstów (ms)"
},
"upNext": "następne",
"upNext": "następny",
"lyrics": "tekst",
"related": "powiązane",
"visualizer": "wizualizer",
@@ -577,11 +577,7 @@
"appearsOn": "pojawia się na",
"viewAllTracks": "zobacz wszystko $t(entity.track, {\"count\": 2})",
"groupingTypeAll": "wszystkie typy wydań",
"groupingTypePrimary": "główne typy wydań",
"favoriteSongs": "ulubione piosenki",
"topSongsCommunity": "społeczność",
"topSongsPersonal": "osobiste",
"favoriteSongsFrom": "ulubione piosenki z {{title}}"
"groupingTypePrimary": "główne typy wydań"
},
"itemDetail": {
"copyPath": "kopiuj ścieżkę do schowka",
@@ -615,11 +611,6 @@
"collections": {
"overrideExisting": "nadpisz istniejące",
"saveAsCollection": "zapisz jako kolekcję"
},
"releasenotes": {
"commitsSinceStable": "commity od {{stable}}",
"noNewCommits": "brak nowych commitów w tym zakresie",
"noStableReleaseToCompare": "brak dostępnego stabilnego wydania do porównania"
}
},
"player": {
@@ -661,16 +652,7 @@
"restoreQueueFromServer": "przywróć kolejkę z serwera",
"saveQueueToServer": "zapisz kolejkę na serwerze",
"artistRadio": "radio wykonawcy",
"trackRadio": "radio utworu",
"sleepTimer": "wyłącznik czasowy",
"sleepTimer_endOfSong": "do końca aktualnej piosenki",
"sleepTimer_minutes": "{{count}} min",
"sleepTimer_hours": "{{count}} godz",
"sleepTimer_custom": "niestandardowy",
"sleepTimer_off": "wyłączony",
"sleepTimer_timeRemaining": "pozostało {{time}}",
"sleepTimer_setCustom": "ustaw wyłącznik",
"sleepTimer_cancel": "anuluj wyłączanie"
"trackRadio": "radio utworu"
},
"setting": {
"crossfadeStyle_description": "wybierz styl przenikania, który ma być używany do odtwarzania dźwięku",
@@ -702,6 +684,7 @@
"globalMediaHotkeys": "globalne skróty klawiszowe multimediów",
"hotkey_globalSearch": "globalne wyszukiwanie",
"gaplessAudio_description": "ustaw dźwięk bez przerw dla mpv",
"disableAutomaticUpdates": "wyłącz automatyczne aktualizacje",
"exitToTray_description": "zamknij aplikację do zasobnika systemowego",
"followLyric_description": "przewiń tekst do obecnego momentu",
"hotkey_favoritePreviousSong": "ulubiona $t(common.previousSong)",
@@ -745,7 +728,7 @@
"hotkey_zoomOut": "oddal",
"hotkey_unfavoriteCurrentSong": "usuń $t(common.currentSong) z ulubionych",
"hotkey_rate0": "wyczyść oceny",
"discordApplicationId": "id aplikacji {{discord}}",
"discordApplicationId": "ID aplikacji {{discord}}",
"applicationHotkeys_description": "ustaw skróty klawiszowe aplikacji. przełącz pole wyboru aby ustawić skrót globalny (tylko komputery)",
"hotkey_volumeMute": "wycisz",
"hotkey_toggleCurrentSongFavorite": "dodaj $t(common.currentSong) do ulubionych",
@@ -903,7 +886,7 @@
"releaseChannel_optionBeta": "beta",
"releaseChannel_optionLatest": "najnowsza",
"releaseChannel": "kanał wydań",
"releaseChannel_description": "wybieraj pomiędzy wydaniami stabilnymi, beta lub alpha (nightly) dla automatycznych aktualizacji",
"releaseChannel_description": "wybieraj pomiędzy stabilnymi wydaniami a wydaniami beta dla automatycznych aktualizacji",
"discordDisplayType_artistname": "nazwa(y) wykonawców",
"discordDisplayType_description": "zmienia co jest pokazywane jako słuchane w twoim statusie",
"discordDisplayType_songname": "nazwa piosenki",
@@ -1014,24 +997,14 @@
"sidebarPlaylistSorting": "sortowanie playlist w bocznym pasku",
"sidebarPlaylistListFilterRegex_description": "ukryj playlisty w pasku bocznym pasujące do wyrażenia regularnego",
"sidebarPlaylistListFilterRegex_placeholder": "np. ^Miks codzienny.^",
"sidebarPlaylistListFilterRegex": "filtr playlist regex",
"blurExplicitImages": "rozmazuj nieodpowiednie obrazy",
"blurExplicitImages_description": "obrazy piosenek oraz albumów oznaczone jako nieodpowiednie będą rozmazywane",
"releaseChannel_optionAlpha": "alpha (nightly)",
"analyticsEnable": "Wysyłaj statystyki na podstawie użytkowania",
"analyticsEnable_description": "Zanonimizowane statystki użytkowania będą wysyłane do twórcy, aby pomóc w poprawie aplikacji",
"automaticUpdates": "Aktualizacje automatyczne",
"automaticUpdates_description": "Sprawdzaj i instaluj aktualizacje automatycznie",
"discordStateIcon": "pokaż ikonę odtwarzania",
"discordStateIcon_description": "pokazuje małą ikonę odtwarzania w statusie. ikona pauzy jest zawsze pokazywana gdy \"Pokaż status podczas pauzy\" jest włączone"
"sidebarPlaylistListFilterRegex": "filtr playlist regex"
},
"table": {
"config": {
"view": {
"table": "tabela",
"grid": "siatka",
"list": "lista",
"detail": "szczegół"
"list": "lista"
},
"general": {
"displayType": "typ wyświetlania",
+1
View File
@@ -230,6 +230,7 @@
"crossfadeDuration_description": "define a duração do efeito crossfade",
"customCssNotice": "Atenção: embora haja alguma sanitização (proibindo url() e content:), usar CSS personalizado ainda pode representar riscos ao alterar a interface",
"crossfadeStyle_description": "seleciona qual estilo de crossfade usado no player de áudio",
"disableAutomaticUpdates": "desabilitar atualizações automáticas",
"disableLibraryUpdateOnStartup": "desabilitar a verificação de novas versões na inicialização",
"artistBackground": "Imagem de fundo do artista",
"artistBackground_description": "Adiciona uma imagem de fundo às páginas do artista contendo a arte do artista",
+1
View File
@@ -523,6 +523,7 @@
"customCssEnable_description": "permite escrever css customizado",
"customCssNotice": "Aviso: apesar de existir alguma higienização (url() e content: não são permitidas), o uso de css personalizado ainda pode representar riscos ao alterar a interface",
"customCss": "css customizado",
"disableAutomaticUpdates": "desativar atualizações automáticas",
"disableLibraryUpdateOnStartup": "desativar a verificação de novas versões na inicialização",
"discordApplicationId": "{{discord}} ID da aplicação",
"discordIdleStatus_description": "quando ativado, atualiza o estado enquanto o player está ocioso",
+1
View File
@@ -727,6 +727,7 @@
"disableLibraryUpdateOnStartup": "отключить проверку новых версий при запуске приложения",
"minimizeToTray_description": "сворачивать приложение в панель уведомлений",
"audioPlayer_description": "укажите, какой аудиоплеер использовать для воспроизведения",
"disableAutomaticUpdates": "отключить проверку обновлений",
"exitToTray_description": "При закрытии приложения - оно останется в панели уведомлений",
"fontType_optionCustom": "пользовательский",
"remotePassword": "пароль к серверу удалённого управления",
+1
View File
@@ -541,6 +541,7 @@
"customCss_description": "vlastný css obsah. Poznámka: obsah a vzdialené url linky sú defaultne deaktivované.Náhľad vášho obsahu je zobrazený nižšie. Pridané polia, ktoré ste nenastavovali boli pridané pri sanitizácii",
"customFontPath": "cesta k vlastným fontom",
"customFontPath_description": "Nastaví cestu k vlastným fontom na použitie aplikáciou",
"disableAutomaticUpdates": "vypnúť automatické aktualizácie",
"disableLibraryUpdateOnStartup": "vypnúť kontrolu nových verzií pri štarte",
"discordApplicationId": "id aplikácie {{discord}}",
"discordApplicationId_description": "aplikačné id pre plnohodnotné prepojenie s {{discord}} (predvolená hodnota {{defaultId}})",
+1
View File
@@ -548,6 +548,7 @@
"customCss_description": "vsebina css po meri. Opomba: vsebina in oddaljeni url-ji so prepovedane lastnosti. Spodaj je prikazan predogled vaše vsebine. Dodatna polja, ki jih niste nastavili, so prisotna zaradi prečiščevanja",
"customFontPath": "pot za pisavo po meri",
"customFontPath_description": "nastavi pot do pisave po meri",
"disableAutomaticUpdates": "onemogoči samodejne posodobitve",
"disableLibraryUpdateOnStartup": "onemogoči prevejranje novih verzij ob zagonu",
"discordApplicationId": "{{discord}} identifikator aplikacije",
"discordApplicationId_description": "identifikator aplikacije za {{discord}} bogato prezenco (privzeto {{defaultId}})",
+1
View File
@@ -88,6 +88,7 @@
"hotkey_globalSearch": "globalno pretraživanje",
"gaplessAudio_description": "postavlja opciju bez pauze zvuka za mpv (preporučeno: slabo)",
"remoteUsername_description": "postavlja korisničko ime za daljinsku kontrolu servera. Ako su i korisničko ime i lozinka prazni, autentifikacija će biti onemogućena",
"disableAutomaticUpdates": "onemogući automatsko ažuriranje",
"exitToTray_description": "izlazak aplikacije u sistemsku traku",
"followLyric_description": "pomera tekst pesme na trenutnu poziciju reprodukcije",
"hotkey_favoritePreviousSong": "omiljena $t(common.previousSong)",
+1
View File
@@ -609,6 +609,7 @@
"customCssEnable": "தனிப்பயன் சிஎச்எச் ஐ இயக்கவும்",
"customCssNotice": "எச்சரிக்கை: சில சுத்திகரிப்பு (URL () மற்றும் உள்ளடக்கத்தை அனுமதிக்காதது :) இருக்கும்போது, தனிப்பயன் சிஎச்எச் ஐப் பயன்படுத்துவது இடைமுகத்தை மாற்றுவதன் மூலம் ஆபத்துக்களை ஏற்படுத்தக்கூடும்",
"contextMenu_description": "நீங்கள் ஒரு உருப்படியை வலது சொடுக்கு செய்யும் போது பட்டியலில் காட்டப்பட்டுள்ள உருப்படிகளை மறைக்க உங்களை அனுமதிக்கிறது. சரிபார்க்கப்படாத உருப்படிகள் மறைக்கப்படும்",
"disableAutomaticUpdates": "தானியங்கி புதுப்பிப்புகளை முடக்கு",
"discordApplicationId_description": "{{discord}} பணக்கார இருப்புக்கான பயன்பாட்டு ஐடி (இயல்புநிலை {{defaultId}})",
"discordIdleStatus": "பணக்கார இருப்பு செயலற்ற நிலையைக் காட்டுங்கள்",
"discordIdleStatus_description": "இயக்கப்பட்டால், பிளேயர் சும்மா இருக்கும்போது நிலையைப் புதுப்பிக்கவும்",
+1
View File
@@ -534,6 +534,7 @@
"customCss_description": "özel css içeriği. Not: içerik ve uzaktan URL'ler izin verilmeyen özelliklerdir. İçeriğinizin önizlemesi aşağıda gösterilmektedir. Ayarlamadığınız ek alanlar sterilleme nedeniyle mevcuttur",
"customFontPath": "özel yazı tipi yolu",
"customFontPath_description": "uygulama için kullanılacak özel yazı tipinin yolunu ayarlar",
"disableAutomaticUpdates": "otomatik güncellemeleri devre dışı bırak",
"disableLibraryUpdateOnStartup": "başlangıçta yeni sürümler için denetimi devre dışı bırak",
"discordApplicationId": "{{discord}} uygulama kimliği",
"discordApplicationId_description": "{{discord}} \"Rich Presence\" için uygulama kimliği (varsayılan olarak {{defaultId}})",
+1 -540
View File
@@ -1,544 +1,5 @@
{
"action": {
"addToFavorites": "додати до $t(entity.favorite, {\"count\": 2})",
"addOrRemoveFromSelection": "додати або видалити з вибору",
"selectRangeOfItems": "вибрати діапазон елементів",
"addToPlaylist": "додати до $t(entity.playlist, {\"count\": 1})",
"clearQueue": "очистити чергу",
"createPlaylist": "створити $t(entity.playlist, {\"count\": 1})",
"createRadioStation": "створити $t(entity.radioStation, {\"count\": 1})",
"deletePlaylist": "видалити $t(entity.playlist, {\"count\": 1})",
"deleteRadioStation": "видалити $t(entity.radioStation, {\"count\": 1})",
"selectAll": "вибрати все",
"deselectAll": "скасувати вибір усього",
"downloadStarted": "почато завантаження {{count}} елементів",
"editPlaylist": "редагувати $t(entity.playlist, {\"count\": 1})",
"goToPage": "перейти на сторінку",
"moveToNext": "перейти до наступного",
"moveToBottom": "перемістити вниз",
"moveToTop": "перемістити вгору",
"moveUp": "перемістити вище",
"moveDown": "перемістити нижче",
"holdToMoveToTop": "утримуйте, щоб перемістити вгору",
"holdToMoveToBottom": "утримувати, щоб перемістити вниз",
"moveItems": "перемістити елементи",
"shuffle": "перемішати",
"shuffleAll": "все випадково",
"shuffleSelected": "вибране випадково",
"refresh": "$t(common.refresh)",
"removeFromFavorites": "видалити з $t(entity.favorite, {\"count\": 2})",
"removeFromPlaylist": "видалити з $t(entity.playlist, {\"count\": 1})",
"removeFromQueue": "видалити з черги",
"setRating": "встановити рейтинг",
"toggleSmartPlaylistEditor": "перемикати редактор $t(entity.smartPlaylist)",
"viewPlaylists": "показати $t(entity.playlist, {\"count\": 2})",
"viewMore": "переглянути більше",
"openApplicationDirectory": "відкрити каталог додатків",
"openIn": {
"lastfm": "Відкрити в Last.fm",
"musicbrainz": "Відкрити в MusicBrainz"
}
},
"common": {
"countSelected": "вибрано {{count}}",
"explicitStatus": "явний статус",
"action_one": "дія",
"action_few": "дії",
"action_many": "дій",
"add": "додати",
"additionalParticipants": "додаткові учасники",
"newVersion": "встановлено нову версію ({{version}})",
"viewReleaseNotes": "переглянути список змін",
"albumGain": "підсилення альбому",
"albumPeak": "піковий рівень альбому",
"areYouSure": "ви впевнені?",
"ascending": "зростаючи",
"backward": "назад",
"biography": "біографія",
"bitDepth": "розрядність",
"bitrate": "бітрейт",
"bpm": "уд/хв",
"cancel": "скасувати",
"center": "посередині",
"channel_one": "канал",
"channel_few": "канали",
"channel_many": "каналів",
"clear": "очистити",
"close": "закрити",
"codec": "кодек",
"collapse": "згорнути",
"comingSoon": "скоро…",
"configure": "налаштувати",
"confirm": "підтвердити",
"create": "створити",
"currentSong": "поточний $t(entity.track, {\"count\": 1})",
"decrease": "знизити",
"delete": "видалити",
"descending": "за спаданням",
"description": "опис",
"disable": "вимкнути",
"disc": "диск",
"dismiss": "відхилити",
"doNotShowAgain": "не показувати це знову",
"duration": "тривалість",
"view": "показати",
"edit": "змінити",
"enable": "увімкнути",
"expand": "розширити",
"example": "приклад",
"externalLinks": "зовнішні посилання",
"faster": "швидше",
"favorite": "улюблений",
"filter_one": "фільтр",
"filter_few": "фільтри",
"filter_many": "фільтрів",
"filters": "фільтри",
"filter_single": "одиночний",
"filter_multiple": "кілька",
"forceRestartRequired": "перезапустіть, щоб застосувати зміни… закрийте повідомлення, щоб перезапустити",
"forward": "уперед",
"gap": "прогалина",
"home": "додому",
"increase": "збільшити",
"left": "ліво",
"limit": "ліміт",
"manage": "управління",
"maximize": "максимізувати",
"menu": "меню",
"minimize": "мінімізувати",
"modified": "відредаговано",
"mbid": "MusicBrainz ID",
"mood": "настрій",
"name": "назва",
"no": "ні",
"none": "жоден",
"noResultsFromQuery": "запит не дав результатів",
"noFilters": "фільтри не налаштовані",
"note": "примітка",
"ok": "ок",
"owner": "власник",
"path": "шлях",
"playerMustBePaused": "плеєр повинен бути призупинений",
"preview": "перегляд",
"previousSong": "минулий $t(entity.track, {\"count\": 1})",
"private": "приватний",
"public": "публічний",
"quit": "вийти",
"random": "випадково",
"rating": "рейтинг",
"retry": "повторити спробу",
"recordLabel": "лейбл звукозапису",
"releaseType": "тип випуску",
"refresh": "оновити",
"reload": "перезавантажити",
"rename": "перейменувати",
"reset": "скинути",
"resetToDefault": "скинути до заводських налаштувань",
"restartRequired": "необхідний перезапуск",
"right": "право",
"clean": "чистo",
"sampleRate": "частота дискретизації",
"save": "зберегти",
"saveAndReplace": "зберегти та замінити",
"saveAs": "зберегти як",
"search": "пошук",
"setting_one": "налаштування",
"setting_few": "налаштування",
"setting_many": "налаштувань",
"slower": "повільніше",
"share": "поділитися",
"size": "розмір",
"sort": "впорядкувати",
"sortOrder": "порядок",
"tags": "теги",
"title": "назва",
"trackNumber": "трек",
"trackGain": "підсилення треку",
"trackPeak": "піковий рівень треку",
"translation": "переклад",
"unknown": "невідомий",
"version": "версія",
"year": "рік",
"yes": "так",
"explicit": "Експліцитний зміст",
"gridRows": "рядки сітки",
"tableColumns": "стовпці таблиці",
"itemsMore": "{{count}} більше"
},
"entity": {
"album_one": "альбом",
"album_few": "альбоми",
"album_many": "альбомів",
"albumArtist_one": "виконавець альбому",
"albumArtist_few": "виконавці альбому",
"albumArtist_many": "виконавців альбому",
"albumArtistCount_one": "{{count}} виконавець альбому",
"albumArtistCount_few": "{{count}} виконавці альбому",
"albumArtistCount_many": "{{count}} виконавців альбому",
"albumWithCount_one": "{{count}} альбом",
"albumWithCount_few": "{{count}} альбоми",
"albumWithCount_many": "{{count}} альбомів",
"radioStation_one": "радіостанція",
"radioStation_few": "радіостанції",
"radioStation_many": "радіостанцій",
"radioStationWithCount_one": "{{count}} радіостанція",
"radioStationWithCount_few": "{{count}} радіостанції",
"radioStationWithCount_many": "{{count}} радіостанцій",
"artist_one": "виконавець",
"artist_few": "виконавці",
"artist_many": "виконавців",
"artistWithCount_one": "{{count}} виконавець",
"artistWithCount_few": "{{count}} виконавці",
"artistWithCount_many": "{{count}} виконавців",
"favorite_one": "улюблений",
"favorite_few": "улюблені",
"favorite_many": "улюблених",
"folder_one": "папка",
"folder_few": "папки",
"folder_many": "папок",
"folderWithCount_one": "{{count}} папка",
"folderWithCount_few": "{{count}} папки",
"folderWithCount_many": "{{count}} папок",
"genre_one": "жанр",
"genre_few": "жанри",
"genre_many": "жанрів",
"genreWithCount_one": "{{count}} жанр",
"genreWithCount_few": "{{count}} жанри",
"genreWithCount_many": "{{count}} жанрів",
"playlist_one": "плейлист",
"playlist_few": "плейлисти",
"playlist_many": "плейлистів",
"play_one": "{{count}} відтворення",
"play_few": "{{count}} відтворення",
"play_many": "{{count}} відтворень",
"playlistWithCount_one": "{{count}} плейлист",
"playlistWithCount_few": "{{count}} плейлисти",
"playlistWithCount_many": "{{count}} плейлистів",
"smartPlaylist": "розумний $t(entity.playlist, {\"count\": 1})",
"track_one": "трек",
"track_few": "треки",
"track_many": "треків",
"song_one": "пісня",
"song_few": "пісні",
"song_many": "пісень",
"trackWithCount_one": "{{count}} трек",
"trackWithCount_few": "{{count}} треки",
"trackWithCount_many": "{{count}} треків"
},
"error": {
"apiRouteError": "неможливо виконати запит",
"audioDeviceFetchError": "сталася помилка під час спроби отримати аудіопристрої",
"authenticationFailed": "аутентифікація не вдалася",
"badAlbum": "ви бачите цю сторінку, тому що ця пісня не входить до альбому. найімовірніше, ця проблема виникає, якщо у верхньому рівні вашої музичної папки знаходиться пісня. Jellyfin групує треки тільки в тому випадку, якщо вони знаходяться в папці",
"badValue": "недійсний параметр \"{{value}}\". це значення більше не існує",
"credentialsRequired": "необхідні дані для входу",
"endpointNotImplementedError": "кінцева точка {{endpoint}} не реалізована для {{serverType}}",
"genericError": "сталася помилка",
"invalidServer": "недійсний сервер",
"localFontAccessDenied": "відмова в доступі до локальних шрифтів",
"loginRateError": "занадто багато спроб входу, спробуйте ще раз через кілька секунд",
"mpvRequired": "необхідний MPV",
"multipleServerSaveQueueError": "у черзі відтворення є одна або кілька пісень, які не належать до поточного сервера. це не підтримується",
"networkError": "сталася мережева помилка",
"noNetwork": "сервер недоступний",
"noNetworkDescription": "не вдалося підключитися до цього сервера",
"notificationDenied": "дозвіл на сповіщення було відхилено. це налаштування не має впливу",
"openError": "не вдалося відкрити файл",
"playbackError": "сталася помилка під час спроби відтворити медіафайл",
"remoteDisableError": "сталася помилка під час спроби $t(common.disable) віддаленого сервера",
"remoteEnableError": "сталася помилка під час спроби $t(common.enable) віддаленого сервера",
"remotePortError": "сталася помилка під час спроби налаштувати порт віддаленого сервера",
"remotePortWarning": "перезапустіть сервер щоб застосувати новий порт",
"saveQueueFailed": "не вдалося зберегти чергу",
"serverNotSelectedError": "не вибрано жодного сервера",
"serverRequired": "потрібен сервер",
"sessionExpiredError": "ваша сесія закінчилася",
"systemFontError": "сталася помилка під час спроби отримати системні шрифти",
"settingsSyncError": "виявлено розбіжності між налаштуваннями в рендерері та основним процесом. перезапустіть програму, щоб застосувати зміни"
},
"filter": {
"album": "$t(entity.album, {\"count\": 1})",
"albumArtist": "$t(entity.albumArtist, {\"count\": 1})",
"albumCount": "кількість $t(entity.album, {\"count\": 2})",
"artist": "$t(entity.artist, {\"count\": 1})",
"biography": "біографія",
"bitrate": "бітрейт",
"bpm": "уд/хв",
"channels": "$t(common.channel, {\"count\": 2})",
"comment": "коментар",
"communityRating": "рейтинг спільноти",
"criticRating": "рейтинг критиків",
"dateAdded": "дата додавання",
"disc": "диск",
"duration": "тривалість",
"favorited": "улюблене",
"fromYear": "з року",
"genre": "$t(entity.genre, {\"count\": 1})",
"id": "id",
"isCompilation": "є компіляцією",
"isFavorited": "є улюбленим",
"isPublic": "є публічним",
"isRated": "є оціненим",
"isRecentlyPlayed": "нещодавно відтворено",
"lastPlayed": "нещодавно відтворені",
"mostPlayed": "найбільш відтворювані",
"name": "назва",
"note": "примітка",
"owner": "$t(common.owner)",
"path": "шлях",
"playCount": "кількість відтворень",
"random": "випадково",
"rating": "рейтинг",
"recentlyAdded": "нещодавно додано",
"recentlyPlayed": "нещодавно відтворено",
"recentlyUpdated": "нещодавно оновлено",
"releaseDate": "дата випуску",
"releaseYear": "рік випуску",
"search": "шукати",
"songCount": "кількість пісень",
"sortName": "сортування за назвою",
"title": "назва",
"toYear": "до року",
"trackNumber": "трек",
"explicitStatus": "$t(common.explicitStatus)"
},
"datetime": {
"minuteShort": "хв.",
"secondShort": "сек.",
"hourShort": "год",
"dayShort": "дн."
},
"filterOperator": {
"after": "є після",
"afterDate": "після (дата)",
"before": "є перед",
"beforeDate": "є перед (дата)",
"contains": "містить",
"endsWith": "закінчується на",
"inPlaylist": "є в",
"inTheLast": "є в останньому",
"inTheRange": "є в межах",
"inTheRangeDate": "є в межах (дата)",
"is": "є",
"isNot": "не є",
"isGreaterThan": "більше ніж",
"isLessThan": "менше ніж",
"matchesRegex": "відповідає регулярному виразу",
"notContains": "не містить",
"notInPlaylist": "немає в",
"notInTheLast": "не є в останньому",
"startsWith": "починається з"
},
"form": {
"addServer": {
"error_savePassword": "сталася помилка під час спроби зберегти пароль",
"ignoreCors": "ігнорувати cors ($t(common.restartRequired))",
"ignoreSsl": "ігнорувати ssl ($t(common.restartRequired)}",
"input_legacyAuthentication": "увімкнути застарілу автентифікацію",
"input_name": "назва сервера",
"input_password": "пароль",
"input_preferInstantMix": "віддавати перевагу миттєвому міксу",
"input_preferInstantMixDescription": "використовувати тільки миттєвий мікс щоб отримати подібні пісні. корисно, коли у вас є плагіни, які змінюють цю поведінку",
"input_preferRemoteUrl": "віддавати перевагу публічній URL-адресі",
"input_remoteUrl": "публічна URL-адреса",
"input_remoteUrlPlaceholder": "опціонально: публічна URL-адреса для зовнішніх функцій",
"input_savePassword": "зберегти пароль",
"input_url": "URL-адреса",
"input_username": "Ім'я користувача",
"success": "сервер додано успішно",
"title": "додати сервер"
},
"largeFetchConfirmation": {
"title": "додати елементи до черги",
"description": "Ця дія додасть усі елементи в поточний відфільтрований перегляд"
},
"addToPlaylist": {
"create": "створити $t(entity.playlist, {\"count\": 1}) {{playlist}}",
"input_playlists": "$t(entity.playlist, {\"count\": 2})",
"input_skipDuplicates": "пропустити дублікати",
"searchOrCreate": "шукайте $t(entity.playlist, {\"count\": 2}) або пишіть, щоб створити новий",
"success": "додано $t(entity.trackWithCount, {\"count\": {{message}} }) до $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
"title": "додати до $t(entity.playlist, {\"count\": 1})"
},
"createPlaylist": {
"input_description": "$t(common.description)",
"input_name": "$t(common.name)",
"input_owner": "$t(common.owner)",
"input_public": "публічний",
"success": "$t(entity.playlist, {\"count\": 1}) стрворено успішно",
"title": "створити $t(entity.playlist, {\"count\": 1})"
},
"createRadioStation": {
"success": "радіостанція створена успішно",
"title": "створити радіостанцію",
"input_homepageUrl": "адреса домашньої сторінки",
"input_name": "назва",
"input_streamUrl": "URL-адреса потоку"
},
"deletePlaylist": {
"input_confirm": "введіть ім'я $t(entity.playlist, {\"count\": 1}) для підтвердження",
"success": "$t(entity.playlist, {\"count\": 1}) успішно видалено",
"title": "видалити $t(entity.playlist, {\"count\": 1})"
},
"editPlaylist": {
"publicJellyfinNote": "Jellyfin з якоїсь причини не показує, чи є плейлист публічним чи ні. Якщо ви хочете, щоб він залишався публічним, виберіть варіант нижче",
"editNote": "ручне редагування не рекомендується для великих плейлистів. ви впевнені, що готові прийняти ризик втрати даних, який виникає при перезапису існуючого плейлисту?",
"success": "$t(entity.playlist, {\"count\": 1}) успішно оновлено",
"title": "змінити $t(entity.playlist, {\"count\": 1})"
},
"lyricsExport": {
"export": "експортувати тексти пісень",
"input_synced": "експортувати синхронізовані тексти пісень",
"input_offset": "$t(setting.lyricOffset)"
},
"lyricSearch": {
"input_artist": "$t(entity.artist, {\"count\": 1})",
"input_name": "$t(common.name)",
"title": "шукати тексти пісень"
},
"queryEditor": {
"title": "редактор запитів",
"input_optionMatchAll": "збіг за всіма",
"input_optionMatchAny": "збіг за будь-яким",
"addRuleGroup": "додати групу правил",
"removeRuleGroup": "видалити групу правил",
"resetToDefault": "скинути до заводських налаштувань",
"clearFilters": "очистити фільтри"
},
"saveQueue": {
"success": "черга відтворення збережена на сервері"
},
"shareItem": {
"allowDownloading": "дозволити завантаження",
"description": "опис",
"setExpiration": "встановити термін дії",
"success": "посилання для спільного використання скопійовано в буфер обміну (натисніть тут, щоб відкрити)",
"expireInvalid": "термін дії повинен бути в майбутньому",
"createFailed": "не вдалося створити спільний доступ (чи ввімкнено спільний доступ?)"
},
"shuffleAll": {
"title": "відтворити випадково",
"input_genre": "$t(entity.genre, {\"count\": 1})",
"input_limit": "скільки пісень?",
"input_minYear": "від року",
"input_maxYear": "до року",
"input_played": "відтворити фільтр",
"input_played_optionAll": "всі треки",
"input_played_optionUnplayed": "тільки не відтворені треки",
"input_played_optionPlayed": "тільки відтворені треки"
},
"updateServer": {
"success": "сервер успішно оновлено",
"title": "оновити сервер"
},
"privateMode": {
"enabled": "приватний режим увімкнено, стан відтворення тепер приховано від зовнішніх інтеграцій",
"disabled": "приватний режим вимкнено, стан відтворення тепер видно для увімкнених зовнішніх інтеграцій",
"title": "приватний режим"
}
},
"player": {
"skip": "пропустити"
},
"page": {
"albumArtistDetail": {
"about": "Про {{artist}}",
"appearsOn": "з'являється на",
"favoriteSongs": "улюблені пісні",
"groupingTypeAll": "всі типи випуску",
"groupingTypePrimary": "основні типи випуску",
"recentReleases": "останні випуски",
"viewDiscography": "переглянути дискографію",
"relatedArtists": "подібні $t(entity.artist, {\"count\": 2})",
"topSongs": "найкращі пісні",
"topSongsCommunity": "спільнота",
"topSongsFrom": "найкращі пісні від {{title}}",
"topSongsPersonal": "особисте",
"favoriteSongsFrom": "улюблені пісні від {{title}}",
"viewAll": "показати все",
"viewAllTracks": "показати усі $t(entity.track, {\"count\": 2})"
},
"albumArtistList": {
"title": "$t(entity.albumArtist, {\"count\": 2})"
},
"albumDetail": {
"moreFromArtist": "більше від цього $t(entity.artist, {\"count\": 1})",
"moreFromGeneric": "більше від {{item}}",
"released": "видано"
},
"albumList": {
"artistAlbums": "альбоми виконавця {{artist}}",
"genreAlbums": "\"{{genre}}\" $t(entity.album, {\"count\": 2})",
"title": "$t(entity.album, {\"count\": 2})"
},
"radioList": {
"title": "радіостанції"
},
"releasenotes": {
"commitsSinceStable": "комміти від {{stable}}",
"noNewCommits": "немає нових коммітів у цьому періоді",
"noStableReleaseToCompare": "немає доступної стабільної версії для порівняння"
},
"favorites": {
"title": "$t(entity.favorite, {\"count\": 2})"
},
"windowBar": {
"paused": "(Призупинено) ",
"privateMode": "(Приватний режим)"
},
"appMenu": {
"collapseSidebar": "згорнути бічну панель",
"commandPalette": "відкрити палітру команд",
"expandSidebar": "розгорнути бічну панель",
"goBack": "повернутися назад",
"goForward": "перейти вперед",
"manageServers": "управління серверами",
"privateModeOff": "вимкнути приватний режим",
"privateModeOn": "увімкнути приватний режим",
"openBrowserDevtools": "відкрити інструменти розробника",
"quit": "$t(common.quit)",
"selectServer": "вибрати сервер",
"selectMusicFolder": "вибрати папку з музикою",
"noMusicFolder": "не вибрано папку з музикою",
"multipleMusicFolders": "Вибрано {{count}} папок з музикою",
"settings": "$t(common.setting, {\"count\": 2})",
"version": "версія {{version}}"
},
"manageServers": {
"title": "управління серверами",
"serverDetails": "інформація про сервер",
"url": "URL-адреса",
"username": "Ім'я користувача",
"editServerDetailsTooltip": "редагувати дані сервера",
"removeServer": "видалити сервер"
},
"contextMenu": {
"addFavorite": "$t(action.addToFavorites)",
"addLast": "$t(player.addLast)",
"addNext": "$t(player.addNext)",
"addToFavorites": "$t(action.addToFavorites)",
"addToPlaylist": "$t(action.addToPlaylist)",
"createPlaylist": "$t(action.createPlaylist)",
"deletePlaylist": "$t(action.deletePlaylist)",
"deselectAll": "$t(action.deselectAll)",
"download": "завантажити",
"moveItems": "$t(action.moveItems)",
"moveToNext": "$t(action.moveToNext)",
"moveToBottom": "$t(action.moveToBottom)",
"moveToTop": "$t(action.moveToTop)",
"numberSelected": "{{count}} вибрано",
"play": "$t(player.play)",
"playSimilarSongs": "$t(player.playSimilarSongs)",
"removeFromFavorites": "$t(action.removeFromFavorites)",
"removeFromPlaylist": "$t(action.removeFromPlaylist)",
"removeFromQueue": "$t(action.removeFromQueue)",
"setRating": "$t(action.setRating)",
"playShuffled": "$t(player.shuffle)",
"shareItem": "поділитися елементом",
"goTo": "перейти до",
"goToAlbum": "перейти до $t(entity.album, {\"count\": 1})",
"goToAlbumArtist": "перейти до $t(entity.albumArtist, {\"count\": 1})",
"showDetails": "отримати інформацію"
}
"addToFavorites": "додати до $t(entity.favorite, {\"count\": 2})"
}
}
+2 -1
View File
@@ -229,6 +229,7 @@
"audioPlayer_description": "选择用于播放的音频播放器",
"globalMediaHotkeys": "全局媒体快捷键",
"gaplessAudio_description": "调整 mpv 无缝音频设置",
"disableAutomaticUpdates": "禁用自动更新",
"followLyric_description": "滚动歌词到当前播放位置",
"audioExclusiveMode": "音频独占模式",
"font": "字体",
@@ -463,7 +464,7 @@
"releaseChannel_optionLatest": "最新的",
"releaseChannel_optionBeta": "测试版",
"releaseChannel": "发布通道",
"releaseChannel_description": "选择稳定版测试版或 Alpha(夜间构建版)以启用自动更新",
"releaseChannel_description": "选择稳定版本或测试版以进行自动更新",
"mediaSession": "启用媒体会话",
"mediaSession_description": "启用媒体会话集成,在系统音量叠加层和锁屏界面显示媒体控件和元数据",
"exportImportSettings_control_description": "通过 JSON 导出和导入设置",
+18 -143
View File
@@ -296,8 +296,7 @@
"myLibrary": "我的媒體庫",
"shared": "已分享 $t(entity.playlist, {\"count\": 2})",
"favorites": "$t(entity.favorite, {\"count\": 2})",
"radio": "$t(entity.radioStation, {\"count\": 2})",
"collections": "收藏"
"radio": "$t(entity.radioStation, {\"count\": 2})"
},
"trackList": {
"title": "$t(entity.track, {\"count\": 2})",
@@ -315,11 +314,7 @@
"viewAll": "檢視所有",
"viewAllTracks": "檢視所有$t(entity.track, {\"count\": 2})",
"groupingTypeAll": "所有發佈類型",
"groupingTypePrimary": "主要發佈類型",
"favoriteSongs": "最愛歌曲",
"favoriteSongsFrom": "{{title}} 的最愛歌曲",
"topSongsCommunity": "社群",
"topSongsPersonal": "個人"
"groupingTypePrimary": "主要發佈類型"
},
"manageServers": {
"title": "管理伺服器",
@@ -347,17 +342,7 @@
"title": "電台"
},
"windowBar": {
"paused": "(暫停) ",
"privateMode": "(私人模式)"
},
"collections": {
"overrideExisting": "複寫現有的",
"saveAsCollection": "儲存為收藏"
},
"releasenotes": {
"commitsSinceStable": "提交自 {{stable}}",
"noNewCommits": "在此區間內沒有新的提交",
"noStableReleaseToCompare": "沒有穩定的發行可供比較"
"paused": "(暫停) "
}
},
"player": {
@@ -399,16 +384,7 @@
"restoreQueueFromServer": "從伺服器還原播放佇列",
"saveQueueToServer": "將播放佇列儲存至伺服器",
"artistRadio": "藝人電台",
"trackRadio": "曲目電台",
"sleepTimer": "睡眠定時器",
"sleepTimer_endOfSong": "歌曲播完時",
"sleepTimer_minutes": "{{count}} 分鐘",
"sleepTimer_hours": "{{count}} 小時",
"sleepTimer_custom": "自訂",
"sleepTimer_off": "關閉",
"sleepTimer_timeRemaining": "剩餘 {{time}}",
"sleepTimer_setCustom": "設定定時器",
"sleepTimer_cancel": "取消定時器"
"trackRadio": "曲目電台"
},
"setting": {
"audioPlayer_description": "選擇用於播放的音訊播放器",
@@ -422,7 +398,7 @@
"accentColor": "強調色",
"accentColor_description": "設定應用程式的強調色",
"applicationHotkeys": "應用程式快捷鍵",
"applicationHotkeys_description": "設定應用程式快捷鍵。切換勾選框來設為全快捷鍵(僅桌面端)",
"applicationHotkeys_description": "設定應用程式快捷鍵。切換勾選框來設為全快捷鍵(僅桌面端)",
"audioDevice": "音訊設備",
"audioDevice_description": "選擇用於播放的音訊設備(僅 web 播放器)",
"audioExclusiveMode": "音訊獨占模式",
@@ -433,6 +409,7 @@
"crossfadeStyle_description": "選擇用於音訊播放器的淡入淡出風格",
"customFontPath": "自定字體路徑",
"customFontPath_description": "設定應用程式使用的自定字體路徑",
"disableAutomaticUpdates": "禁用自動更新",
"disableLibraryUpdateOnStartup": "禁用啟動時檢查新版本",
"discordApplicationId": "{{discord}} 應用程式 id",
"discordApplicationId_description": "{{discord}} rich presence 應用程式 id(預設為 {{defaultId}}",
@@ -454,10 +431,10 @@
"gaplessAudio": "無間隔音訊",
"gaplessAudio_description": "調整 mpv 無間隔音訊設定",
"gaplessAudio_optionWeak": "弱(建議)",
"globalMediaHotkeys": "全媒體快捷鍵",
"globalMediaHotkeys": "全媒體快捷鍵",
"hotkey_browserForward": "瀏覽器往前",
"hotkey_favoritePreviousSong": "收藏 $t(common.previousSong)",
"hotkey_globalSearch": "全搜尋",
"hotkey_globalSearch": "全搜尋",
"hotkey_localSearch": "頁面內搜尋",
"hotkey_playbackNext": "下一首",
"hotkey_playbackPause": "暫停",
@@ -581,7 +558,7 @@
"contextMenu_description": "允許您隱藏在右鍵選單項目時顯示的項目。未選取的項目將被隱藏",
"customCssEnable": "啟用自訂CSS",
"customCssEnable_description": "允許撰寫自訂CSS",
"customCssNotice": "警告:即使已限制某些用法(不允許 url() 和 content:),但使用自訂 CSS 仍然會透過更改介面帶來風險",
"customCssNotice": "警告:雖然有一些清理措施(不允許 url() 和 content:),但使用自訂 CSS 仍然會透過更改介面帶來風險",
"customCss": "自訂CSS",
"customCss_description": "自訂 CSS 內容。注意:內容和遠端 URL 是不允許使用的屬性。您的內容預覽如下所示。由於需要進行清理,因此存在一些您未設定的其他欄位",
"discordPausedStatus": "暫停時顯示 rich presence",
@@ -641,7 +618,7 @@
"artistBackgroundBlur_description": "調整套用至藝人背景圖片的模糊程度",
"releaseChannel_optionLatest": "最新版本",
"releaseChannel_optionBeta": "測試版",
"releaseChannel_description": "選擇自動更新時要使用穩定、測試或是 alpha (每日建構版) 版本",
"releaseChannel_description": "選擇自動更新時要使用穩定版本或是測試版本",
"discordDisplayType": "{{discord}} presence 顯示類型",
"discordDisplayType_description": "變更您在狀態中正在聆聽的內容",
"discordDisplayType_songname": "歌曲名稱",
@@ -653,8 +630,8 @@
"hotkey_navigateHome": "導航至首頁",
"preventSleepOnPlayback": "防止播放時進入睡眠狀態",
"preventSleepOnPlayback_description": "在音樂播放時防止螢幕進入睡眠狀態",
"mediaSession": "啟用 Media Session",
"mediaSession_description": "啟用 Media Session 整合功能,於系統音量 Overlay 和鎖定畫面中顯示媒體資料與控制面板",
"mediaSession": "啟用Media Session",
"mediaSession_description": "啟用 Media Session 整合功能,於系統音量Overlay和鎖定畫面中顯示媒體資料與控制面板",
"releaseChannel": "發佈通道",
"analyticsDisable": "選擇退出使用情況分析",
"analyticsDisable_description": "經過匿名處理的使用情況資料將傳送給開發者,以協助改進應用程式",
@@ -738,30 +715,7 @@
"pathReplace_description": "替換您伺服器的預設檔案路徑",
"pathReplace_optionRemovePrefix": "移除前綴",
"pathReplace_optionAddPrefix": "增加前綴",
"sidebarPlaylistSorting": "側邊欄播放清單排序",
"homeFeatureStyle_description": "控制首頁輪播的樣式",
"homeFeatureStyle": "首頁特色輪播樣式",
"homeFeatureStyle_optionMultiple": "多重",
"homeFeatureStyle_optionSingle": "單一",
"hotkey_listPlayDefault": "清單播放",
"hotkey_listPlayLast": "清單尾端播放",
"hotkey_listPlayNext": "清單下一項播放",
"hotkey_listPlayNow": "清單立即播放",
"enableGridMultiSelect": "啟用網格多選",
"enableGridMultiSelect_description": "啟用時,允許在網格檢視中選擇多項。停用時,單擊網格項目圖片將導航到項目頁面",
"sidebarPlaylistSorting_description": "允許在側邊欄中使用拖放手動對播放清單進行排序,而不是預設的伺服器排序",
"sidebarPlaylistListFilterRegex_description": "在側邊欄中隱藏與此正規表達式匹配的播放清單",
"sidebarPlaylistListFilterRegex_placeholder": "範例: ^Daily Mix.*",
"sidebarPlaylistListFilterRegex": "播放清單過濾器正規表達式",
"blurExplicitImages": "模糊露骨圖片",
"blurExplicitImages_description": "標記為露骨的專輯和歌曲封面將被模糊",
"releaseChannel_optionAlpha": "alpha (每日建構版)",
"analyticsEnable": "傳送基於使用情況的分析報告",
"analyticsEnable_description": "匿名化的使用情況資料會傳送給開發者,以協助改進應用程式",
"automaticUpdates": "自動更新",
"automaticUpdates_description": "自動檢查並安裝更新",
"discordStateIcon": "顯示播放中圖示",
"discordStateIcon_description": "在 rich presence 狀態中顯示一個小的播放圖示。啟用「暫停時顯示 rich presence」時,會始終顯示暫停的圖示"
"sidebarPlaylistSorting": "側邊欄播放清單排序"
},
"table": {
"config": {
@@ -794,8 +748,7 @@
"alternateRowColors": "隔行上色",
"horizontalBorders": "行邊框線",
"rowHoverHighlight": "滑鼠懸停Highlight",
"verticalBorders": "列邊框線",
"showHeader": "顯示標題"
"verticalBorders": "列邊框線"
},
"label": {
"actions": "$t(common.action, {\"count\": 2})",
@@ -830,15 +783,12 @@
"genreBadge": "$t(entity.genre, {\"count\": 1}) (徽章)",
"image": "圖片",
"bitDepth": "$t(common.bitDepth)",
"sampleRate": "$t(common.sampleRate)",
"composer": "作曲者",
"titleArtist": "$t(common.title) (藝人)"
"sampleRate": "$t(common.sampleRate)"
},
"view": {
"table": "表格",
"grid": "網格",
"list": "列表",
"detail": "詳情"
"list": "列表"
}
},
"column": {
@@ -976,8 +926,7 @@
"title": "標題",
"toYear": "從年份",
"trackNumber": "曲目",
"explicitStatus": "$t(common.explicitStatus)",
"sortName": "排序名稱"
"explicitStatus": "$t(common.explicitStatus)"
},
"form": {
"addServer": {
@@ -1219,80 +1168,6 @@
"gravity": "重力",
"peakFadeTime": "峰值淡出時間 (毫秒)",
"peakHoldTime": "峰值停留時間 (毫秒)",
"radialSpectrum": "圓形頻譜",
"level": "層級",
"pasteGradient": "貼上漸層",
"pasteGradientPlaceholder": "在這裡貼上漸層JSON...",
"radial": "放射",
"radialInvert": "反轉放射",
"spinSpeed": "旋轉速度",
"radius": "半徑",
"reflexMirror": "反射鏡像",
"reflexFit": "反射貼齊",
"reflexRatio": "反射比例",
"reflexAlpha": "反射 Alpha",
"reflexBrightness": "反射亮度",
"mirror": "鏡像",
"miscellaneousSettings": "雜項設定",
"alphaBars": "Alpha 條",
"ansiBands": "ASNI 波段",
"ledBars": "LED 條",
"trueLeds": "真實 LED",
"lumiBars": "輝光條",
"outlineBars": "外框條",
"roundBars": "圓角條",
"lowResolution": "低解析",
"splitGradient": "分割漸層",
"showFPS": "顯示 FPS",
"showScaleX": "顯示 X 軸比例",
"noteLabels": "音符標籤",
"showScaleY": "顯示Y軸比例",
"options": {
"mode": {
"0": "[0] 離散頻率",
"1": "[1] 1/24th 八度音 / 240 頻段",
"2": "[2] 1/12th 八度音 / 120 頻段",
"3": "[3] 1/8th 八度音 / 80 頻段",
"4": "[4] 1/6th 八度音 / 60 頻段",
"5": "[5] 1/4th 八度音 / 40 頻段",
"6": "[6] 1/3rd 八度音 / 30 頻段",
"7": "[7] 一半八度音 / 20 頻段",
"8": "[8] 完整八度音 / 10 頻段",
"10": "[10] 線 / 區域圖表"
},
"colorMode": {
"gradient": "梯度",
"barIndex": "條-指數",
"barLevel": "條-高度"
},
"gradient": {
"classic": "經典",
"prism": "菱鏡",
"rainbow": "彩虹",
"steelblue": "鋼藍",
"orangered": "橙紅色"
},
"channelLayout": {
"single": "單一",
"dualCombined": "雙重-合併",
"dualHorizontal": "雙重-水平",
"dualVertical": "雙重-垂直"
},
"frequencyScale": {
"none": "無",
"bark": "比例刻度",
"linear": "線性比例",
"log": "Log 比例",
"mel": "Mel 比例"
},
"weightingFilter": {
"none": "無",
"a": "A",
"b": "B",
"c": "C",
"d": "D",
"z": "Z"
}
}
"radialSpectrum": "圓形頻譜"
}
}
+4 -4
View File
@@ -1,8 +1,8 @@
import { createSocket } from 'dgram';
import { ipcMain } from 'electron';
import { mainLogger } from '/@/main/logger';
import { DiscoveredServerItem, ServerType } from '/@/shared/types/types';
import { ServerType } from '/@/shared/types/domain-types';
import { DiscoveredServerItem } from '/@/shared/types/types';
type JellyfinResponse = {
Address: string;
@@ -27,7 +27,7 @@ function discoverJellyfin(reply: (server: DiscoveredServerItem) => void) {
});
} catch (e) {
// Got a spurious response, ignore?
mainLogger.error('Autodiscover Jellyfin parse error', e);
console.error(e);
}
});
@@ -52,5 +52,5 @@ ipcMain.on('autodiscover-ping', (ev) => {
discoverAll((result) => port.postMessage(result))
.then(() => port.close())
.catch((err) => mainLogger.error('Autodiscover failed', err));
.catch((err) => console.error(err));
});
+1
View File
@@ -4,3 +4,4 @@ import './player';
import './remote';
import './settings';
import './discord-rpc';
import './youtube';
+3 -4
View File
@@ -7,7 +7,6 @@ import {
LyricSearchQuery,
LyricSource,
} from '.';
import { mainLogger } from '../../../logger';
import { orderSearchResults } from './shared';
const SEARCH_URL = 'https://genius.com/api/search/song';
@@ -101,7 +100,7 @@ export async function getLyricsBySongId(url: string): Promise<null | string> {
try {
result = await axios.get<string>(url, { responseType: 'text' });
} catch (e) {
mainLogger.error('Genius lyrics request failed', (e as Error)?.message);
console.error('Genius lyrics request got an error!', (e as Error)?.message);
return null;
}
@@ -139,7 +138,7 @@ export async function getSearchResults(
},
});
} catch (e) {
mainLogger.error('Genius search request failed', (e as Error)?.message);
console.error('Genius search request got an error!', (e as Error)?.message);
return null;
}
@@ -194,7 +193,7 @@ async function getSongId(
},
});
} catch (e) {
mainLogger.error('Genius search request failed', (e as Error)?.message);
console.error('Genius search request got an error!', (e as Error)?.message);
return null;
}
+2 -3
View File
@@ -1,6 +1,5 @@
import { ipcMain } from 'electron';
import { mainLogger } from '../../../logger';
import { store } from '../settings';
import { getLyricsBySongId as getGenius, getSearchResults as searchGenius } from './genius';
import { getLyricsBySongId as getLrcLib, getSearchResults as searchLrcLib } from './lrclib';
@@ -97,7 +96,7 @@ const searchAllSources = async (
allSearchResults.push(...result.value.searchResults);
} else if (result.status === 'rejected') {
const index = settled.indexOf(result);
mainLogger.error(`Error searching ${sources[index]} for lyrics`, result.reason);
console.error(`Error searching ${sources[index]} for lyrics:`, result.reason);
}
}
return allSearchResults;
@@ -161,7 +160,7 @@ const getRemoteLyrics = async (song: Song) => {
};
}
} catch (error) {
mainLogger.error(`Error fetching lyrics from ${bestMatch.source}`, error);
console.error(`Error fetching lyrics from ${bestMatch.source}:`, error);
}
if (lyricsFromSource) {
+3 -4
View File
@@ -7,7 +7,6 @@ import {
LyricSearchQuery,
LyricSource,
} from '.';
import { mainLogger } from '../../../logger';
import { orderSearchResults } from './shared';
const FETCH_URL = 'https://lrclib.net/api/get';
@@ -47,7 +46,7 @@ export async function getLyricsBySongId(songId: string): Promise<null | string>
try {
result = await axios.get<LrcLibTrackResponse>(`${FETCH_URL}/${songId}`);
} catch (e) {
mainLogger.error('LrcLib lyrics request failed', (e as Error)?.message);
console.error('LrcLib lyrics request got an error!', (e as Error)?.message);
return null;
}
@@ -70,7 +69,7 @@ export async function getSearchResults(
},
});
} catch (e) {
mainLogger.error('LrcLib search request failed', (e as Error)?.message);
console.error('LrcLib search request got an error!', (e as Error)?.message);
return null;
}
@@ -108,7 +107,7 @@ export async function query(
timeout: TIMEOUT_MS,
});
} catch (e) {
mainLogger.error('LrcLib search request failed', (e as Error).message);
console.error('LrcLib search request got an error!', (e as Error).message);
return null;
}
+2 -3
View File
@@ -6,7 +6,6 @@ import {
LyricSearchQuery,
LyricSource,
} from '.';
import { mainLogger } from '../../../logger';
import { store } from '../settings';
import { orderSearchResults } from './shared';
@@ -82,7 +81,7 @@ export async function getLyricsBySongId(songId: string): Promise<null | string>
},
});
} catch (e) {
mainLogger.error('NetEase lyrics request failed', e);
console.error('NetEase lyrics request got an error!', e);
return null;
}
const enableTranslation = store.get('enableNeteaseTranslation', false) as boolean;
@@ -115,7 +114,7 @@ export async function getSearchResults(
},
});
} catch (e) {
mainLogger.error('NetEase search request failed', e);
console.error('NetEase search request got an error!', e);
return null;
}
+4 -4
View File
@@ -1,3 +1,4 @@
import console from 'console';
import { app, ipcMain } from 'electron';
import { rm } from 'fs/promises';
import uniq from 'lodash/uniq';
@@ -6,7 +7,6 @@ import { pid } from 'node:process';
import process from 'process';
import { getMainWindow, sendToastToRenderer } from '../../../index';
import { mainLogger } from '../../../logger';
import { createLog, isWindows } from '../../../utils';
import { store } from '../settings';
@@ -109,7 +109,7 @@ const createMpv = async (data: {
try {
await mpv.start();
} catch (error: any) {
mainLogger.error('mpv failed to start', error);
console.error('mpv failed to start', error);
} finally {
await mpv.setMultipleProperties(properties || {});
}
@@ -672,7 +672,7 @@ process.on('SIGTERM', async () => {
// Handle uncaught exceptions - cleanup mpv before crashing
process.on('uncaughtException', async (error) => {
mainLogger.error('Uncaught exception', error);
console.error('Uncaught exception:', error);
await cleanupMpv(true).catch(() => {
// Ignore cleanup errors during crash
});
@@ -680,7 +680,7 @@ process.on('uncaughtException', async (error) => {
// Handle unhandled rejections - cleanup mpv
process.on('unhandledRejection', async (reason) => {
mainLogger.error('Unhandled rejection', reason);
console.error('Unhandled rejection:', reason);
await cleanupMpv(true).catch(() => {
// Ignore cleanup errors
});
+2 -3
View File
@@ -10,7 +10,6 @@ import { deflate, gzip } from 'zlib';
import manifest from './manifest.json';
import { getMainWindow } from '/@/main/index';
import { mainLogger } from '/@/main/logger';
import { isLinux } from '/@/main/utils';
import { QueueSong } from '/@/shared/types/domain-types';
import { ClientEvent, ServerEvent } from '/@/shared/types/remote-types';
@@ -350,7 +349,7 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
}, 10000) as unknown as number;
}
ws.on('error', (err) => mainLogger.error('Remote WebSocket error', err));
ws.on('error', console.error);
ws.on('message', (data) => {
try {
@@ -489,7 +488,7 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
}
}
} catch (error) {
mainLogger.error('Remote message handler error', error);
console.error(error);
}
});
+1
View File
@@ -38,6 +38,7 @@ export const store = new Store<any>({
lyrics: ['NetEase', 'lrclib.net'],
mediaSession: false,
playbackType: 'web',
renderer_server_port: 38472,
should_prompt_accessibility: true,
shown_accessibility_warning: false,
window_enable_tray: true,
+18
View File
@@ -0,0 +1,18 @@
import { ipcMain } from 'electron';
import YTMusic from 'ytmusic-api';
let youtubeApi: InstanceType<typeof YTMusic> | null = null;
const getYoutubeApi = async (): Promise<InstanceType<typeof YTMusic>> => {
if (!youtubeApi) {
youtubeApi = new YTMusic();
await youtubeApi.initialize();
}
return youtubeApi;
};
ipcMain.handle('youtube-search', async (_event, query: string) => {
const api = await getYoutubeApi();
const results = await api.search(query);
return results;
});
+78 -134
View File
@@ -1,5 +1,3 @@
import type { UpdateCheckResult } from 'electron-updater';
import { is } from '@electron-toolkit/utils';
import {
app,
@@ -21,15 +19,14 @@ import {
import electronLocalShortcut from 'electron-localshortcut';
import log from 'electron-log/main';
import { AppImageUpdater, autoUpdater, MacUpdater, NsisUpdater } from 'electron-updater';
import express from 'express';
import { access, constants } from 'fs';
import path, { join } from 'path';
import semver from 'semver';
import packageJson from '../../package.json';
import { disableMediaKeys, enableMediaKeys } from './features/core/player/media-keys';
import { shutdownServer } from './features/core/remote';
import { store } from './features/core/settings';
import { mainLogger } from './logger';
import MenuBuilder from './menu';
import {
autoUpdaterLogInterface,
@@ -56,25 +53,29 @@ const ALPHA_UPDATER_CONFIG: {
provider: 's3',
};
const GITHUB_UPDATER_CONFIG = {
owner: 'jeffvli',
provider: 'github' as const,
repo: 'feishin',
};
type UpdaterInstance = AppImageUpdater | MacUpdater | NsisUpdater | typeof autoUpdater;
class AlphaAppUpdater {
constructor() {
const updater = createAlphaUpdaterInstance();
log.transports.file.level = 'info';
updater.logger = autoUpdaterLogInterface;
updater.channel = ALPHA_UPDATER_CONFIG.channel;
updater.allowPrerelease = true;
updater.disableDifferentialDownload = true;
updater.allowDowngrade = true;
updater.autoInstallOnAppQuit = true;
updater.autoRunAppAfterInstall = true;
updater.checkForUpdatesAndNotify();
}
}
class AppUpdater {
constructor() {
const effectiveChannel = store.get('release_channel') as string;
mainLogger.info('Effective update channel:', effectiveChannel);
console.log('Effective update channel:', effectiveChannel);
if (effectiveChannel === 'alpha') {
checkAllChannelsAndGetBest().then(({ updater: updaterInstance }) => {
updaterInstance.autoInstallOnAppQuit = true;
updaterInstance.autoRunAppAfterInstall = true;
updaterInstance.checkForUpdatesAndNotify();
});
return;
return new AlphaAppUpdater();
}
configureAndGetUpdater();
@@ -82,87 +83,19 @@ class AppUpdater {
}
}
// When release channel is alpha, check alpha and latest for updates and return
// the updater + result for the newest version found (so alpha users can receive
// latest updates when they are newer than the current alpha).
async function checkAllChannelsAndGetBest(): Promise<{
result: null | UpdateCheckResult;
updater: UpdaterInstance;
}> {
const currentVersion = packageJson.version;
const candidates: Array<{
channel: 'alpha' | 'beta' | 'latest';
result: UpdateCheckResult;
updater: UpdaterInstance;
}> = [];
const alphaUpdater = createAlphaUpdaterInstance();
alphaUpdater.logger = autoUpdaterLogInterface;
alphaUpdater.channel = ALPHA_UPDATER_CONFIG.channel;
alphaUpdater.allowPrerelease = true;
alphaUpdater.disableDifferentialDownload = true;
alphaUpdater.allowDowngrade = true;
try {
mainLogger.info('Checking for updates on alpha channel');
const alphaResult = await alphaUpdater.checkForUpdates();
if (
alphaResult?.updateInfo?.version &&
alphaResult.isUpdateAvailable &&
semver.valid(alphaResult.updateInfo.version) &&
semver.gt(alphaResult.updateInfo.version, currentVersion)
) {
candidates.push({ channel: 'alpha', result: alphaResult, updater: alphaUpdater });
}
} catch (e) {
log.warn('Alpha channel check failed', e);
}
try {
autoUpdater.setFeedURL(GITHUB_UPDATER_CONFIG);
configureAutoUpdaterForChannel('latest');
mainLogger.info('Checking for updates on latest channel (GitHub)');
const latestResult = await autoUpdater.checkForUpdates();
if (
latestResult?.updateInfo?.version &&
latestResult.isUpdateAvailable &&
semver.valid(latestResult.updateInfo.version) &&
semver.gt(latestResult.updateInfo.version, currentVersion)
) {
candidates.push({ channel: 'latest', result: latestResult, updater: autoUpdater });
}
} catch (e) {
log.warn('Latest channel check failed', e);
}
if (candidates.length === 0) {
return { result: null, updater: alphaUpdater };
}
const best = candidates.reduce((a, b) =>
semver.gt(a.result.updateInfo.version, b.result.updateInfo.version) ? a : b,
);
if (best.channel === 'latest') {
configureAutoUpdaterForChannel('latest');
}
return { result: best.result, updater: best.updater };
}
function configureAndGetUpdater(): UpdaterInstance {
const isBetaVersion = packageJson.version.includes('-beta');
const isAlphaVersion = packageJson.version.includes('-alpha');
let releaseChannel = store.get('release_channel');
const isNotConfigured = !releaseChannel;
mainLogger.info('Release channel:', releaseChannel);
mainLogger.info('Is beta version:', isBetaVersion);
mainLogger.info('Is alpha version:', isAlphaVersion);
mainLogger.info('Is not configured:', isNotConfigured);
console.log('Release channel:', releaseChannel);
console.log('Is beta version:', isBetaVersion);
console.log('Is alpha version:', isAlphaVersion);
console.log('Is not configured:', isNotConfigured);
if (isNotConfigured) {
mainLogger.info('Release channel not configured, setting default channel');
console.log('Release channel not configured, setting default channel');
const defaultChannel = isAlphaVersion ? 'alpha' : isBetaVersion ? 'beta' : 'latest';
store.set('release_channel', defaultChannel);
releaseChannel = defaultChannel;
@@ -190,37 +123,17 @@ function configureAndGetUpdater(): UpdaterInstance {
if (effectiveChannel === 'beta') {
autoUpdater.channel = 'beta';
autoUpdater.allowDowngrade = true;
autoUpdater.allowPrerelease = true;
autoUpdater.disableDifferentialDownload = true;
} else {
autoUpdater.channel = 'latest';
autoUpdater.allowDowngrade = true;
autoUpdater.allowPrerelease = false;
}
return autoUpdater;
}
/**
* Configures the global autoUpdater for a specific GitHub channel (beta or latest).
* Used when checking multiple channels or when the winning channel is beta/latest.
*/
function configureAutoUpdaterForChannel(channel: 'beta' | 'latest'): void {
log.transports.file.level = 'info';
autoUpdater.logger = autoUpdaterLogInterface;
autoUpdater.autoInstallOnAppQuit = true;
autoUpdater.autoRunAppAfterInstall = true;
if (channel === 'beta') {
autoUpdater.channel = 'beta';
autoUpdater.allowDowngrade = true;
autoUpdater.allowPrerelease = true;
autoUpdater.disableDifferentialDownload = true;
} else {
autoUpdater.channel = 'latest';
autoUpdater.allowPrerelease = false;
}
}
function createAlphaUpdaterInstance(): AppImageUpdater | MacUpdater | NsisUpdater {
if (isMacOS()) {
return new MacUpdater(ALPHA_UPDATER_CONFIG);
@@ -236,7 +149,7 @@ function createAlphaUpdaterInstance(): AppImageUpdater | MacUpdater | NsisUpdate
protocol.registerSchemesAsPrivileged([{ privileges: { bypassCSP: true }, scheme: 'feishin' }]);
process.on('uncaughtException', (error: any) => {
mainLogger.error('Uncaught exception in main process', error);
console.error('Error in main process', error);
});
if (store.get('ignore_ssl')) {
@@ -306,6 +219,37 @@ const getAssetPath = (...paths: string[]): string => {
return path.join(RESOURCES_PATH, ...paths);
};
const DEFAULT_RENDERER_SERVER_PORT = 38472;
const getRendererServerPort = (): number => {
const port = Number(store.get('renderer_server_port', DEFAULT_RENDERER_SERVER_PORT));
if (!Number.isInteger(port) || port < 1024 || port > 65535) {
return DEFAULT_RENDERER_SERVER_PORT;
}
return port;
};
let rendererServerUrl: null | string = null;
let rendererHttpServer: null | ReturnType<express.Application['listen']> = null;
const startRendererServer = (): Promise<string> => {
return new Promise((resolve, reject) => {
if (rendererServerUrl) {
resolve(rendererServerUrl);
return;
}
const port = getRendererServerPort();
const rendererPath = join(__dirname, '../renderer');
const app = express();
app.use(express.static(rendererPath));
rendererHttpServer = app.listen(port, () => {
rendererServerUrl = `http://localhost:${port}`;
resolve(rendererServerUrl);
});
rendererHttpServer.on('error', reject);
});
};
export const getMainWindow = () => {
return mainWindow;
};
@@ -522,29 +466,18 @@ async function createWindow(first = true): Promise<void> {
'app-check-for-updates',
async (): Promise<{ updateAvailable: boolean; version?: string }> => {
if (disableAutoUpdates()) {
mainLogger.info('Auto updates are disabled');
console.log('Auto updates are disabled');
return { updateAvailable: false };
}
try {
mainLogger.info('Checking for updates');
const effectiveChannel = store.get('release_channel') as string;
let result: null | UpdateCheckResult;
let updater: UpdaterInstance;
if (effectiveChannel === 'alpha') {
const best = await checkAllChannelsAndGetBest();
result = best.result;
updater = best.updater;
} else {
updater = configureAndGetUpdater();
result = await updater.checkForUpdates();
}
console.log('Checking for updates');
const updater = configureAndGetUpdater();
const result = await updater.checkForUpdates();
const updateAvailable = result?.isUpdateAvailable ?? false;
mainLogger.info('Update available:', updateAvailable);
console.log('Update available:', updateAvailable);
if (updateAvailable && store.get('disable_auto_updates') !== true) {
mainLogger.info('Downloading update');
console.log('Downloading update');
updater.downloadUpdate();
}
@@ -553,7 +486,7 @@ async function createWindow(first = true): Promise<void> {
version: result?.updateInfo?.version,
};
} catch {
mainLogger.error('Error checking for updates');
console.log('Error checking for updates');
return { updateAvailable: false };
}
},
@@ -679,12 +612,11 @@ async function createWindow(first = true): Promise<void> {
return { action: 'deny' };
});
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
// HMR for renderer: use Vite dev server URL in development, otherwise the local HTTP server.
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']);
} else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'));
mainWindow.loadURL(rendererServerUrl!);
}
}
@@ -838,6 +770,14 @@ app.on('window-all-closed', () => {
}
});
app.on('before-quit', () => {
if (rendererHttpServer) {
rendererHttpServer.close();
rendererHttpServer = null;
rendererServerUrl = null;
}
});
const FONT_HEADERS = [
'font/collection',
'font/otf',
@@ -865,7 +805,7 @@ if (!singleInstance) {
});
app.whenReady()
.then(() => {
.then(async () => {
protocol.handle('feishin', async (request) => {
const filePath = `file://${request.url.slice('feishin://'.length)}`;
const response = await net.fetch(filePath);
@@ -883,6 +823,10 @@ if (!singleInstance) {
return response;
});
if (!(is.dev && process.env['ELECTRON_RENDERER_URL'])) {
await startRendererServer();
}
createWindow();
if (store.get('window_enable_tray', true)) {
createTray();
-36
View File
@@ -1,36 +0,0 @@
const pad = (n: number) => String(n).padStart(2, '0');
const timestamp = () => {
const d = new Date();
return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
};
const format = (level: string, message: string, ...args: unknown[]) => {
const prefix = `[${timestamp()}] [${level}] ${message}`;
if (args.length > 0) {
console.log(prefix, ...args);
} else {
console.log(prefix);
}
};
export const mainLogger = {
debug: (message: string, ...args: unknown[]) => format('DEBUG', message, ...args),
error: (message: string, ...args: unknown[]) => {
const prefix = `[${timestamp()}] [ERROR] ${message}`;
if (args.length > 0) {
console.error(prefix, ...args);
} else {
console.error(prefix);
}
},
info: (message: string, ...args: unknown[]) => format('INFO', message, ...args),
warn: (message: string, ...args: unknown[]) => {
const prefix = `[${timestamp()}] [WARN] ${message}`;
if (args.length > 0) {
console.warn(prefix, ...args);
} else {
console.warn(prefix);
}
},
};
+2 -18
View File
@@ -21,14 +21,6 @@ export default class MenuBuilder {
selector: 'orderFrontStandardAboutPanel:',
},
{ type: 'separator' },
{
accelerator: 'Command+,',
click: () => {
this.mainWindow.webContents.send('renderer-open-settings');
},
label: 'Settings',
},
{ type: 'separator' },
{ label: 'Services', submenu: [] },
{ type: 'separator' },
{
@@ -159,8 +151,8 @@ export default class MenuBuilder {
return [subMenuAbout, subMenuEdit, subMenuView, subMenuWindow, subMenuHelp];
}
buildDefaultTemplate(): MenuItemConstructorOptions[] {
const templateDefault: MenuItemConstructorOptions[] = [
buildDefaultTemplate() {
const templateDefault = [
{
label: '&File',
submenu: [
@@ -168,14 +160,6 @@ export default class MenuBuilder {
accelerator: 'Ctrl+O',
label: '&Open',
},
{
accelerator: 'Ctrl+,',
click: () => {
this.mainWindow.webContents.send('renderer-open-settings');
},
label: '&Settings...',
},
{ type: 'separator' },
{
accelerator: 'Ctrl+W',
click: () => {
+2
View File
@@ -11,6 +11,7 @@ import { mpris } from './mpris';
import { mpvPlayer, mpvPlayerListener } from './mpv-player';
import { remote } from './remote';
import { utils } from './utils';
import { youtube } from './youtube';
// Custom APIs for renderer
const api = {
@@ -25,6 +26,7 @@ const api = {
mpvPlayerListener,
remote,
utils,
youtube,
};
export type PreloadApi = typeof api;
-5
View File
@@ -61,10 +61,6 @@ const forceGarbageCollection = (): boolean => {
}
};
const rendererOpenSettings = (cb: (event: IpcRendererEvent) => void) => {
ipcRenderer.on('renderer-open-settings', cb);
};
export const utils = {
checkForUpdates,
disableAutoUpdates,
@@ -78,7 +74,6 @@ export const utils = {
openApplicationDirectory,
openItem,
playerErrorListener,
rendererOpenSettings,
};
export type Utils = typeof utils;
+11
View File
@@ -0,0 +1,11 @@
import { ipcRenderer } from 'electron';
const search = (query: string) => {
return ipcRenderer.invoke('youtube-search', query);
};
export const youtube = {
search,
};
export type Youtube = typeof youtube;
+87 -59
View File
@@ -4,6 +4,7 @@ import { immer } from 'zustand/middleware/immer';
import { createWithEqualityFn } from 'zustand/traditional';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { logMsg } from '/@/renderer/utils/logger-message';
import { toast } from '/@/shared/components/toast/toast';
import { ClientEvent, ServerEvent, SongUpdateSocket } from '/@/shared/types/remote-types';
@@ -41,7 +42,7 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
immer((set, get) => ({
actions: {
reconnect: async () => {
logFn.debug('Reconnect initiated', {
logFn.debug(logMsg[LogCategory.REMOTE].reconnectInitiated, {
category: LogCategory.REMOTE,
});
const existing = get().socket;
@@ -51,7 +52,7 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
existing.readyState === WebSocket.OPEN ||
existing.readyState === WebSocket.CONNECTING
) {
logFn.debug('Closing existing socket', {
logFn.debug(logMsg[LogCategory.REMOTE].closingExistingSocket, {
category: LogCategory.REMOTE,
meta: { readyState: existing.readyState },
});
@@ -63,17 +64,17 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
let authHeader: string | undefined;
try {
logFn.debug('Fetching credentials', {
logFn.debug(logMsg[LogCategory.REMOTE].fetchingCredentials, {
category: LogCategory.REMOTE,
});
const credentials = await fetch('/credentials');
authHeader = await credentials.text();
logFn.debug('Credentials fetched', {
logFn.debug(logMsg[LogCategory.REMOTE].credentialsFetched, {
category: LogCategory.REMOTE,
meta: { hasAuthHeader: !!authHeader },
});
} catch (error) {
logFn.error('Failed to get credentials', {
logFn.error(logMsg[LogCategory.REMOTE].failedToGetCredentials, {
category: LogCategory.REMOTE,
meta: { error },
});
@@ -81,7 +82,7 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
set((state) => {
const wsUrl = location.href.replace('http', 'ws');
logFn.debug('Creating new WebSocket', {
logFn.debug(logMsg[LogCategory.REMOTE].creatingWebSocket, {
category: LogCategory.REMOTE,
meta: { url: wsUrl },
});
@@ -92,28 +93,34 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
socket.addEventListener('message', (message) => {
const { data, event } = JSON.parse(message.data) as ServerEvent;
logFn.debug('WebSocket message received', {
logFn.debug(logMsg[LogCategory.REMOTE].webSocketMessageReceived, {
category: LogCategory.REMOTE,
meta: { data, event },
});
switch (event) {
case 'error': {
logFn.error('WebSocket error event', {
category: LogCategory.REMOTE,
meta: { data },
});
logFn.error(
logMsg[LogCategory.REMOTE].webSocketErrorEvent,
{
category: LogCategory.REMOTE,
meta: { data },
},
);
toast.error({ message: data, title: 'Socket error' });
break;
}
case 'favorite': {
logFn.debug('Favorite event received', {
category: LogCategory.REMOTE,
meta: {
favorite: data.favorite,
id: data.id,
logFn.debug(
logMsg[LogCategory.REMOTE].favoriteEventReceived,
{
category: LogCategory.REMOTE,
meta: {
favorite: data.favorite,
id: data.id,
},
},
});
);
set((state) => {
if (state.info.song?.id === data.id) {
state.info.song.userFavorite = data.favorite;
@@ -122,27 +129,33 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
break;
}
case 'playback': {
logFn.debug('Playback event received', {
category: LogCategory.REMOTE,
meta: { status: data },
});
logFn.debug(
logMsg[LogCategory.REMOTE].playbackEventReceived,
{
category: LogCategory.REMOTE,
meta: { status: data },
},
);
set((state) => {
state.info.status = data;
});
break;
}
case 'position': {
logFn.debug('Position event received', {
category: LogCategory.REMOTE,
meta: { position: data },
});
logFn.debug(
logMsg[LogCategory.REMOTE].positionEventReceived,
{
category: LogCategory.REMOTE,
meta: { position: data },
},
);
set((state) => {
state.info.position = data;
});
break;
}
case 'proxy': {
logFn.debug('Proxy event received (image update)', {
logFn.debug(logMsg[LogCategory.REMOTE].proxyEventReceived, {
category: LogCategory.REMOTE,
meta: {
dataLength: data?.length,
@@ -157,13 +170,16 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
break;
}
case 'rating': {
logFn.debug('Rating event received', {
category: LogCategory.REMOTE,
meta: {
id: data.id,
rating: data.rating,
logFn.debug(
logMsg[LogCategory.REMOTE].ratingEventReceived,
{
category: LogCategory.REMOTE,
meta: {
id: data.id,
rating: data.rating,
},
},
});
);
set((state) => {
if (state.info.song?.id === data.id) {
state.info.song.userRating = data.rating;
@@ -172,27 +188,33 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
break;
}
case 'repeat': {
logFn.debug('Repeat event received', {
category: LogCategory.REMOTE,
meta: { repeat: data },
});
logFn.debug(
logMsg[LogCategory.REMOTE].repeatEventReceived,
{
category: LogCategory.REMOTE,
meta: { repeat: data },
},
);
set((state) => {
state.info.repeat = data;
});
break;
}
case 'shuffle': {
logFn.debug('Shuffle event received', {
category: LogCategory.REMOTE,
meta: { shuffle: data },
});
logFn.debug(
logMsg[LogCategory.REMOTE].shuffleEventReceived,
{
category: LogCategory.REMOTE,
meta: { shuffle: data },
},
);
set((state) => {
state.info.shuffle = data;
});
break;
}
case 'song': {
logFn.debug('Song event received', {
logFn.debug(logMsg[LogCategory.REMOTE].songEventReceived, {
category: LogCategory.REMOTE,
meta: {
artistName: data?.artistName,
@@ -206,7 +228,7 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
break;
}
case 'state': {
logFn.debug('State event received (full state update)', {
logFn.debug(logMsg[LogCategory.REMOTE].stateEventReceived, {
category: LogCategory.REMOTE,
meta: {
hasSong: !!data.song,
@@ -221,10 +243,13 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
break;
}
case 'volume': {
logFn.debug('Volume event received', {
category: LogCategory.REMOTE,
meta: { volume: data },
});
logFn.debug(
logMsg[LogCategory.REMOTE].volumeEventReceived,
{
category: LogCategory.REMOTE,
meta: { volume: data },
},
);
set((state) => {
state.info.volume = data;
});
@@ -233,7 +258,7 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
});
socket.addEventListener('open', () => {
logFn.debug('WebSocket opened', {
logFn.debug(logMsg[LogCategory.REMOTE].webSocketOpened, {
category: LogCategory.REMOTE,
meta: {
hasAuthHeader: !!authHeader,
@@ -241,7 +266,7 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
},
});
if (authHeader) {
logFn.debug('Sending authentication', {
logFn.debug(logMsg[LogCategory.REMOTE].sendingAuthentication, {
category: LogCategory.REMOTE,
});
socket.send(
@@ -255,7 +280,7 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
});
socket.addEventListener('close', (reason) => {
logFn.debug('WebSocket closed', {
logFn.debug(logMsg[LogCategory.REMOTE].webSocketClosed, {
category: LogCategory.REMOTE,
meta: {
code: reason.code,
@@ -265,13 +290,13 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
},
});
if (reason.code === 4002 || reason.code === 4003) {
logFn.debug('Reloading page due to close code', {
logFn.debug(logMsg[LogCategory.REMOTE].reloadingPage, {
category: LogCategory.REMOTE,
meta: { code: reason.code },
});
location.reload();
} else if (reason.code === 4000) {
logFn.warn('Server is down', {
logFn.warn(logMsg[LogCategory.REMOTE].serverIsDown, {
category: LogCategory.REMOTE,
});
toast.warn({
@@ -279,13 +304,16 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
title: 'Connection closed',
});
} else if (reason.code !== 4001 && !socket.natural) {
logFn.error('Socket closed unexpectedly', {
category: LogCategory.REMOTE,
meta: {
code: reason.code,
reason: reason.reason,
logFn.error(
logMsg[LogCategory.REMOTE].socketClosedUnexpectedly,
{
category: LogCategory.REMOTE,
meta: {
code: reason.code,
reason: reason.reason,
},
},
});
);
toast.error({
message: 'Socket closed for unexpected reason',
title: 'Connection closed',
@@ -303,7 +331,7 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
send: (data: ClientEvent) => {
const socket = get().socket;
if (socket) {
logFn.debug('Sending event to server', {
logFn.debug(logMsg[LogCategory.REMOTE].sendingEventToServer, {
category: LogCategory.REMOTE,
meta: {
data: data,
@@ -313,7 +341,7 @@ export const useRemoteStore = createWithEqualityFn<SettingsSlice>()(
});
socket.send(JSON.stringify(data));
} else {
logFn.warn('Cannot send event - socket not available', {
logFn.warn(logMsg[LogCategory.REMOTE].cannotSendEvent, {
category: LogCategory.REMOTE,
meta: { event: data.event },
});
-10
View File
@@ -4,7 +4,6 @@ import { NavidromeController } from '/@/renderer/api/navidrome/navidrome-control
import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller';
import { mergeMusicFolderId } from '/@/renderer/api/utils-music-folder';
import { getServerById, useAuthStore, useSettingsStore } from '/@/renderer/store';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { toast } from '/@/shared/components/toast/toast';
import {
AuthenticationResponse,
@@ -32,7 +31,6 @@ const apiController = <K extends keyof ControllerEndpoint>(
const serverType = type || useAuthStore.getState().currentServer?.type;
if (!serverType) {
logFn.warn('No server selected', { category: LogCategory.API });
toast.error({
message: i18n.t('error.serverNotSelectedError', {
postProcess: 'sentenceCase',
@@ -45,10 +43,6 @@ const apiController = <K extends keyof ControllerEndpoint>(
const controllerFn = endpoints?.[serverType]?.[endpoint];
if (typeof controllerFn !== 'function') {
logFn.warn('Endpoint not implemented', {
category: LogCategory.API,
meta: { endpoint, serverType },
});
toast.error({
message: `Endpoint ${endpoint} is not implemented for ${serverType}`,
title: i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' }) as string,
@@ -63,10 +57,6 @@ const apiController = <K extends keyof ControllerEndpoint>(
);
}
logFn.debug('API controller call', {
category: LogCategory.API,
meta: { endpoint, serverType },
});
return controllerFn;
};
@@ -57,7 +57,7 @@ const JF_FIELDS = {
ALBUM_ARTIST_DETAIL: 'Genres, Overview, SortName, ProviderIds',
ALBUM_ARTIST_LIST: 'Genres, DateCreated, ExternalUrls, Overview, SortName, ProviderIds',
ALBUM_DETAIL: 'Genres, DateCreated, ChildCount, People, Tags, ProviderIds',
ALBUM_LIST: 'People, Tags, Studios, SortName, UserData, ProviderIds, ChildCount',
ALBUM_LIST: 'People, Tags, Studios, SortName, UserData, ProviderIds',
FOLDER: 'Genres, DateCreated, MediaSources, UserData, ParentId',
GENRE: 'ItemCounts',
PLAYLIST_DETAIL: 'Genres, DateCreated, MediaSources, ChildCount, ParentId, SortName',
@@ -242,7 +242,7 @@ export const JellyfinController: InternalControllerEndpoint = {
userId: apiClientProps.server?.userId,
},
query: {
Fields: JF_FIELDS.ALBUM_ARTIST_DETAIL,
Fields: 'Genres, Overview, SortName, ProviderIds',
},
}),
jfApiClient(apiClientProps).getSimilarArtistList({
@@ -269,7 +269,7 @@ export const JellyfinController: InternalControllerEndpoint = {
const res = await jfApiClient(apiClientProps).getAlbumArtistList({
query: {
Fields: JF_FIELDS.ALBUM_ARTIST_LIST,
Fields: 'Genres, DateCreated, ExternalUrls, Overview, SortName, ProviderIds',
ImageTypeLimit: 1,
Limit: query.limit,
ParentId: getLibraryId(query.musicFolderId),
@@ -321,7 +321,7 @@ export const JellyfinController: InternalControllerEndpoint = {
userId: apiClientProps.server.userId,
},
query: {
Fields: JF_FIELDS.SONG,
Fields: 'Genres, DateCreated, MediaSources, ParentId, People, Tags, SortName, ProviderIds',
IncludeItemTypes: 'Audio',
ParentId: query.id,
SortBy: 'ParentIndexNumber,IndexNumber,SortName',
@@ -1112,7 +1112,7 @@ export const JellyfinController: InternalControllerEndpoint = {
GenreIds: query.genreIds?.join(','),
IncludeItemTypes: 'Audio',
IsFavorite: query.favorite,
Limit: query.limit === -1 ? undefined : query.limit,
Limit: query.limit,
ParentId: getLibraryId(query.musicFolderId),
Recursive: true,
SearchTerm: query.searchTerm,
@@ -1147,7 +1147,7 @@ export const JellyfinController: InternalControllerEndpoint = {
GenreIds: query.genreIds?.join(','),
IncludeItemTypes: 'Audio',
IsFavorite: query.favorite,
Limit: query.limit === -1 ? undefined : query.limit,
Limit: query.limit,
ParentId: getLibraryId(query.musicFolderId),
Recursive: true,
SearchTerm: query.searchTerm,
+3 -17
View File
@@ -8,7 +8,6 @@ import qs from 'qs';
import i18n from '/@/i18n/i18n';
import { authenticationFailure } from '/@/renderer/api/utils';
import { useAuthStore } from '/@/renderer/store';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { getServerUrl } from '/@/renderer/utils/normalize-server-url';
import { ndType } from '/@/shared/api/navidrome/navidrome-types';
import { resultWithHeaders } from '/@/shared/api/utils';
@@ -368,21 +367,11 @@ axiosClient.interceptors.response.use(
})
.catch((newError: any) => {
if (newError !== TIMEOUT_ERROR) {
logFn.error('Reauthentication failed', {
category: LogCategory.API,
meta: {
message: (newError as Error)?.message,
serverId: currentServer.id,
},
});
console.error('Error when trying to reauthenticate: ', newError);
if (isAxiosError(newError) && newError.code === 'ERR_NETWORK') {
logFn.info(
console.log(
'Network error during reauthentication - preserving credentials',
{
category: LogCategory.API,
meta: { serverId: currentServer.id },
},
);
} else {
limitedFail(currentServer);
@@ -398,10 +387,7 @@ axiosClient.interceptors.response.use(
}
if (isAxiosError(error) && error.code === 'ERR_NETWORK') {
logFn.info('Network error during authentication - preserving credentials', {
category: LogCategory.API,
meta: { serverId: useAuthStore.getState().currentServer?.id },
});
console.log('Network error during authentication - preserving credentials');
} else {
limitedFail(currentServer);
}
+26
View File
@@ -270,6 +270,32 @@ export const queryKeys: Record<
},
root: (serverId: string) => [serverId, 'genres'] as const,
},
musicbrainz: {
artist: (
limit: number | undefined,
mbzArtistId: string,
config?: {
autoCountryPriority: boolean;
excludeReleaseTypes: string[];
prioritizeCountries: string[];
},
) =>
[
'musicbrainz',
'artist',
mbzArtistId,
limit,
config
? [
String(config.autoCountryPriority),
[...config.excludeReleaseTypes].sort().join(','),
[...config.prioritizeCountries].sort().join(','),
]
: null,
] as const,
release: (releaseId: string) => ['musicbrainz', 'release', releaseId] as const,
root: () => ['musicbrainz'] as const,
},
musicFolders: {
list: (serverId: string) => [serverId, 'musicFolders', 'list'] as const,
},
@@ -38,7 +38,6 @@ const ALBUM_LIST_SORT_MAPPING: Record<AlbumListSort, AlbumListSortType | undefin
[AlbumListSort.DURATION]: undefined,
[AlbumListSort.EXPLICIT_STATUS]: undefined,
[AlbumListSort.FAVORITED]: AlbumListSortType.STARRED,
[AlbumListSort.ID]: undefined,
[AlbumListSort.NAME]: AlbumListSortType.ALPHABETICAL_BY_NAME,
[AlbumListSort.PLAY_COUNT]: AlbumListSortType.FREQUENT,
[AlbumListSort.RANDOM]: AlbumListSortType.RANDOM,
+2 -5
View File
@@ -1,19 +1,16 @@
import { useAuthStore } from '/@/renderer/store';
import { LogCategory, logFn } from '/@/renderer/utils/logger';
import { toast } from '/@/shared/components/toast/toast';
import { ServerListItem } from '/@/shared/types/types';
export const authenticationFailure = (currentServer: null | ServerListItem) => {
logFn.error('Token expired', {
category: LogCategory.API,
meta: { serverId: currentServer?.id },
});
toast.error({
message: 'Your session has expired.',
});
if (currentServer) {
const serverId = currentServer.id;
const token = currentServer.ndCredential;
console.error(`token is expired: ${token}`);
useAuthStore.getState().actions.updateServer(serverId, { ndCredential: undefined });
useAuthStore.getState().actions.setCurrentServer(null);
}
-14
View File
@@ -10,7 +10,6 @@ import isElectron from 'is-electron';
import { lazy, Suspense, useEffect, useMemo, useRef, useState } from 'react';
import i18n from '/@/i18n/i18n';
import { openSettingsModal } from '/@/renderer/features/settings/utils/open-settings-modal';
import { WebAudioContext } from '/@/renderer/features/player/context/webaudio-context';
import { useCheckForUpdates } from '/@/renderer/hooks/use-check-for-updates';
import { useSyncSettingsToMain } from '/@/renderer/hooks/use-sync-settings-to-main';
@@ -80,19 +79,6 @@ export const App = () => {
}
}, [language]);
useEffect(() => {
if (isElectron()) {
window.api.utils.rendererOpenSettings(() => {
openSettingsModal();
});
return () => {
ipc?.removeAllListeners('renderer-open-settings');
};
}
return undefined;
}, []);
const notificationStyles = useMemo(
() => ({
root: {
@@ -32,8 +32,10 @@ export const DragPreview = memo(({ data }: DragPreviewProps) => {
const itemName = firstItem ? getItemName(firstItem) : 'Item';
const itemImage = useItemImageUrl({
id: (firstItem as { imageId: string })?.imageId,
id: (firstItem as { imageId?: string })?.imageId,
imageUrl: (firstItem as { imageUrl?: string })?.imageUrl,
itemType: data.itemType || LibraryItem.SONG,
serverId: (firstItem as { _serverId?: string })?._serverId,
type: 'table',
});
@@ -59,12 +59,33 @@
}
}
.image-container.external {
img {
opacity: 0.5;
transition: all 0.2s ease-in-out;
}
&:hover {
img {
opacity: 1;
}
}
}
.image-container.is-round {
&::before {
border-radius: 50%;
}
}
.image-container.no-hover-overlay {
&:hover {
&::before {
opacity: 0 !important;
}
}
}
.favorite-badge {
position: absolute;
top: -50px;
@@ -100,9 +121,19 @@
transition: opacity 0.2s ease-in-out;
}
.image-container:hover .favorite-badge,
.image-container:hover .rating-badge {
opacity: 0;
.external-badge {
position: absolute;
bottom: var(--theme-spacing-sm);
left: var(--theme-spacing-sm);
z-index: 5;
display: flex;
align-items: center;
justify-content: center;
padding: var(--theme-spacing-xs) var(--theme-spacing-sm);
pointer-events: none;
background-color: alpha(var(--theme-colors-state-error), 0.85);
border-radius: var(--theme-radius-md);
box-shadow: 0 2px 8px rgb(0 0 0 / 30%);
}
.image {
+63 -21
View File
@@ -19,7 +19,7 @@ import { ItemControls } from '/@/renderer/components/item-list/types';
import { JoinedArtists } from '/@/renderer/features/albums/components/joined-artists';
import { useDragDrop } from '/@/renderer/hooks/use-drag-drop';
import { AppRoute } from '/@/renderer/router/routes';
import { useShowRatings } from '/@/renderer/store';
import { useIntegrationsSettings, useShowRatings } from '/@/renderer/store';
import {
formatDateAbsolute,
formatDateAbsoluteUTC,
@@ -29,6 +29,7 @@ import {
} from '/@/renderer/utils/format';
import { SEPARATOR_STRING } from '/@/shared/api/utils';
import { ExplicitIndicator } from '/@/shared/components/explicit-indicator/explicit-indicator';
import { ExternalSongIndicator } from '/@/shared/components/external-song-indicator/external-song-indicator';
import { Group } from '/@/shared/components/group/group';
import { Icon } from '/@/shared/components/icon/icon';
import { Separator } from '/@/shared/components/separator/separator';
@@ -42,6 +43,7 @@ import {
Genre,
LibraryItem,
Playlist,
ServerType,
Song,
} from '/@/shared/types/domain-types';
import { DragOperation, DragTarget } from '/@/shared/types/drag-and-drop';
@@ -178,6 +180,7 @@ const CompactItemCard = ({
showRating,
withControls,
}: ItemCardDerivativeProps) => {
const { youtube: youtubeIntegrationEnabled } = useIntegrationsSettings();
const [showControls, setShowControls] = useState(false);
const itemRowId =
data && internalState && typeof data === 'object' && 'id' in data
@@ -339,9 +342,15 @@ const CompactItemCard = ({
? (data as { userRating: null | number }).userRating
: null;
const hasRating = showRating && userRating !== null && userRating > 0;
const isExternal = data._serverType === ServerType.EXTERNAL;
const showItemCardControls =
withControls && showControls && data && (!isExternal || youtubeIntegrationEnabled);
const imageContainerClassName = clsx(styles.imageContainer, {
[styles.external]: isExternal,
[styles.isRound]: isRound,
[styles.noHoverOverlay]: isExternal && !showItemCardControls,
});
const imageContainerContent = (
@@ -373,8 +382,13 @@ const CompactItemCard = ({
)}
{isFavorite && <div className={styles.favoriteBadge} />}
{hasRating && <div className={styles.ratingBadge}>{userRating}</div>}
{isExternal && (
<div className={styles.externalBadge} title={i18n.t('common.external')}>
<ExternalSongIndicator isExternal size="sm" withSpace={false} />
</div>
)}
<AnimatePresence>
{withControls && showControls && data && (
{showItemCardControls && (
<ItemCardControls
controls={controls}
enableExpansion={enableExpansion}
@@ -409,6 +423,7 @@ const CompactItemCard = ({
<div
className={clsx(styles.container, styles.compact, {
[styles.dragging]: isDragging,
[styles.external]: isExternal,
[styles.selected]: isSelected,
})}
ref={ref}
@@ -482,6 +497,7 @@ const DefaultItemCard = ({
showRating,
withControls,
}: ItemCardDerivativeProps) => {
const { youtube: youtubeIntegrationEnabled } = useIntegrationsSettings();
const [showControls, setShowControls] = useState(false);
const itemRowId =
data && internalState && typeof data === 'object' && 'id' in data
@@ -570,10 +586,6 @@ const DefaultItemCard = ({
e.stopPropagation();
};
const imageContainerClassName = clsx(styles.imageContainer, {
[styles.isRound]: isRound,
});
const isFavorite =
'userFavorite' in data && (data as { userFavorite: boolean }).userFavorite;
const userRating =
@@ -582,6 +594,16 @@ const DefaultItemCard = ({
? (data as { userRating: null | number }).userRating
: null;
const hasRating = showRating && userRating !== null && userRating > 0;
const isExternal = data._serverType === ServerType.EXTERNAL;
const showItemCardControls =
withControls && showControls && data && (!isExternal || youtubeIntegrationEnabled);
const imageContainerClassName = clsx(styles.imageContainer, {
[styles.external]: isExternal,
[styles.isRound]: isRound,
[styles.noHoverOverlay]: isExternal && !showItemCardControls,
});
const imageContainerContent = (
<>
@@ -610,8 +632,13 @@ const DefaultItemCard = ({
)}
{isFavorite && <div className={styles.favoriteBadge} />}
{hasRating && <div className={styles.ratingBadge}>{userRating}</div>}
{isExternal && (
<div className={styles.externalBadge} title={i18n.t('common.external')}>
<ExternalSongIndicator isExternal size="sm" withSpace={false} />
</div>
)}
<AnimatePresence>
{withControls && showControls && (
{showItemCardControls && (
<ItemCardControls
controls={controls}
enableExpansion={enableExpansion}
@@ -628,6 +655,7 @@ const DefaultItemCard = ({
return (
<div
className={clsx(styles.container, {
[styles.external]: isExternal,
[styles.selected]: isSelected,
})}
>
@@ -717,6 +745,7 @@ const PosterItemCard = ({
showRating,
withControls,
}: ItemCardDerivativeProps) => {
const { youtube: youtubeIntegrationEnabled } = useIntegrationsSettings();
const [showControls, setShowControls] = useState(false);
const itemRowId =
data && internalState && typeof data === 'object' && 'id' in data
@@ -870,10 +899,6 @@ const PosterItemCard = ({
e.stopPropagation();
};
const imageContainerClassName = clsx(styles.imageContainer, {
[styles.isRound]: isRound,
});
const isFavorite =
'userFavorite' in data && (data as { userFavorite: boolean }).userFavorite;
const userRating =
@@ -882,6 +907,16 @@ const PosterItemCard = ({
? (data as { userRating: null | number }).userRating
: null;
const hasRating = showRating && userRating !== null && userRating > 0;
const isExternal = data._serverType === ServerType.EXTERNAL;
const showItemCardControls =
withControls && showControls && data && (!isExternal || youtubeIntegrationEnabled);
const imageContainerClassName = clsx(styles.imageContainer, {
[styles.external]: isExternal,
[styles.isRound]: isRound,
[styles.noHoverOverlay]: isExternal && !showItemCardControls,
});
const imageContainerContent = (
<>
@@ -910,8 +945,13 @@ const PosterItemCard = ({
)}
{isFavorite && <div className={styles.favoriteBadge} />}
{hasRating && <div className={styles.ratingBadge}>{userRating}</div>}
{isExternal && (
<div className={styles.externalBadge}>
<ExternalSongIndicator isExternal size="xl" withSpace={false} />
</div>
)}
<AnimatePresence>
{withControls && showControls && data && (
{showItemCardControls && (
<ItemCardControls
controls={controls}
enableExpansion={enableExpansion}
@@ -930,6 +970,7 @@ const PosterItemCard = ({
<div
className={clsx(styles.container, styles.poster, {
[styles.dragging]: isDragging,
[styles.external]: isExternal,
[styles.selected]: isSelected,
})}
ref={ref}
@@ -1025,18 +1066,20 @@ export const getDataRows = (type?: 'compact' | 'default' | 'poster'): DataRow[]
if ('id' in data && data.id) {
if ('_itemType' in data) {
switch (data._itemType) {
case LibraryItem.ALBUM:
return (
<Link
state={{ item: data }}
to={generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, {
albumId: data.id,
})}
>
case LibraryItem.ALBUM: {
const albumPath = getTitlePath(LibraryItem.ALBUM, data.id);
return albumPath ? (
<Link state={{ item: data }} to={albumPath}>
<ExplicitIndicator explicitStatus={explicitStatus} />
{data.name}
</Link>
) : (
<>
<ExplicitIndicator explicitStatus={explicitStatus} />
{data.name}
</>
);
}
case LibraryItem.ALBUM_ARTIST:
return (
<Link
@@ -1333,7 +1376,6 @@ const getItemNavigationPath = (
}
const effectiveItemType = '_itemType' in data && data._itemType ? data._itemType : itemType;
return getTitlePath(effectiveItemType, data.id);
};
@@ -0,0 +1,84 @@
.container {
display: grid;
grid-template-rows: 1fr;
grid-template-columns: auto minmax(0, 1fr);
gap: var(--theme-spacing-sm);
width: 100%;
height: 100%;
padding: var(--theme-spacing-sm);
container-type: inline-size;
background: var(--theme-colors-surface);
border-radius: var(--theme-radius-md);
@container (min-width: 500px) {
grid-template-columns: minmax(0, 1fr);
}
}
.image-container {
position: relative;
display: none;
height: 100%;
min-height: 0;
aspect-ratio: 1/1;
&::before {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
content: '';
background-color: rgb(0 0 0);
opacity: 0;
transition: all 0.2s ease-in-out;
}
&:hover {
&::before {
opacity: 0.6;
}
}
@container (min-width: 500px) {
display: block;
}
}
.image {
aspect-ratio: 1/1;
}
.metadata-container {
display: flex;
flex-direction: column;
gap: var(--theme-spacing-sm);
width: 100%;
height: 100%;
padding: var(--theme-spacing-xs) 0;
overflow: hidden;
}
.metadata-container .header {
display: flex;
align-items: center;
justify-content: space-between;
font-weight: 600;
line-height: 1.2;
}
.metadata-container .header .title {
max-width: 70%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.metadata-container .content {
display: flex;
flex-direction: column;
gap: var(--theme-spacing-xs);
}
.metadata-container .content .tags {
}
@@ -0,0 +1,146 @@
// import { AnimatePresence } from 'motion/react';
// import { MouseEvent, useMemo, useState } from 'react';
// import { Link } from 'react-router';
// import styles from './item-detail.module.css';
// import { ItemCardControls } from '/@/renderer/components/item-card/item-card-controls';
// import { useFastAverageColor } from '/@/renderer/hooks';
// import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
// import { Badge } from '/@/shared/components/badge/badge';
// import { Divider } from '/@/shared/components/divider/divider';
// import { Group } from '/@/shared/components/group/group';
// import { Image } from '/@/shared/components/image/image';
// import { Rating } from '/@/shared/components/rating/rating';
// import { Text } from '/@/shared/components/text/text';
// import {
// Album,
// AlbumArtist,
// Artist,
// LibraryItem,
// Playlist,
// Song,
// } from '/@/shared/types/domain-types';
// import { stringToColor } from '/@/shared/utils/string-to-color';
// interface ItemDetailProps {
// data: Album | AlbumArtist | Artist | Playlist | Song | undefined;
// itemHeight: number;
// itemType: LibraryItem;
// onClick?: (e: MouseEvent<HTMLDivElement>, item: unknown, itemType: LibraryItem) => void;
// withControls?: boolean;
// }
// export const ItemDetail = ({ data, itemType, onClick, withControls }: ItemDetailProps) => {
// const imageUrl = getImageUrl(data);
// const [showControls, setShowControls] = useState(false);
// const { background } = useFastAverageColor({
// algorithm: 'simple',
// src: imageUrl,
// srcLoaded: false,
// });
// // const tags = [...(data?.genres ?? [])];
// const tags = useMemo(() => {
// if (!data) {
// return [];
// }
// const items: {
// color?: string;
// id: string;
// isLight?: boolean;
// itemType: LibraryItem;
// name: string;
// }[] = [];
// if ('albumArtists' in data && Array.isArray(data.albumArtists)) {
// data.albumArtists?.forEach((tag: { id: string; name: string }) => {
// items.push({ id: tag.id, itemType: LibraryItem.ALBUM_ARTIST, name: tag.name });
// });
// }
// if ('genres' in data && Array.isArray(data.genres)) {
// data.genres?.forEach((tag: { id: string; itemType: LibraryItem; name: string }) => {
// const { color, isLight } = stringToColor(tag.name);
// items.push({ ...tag, color, isLight });
// });
// }
// // if ('tags' in data && typeof data.tags === 'object') {
// // console.log('data.tags :>> ', data.tags);
// // Object.entries(data.tags).forEach(([key, value]) => {
// // items.push({ id: key, itemType: LibraryItem.TAG, name: value });
// // });
// // }
// return items;
// }, [data]);
// return (
// <div
// className={styles.container}
// onClick={(e) => onClick?.(e, data, itemType)}
// style={{ backgroundColor: background }}
// >
// <div
// className={styles.imageContainer}
// onMouseEnter={() => withControls && setShowControls(true)}
// onMouseLeave={() => withControls && setShowControls(false)}
// >
// <Image alt={data?.name} src={imageUrl} />
// <AnimatePresence>
// {withControls && showControls && <ItemCardControls type="compact" />}
// </AnimatePresence>
// </div>
// <div className={styles.metadataContainer}>
// <div className={styles.header}>
// <Text className={styles.title} component={Link} isLink size="lg" weight={500}>
// {data?.name}
// </Text>
// <Group>
// {data && 'userRating' in data && (
// <Rating size="xs" value={data?.userRating ?? 0} />
// )}
// {data && 'userFavorite' in data && (
// <ActionIcon
// icon="favorite"
// iconProps={{
// fill: data?.userFavorite ? 'primary' : 'default',
// }}
// size="xs"
// />
// )}
// </Group>
// </div>
// <Divider />
// <div className={styles.content}>
// <Group className={styles.tags} gap="xs">
// {tags.map((tag) => (
// <Badge
// key={tag.id}
// style={{
// backgroundColor: tag.color,
// color: tag.isLight ? 'black' : 'white',
// }}
// >
// {tag.name}
// </Badge>
// ))}
// </Group>
// </div>
// </div>
// </div>
// );
// };
// const getImageUrl = (data: Album | AlbumArtist | Artist | Playlist | Song | undefined) => {
// if (data && 'imageUrl' in data) {
// return data.imageUrl || undefined;
// }
// return undefined;
// };
@@ -8,7 +8,7 @@ import { ContextMenuController } from '/@/renderer/features/context-menu/context
import { usePlayer } from '/@/renderer/features/player/context/player-context';
import { useSetFavorite } from '/@/renderer/features/shared/hooks/use-set-favorite';
import { useSetRating } from '/@/renderer/features/shared/hooks/use-set-rating';
import { LibraryItem, QueueSong, Song } from '/@/shared/types/domain-types';
import { LibraryItem, QueueSong, ServerType, Song } from '/@/shared/types/domain-types';
import { Play, TableColumn } from '/@/shared/types/types';
interface UseDefaultItemListControlsArgs {
@@ -192,10 +192,9 @@ export const useDefaultItemListControls = (args?: UseDefaultItemListControlsArgs
onColumnReordered?.(columnIdFrom, columnIdTo, edge);
},
onColumnResized: onColumnResized
? ({ columnId, width }: { columnId: TableColumn; width: number }) =>
onColumnResized(columnId, width)
: undefined,
onColumnResized: ({ columnId, width }: { columnId: TableColumn; width: number }) => {
onColumnResized?.(columnId, width);
},
onDoubleClick: ({ internalState, item, itemType, meta }: DefaultItemControlProps) => {
if (!item || !internalState) {
@@ -242,11 +241,11 @@ export const useDefaultItemListControls = (args?: UseDefaultItemListControlsArgs
}
const playType = (meta?.playType as Play) || Play.NOW;
const singleSongOnly = meta?.singleSongOnly === true;
// For NEXT, LAST, NEXT_SHUFFLE, and LAST_SHUFFLE, only add the clicked song
// For NOW and SHUFFLE, add a range of songs around the clicked song
let songsToAdd: Song[];
if (
singleSongOnly ||
playType === Play.NEXT ||
playType === Play.LAST ||
playType === Play.NEXT_SHUFFLE ||
@@ -385,6 +384,15 @@ export const useDefaultItemListControls = (args?: UseDefaultItemListControlsArgs
return;
}
const isExternal =
(item as Song & { _serverType?: ServerType })._serverType ===
ServerType.EXTERNAL;
if (isExternal && itemType === LibraryItem.SONG) {
player.addToQueueByData([item as Song], playType, item.id);
return;
}
player.addToQueueByFetch(item._serverId, [item.id], itemType, playType);
},
@@ -418,9 +426,9 @@ export const useDefaultItemListControls = (args?: UseDefaultItemListControlsArgs
};
}, [
enableMultiSelect,
overrides,
onColumnReordered,
onColumnResized,
overrides,
player,
setFavorite,
setRating,
@@ -7,19 +7,14 @@ import { ItemListKey, TableColumn } from '/@/shared/types/types';
interface UseItemListColumnReorderProps {
itemListKey: ItemListKey;
tableKey?: 'detail' | 'main';
}
export const useItemListColumnReorder = ({
itemListKey,
tableKey = 'main',
}: UseItemListColumnReorderProps) => {
export const useItemListColumnReorder = ({ itemListKey }: UseItemListColumnReorderProps) => {
const { setList } = useSettingsStoreActions();
const handleColumnReordered = useCallback(
(columnIdFrom: TableColumn, columnIdTo: TableColumn, edge: Edge | null) => {
const list = useSettingsStore.getState().lists[itemListKey];
const columns = tableKey === 'detail' ? list?.detail?.columns : list?.table?.columns;
const columns = useSettingsStore.getState().lists[itemListKey]?.table.columns;
if (!columns) {
return;
@@ -88,20 +83,13 @@ export const useItemListColumnReorder = ({
// Insert the column at the new position
newColumns.splice(newIndex, 0, updatedMovedColumn);
if (tableKey === 'detail') {
type SetListData = Parameters<
ReturnType<typeof useSettingsStoreActions>['setList']
>[1];
setList(itemListKey, { detail: { columns: newColumns } } as SetListData);
} else {
setList(itemListKey, {
table: {
columns: newColumns,
},
});
}
setList(itemListKey, {
table: {
columns: newColumns,
},
});
},
[itemListKey, setList, tableKey],
[itemListKey, setList],
);
return { handleColumnReordered };
@@ -5,18 +5,11 @@ import { ItemListKey, TableColumn } from '/@/shared/types/types';
interface UseItemListColumnResizeProps {
itemListKey: ItemListKey;
tableKey?: 'detail' | 'main';
}
export const useItemListColumnResize = ({
itemListKey,
tableKey = 'main',
}: UseItemListColumnResizeProps) => {
export const useItemListColumnResize = ({ itemListKey }: UseItemListColumnResizeProps) => {
const { setList } = useSettingsStoreActions();
const columns = useSettingsStore((state) => {
const list = state.lists[itemListKey];
return tableKey === 'detail' ? list?.detail?.columns : list?.table?.columns;
});
const columns = useSettingsStore((state) => state.lists[itemListKey]?.table.columns);
const handleColumnResized = useCallback(
(columnId: TableColumn, width: number) => {
@@ -26,20 +19,13 @@ export const useItemListColumnResize = ({
column.id === columnId ? { ...column, width } : column,
);
if (tableKey === 'detail') {
type SetListData = Parameters<
ReturnType<typeof useSettingsStoreActions>['setList']
>[1];
setList(itemListKey, { detail: { columns: updatedColumns } } as SetListData);
} else {
setList(itemListKey, {
table: {
columns: updatedColumns,
},
});
}
setList(itemListKey, {
table: {
columns: updatedColumns,
},
});
},
[columns, itemListKey, setList, tableKey],
[columns, itemListKey, setList],
);
return { handleColumnResized };
@@ -1,29 +1,26 @@
import { useCallback, useMemo } from 'react';
import { useLocation, useNavigationType } from 'react-router';
import { useSearchParams } from 'react-router';
import { useScrollStore } from '/@/renderer/store/scroll.store';
import { parseIntParam, setSearchParam } from '/@/renderer/utils/query-params';
interface UseItemListScrollPersistProps {
enabled: boolean;
}
export const useItemListScrollPersist = ({ enabled }: UseItemListScrollPersistProps) => {
const location = useLocation();
const navigationType = useNavigationType();
const setOffset = useScrollStore((s) => s.setOffset);
const getOffset = useScrollStore((s) => s.getOffset);
const [searchParams, setSearchParams] = useSearchParams();
const scrollOffset = useMemo(() => {
if (navigationType !== 'POP') return undefined;
return getOffset(location.key);
}, [getOffset, location.key, navigationType]);
const scrollOffset = useMemo(() => parseIntParam(searchParams, 'scrollOffset'), [searchParams]);
const handleOnScrollEnd = useCallback(
(offset: number) => {
if (!enabled) return;
setOffset(location.key, offset);
setSearchParams((prev) => setSearchParam(prev, 'scrollOffset', offset), {
replace: true,
});
},
[enabled, location.key, setOffset],
[enabled, setSearchParams],
);
return { handleOnScrollEnd, scrollOffset };
@@ -1,38 +0,0 @@
import { ItemDetailListCellProps } from './types';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { LibraryItem } from '/@/shared/types/domain-types';
export const ActionsColumn = ({ controls, internalState, song }: ItemDetailListCellProps) => {
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
event.preventDefault();
const index = internalState?.findItemIndex(song.id) ?? -1;
controls?.onMore?.({
event,
index,
internalState: internalState ?? undefined,
item: song,
itemType: LibraryItem.SONG,
});
};
const handleDoubleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
event.preventDefault();
};
return (
<ActionIcon
icon="ellipsisHorizontal"
iconProps={{
color: 'muted',
size: 'xs',
}}
onClick={handleClick}
onDoubleClick={handleDoubleClick}
size="xs"
variant="subtle"
/>
);
};
@@ -1,23 +0,0 @@
import { ItemDetailListCellProps } from './types';
import {
JOINED_ARTISTS_MUTED_PROPS,
JoinedArtists,
} from '/@/renderer/features/albums/components/joined-artists';
export const AlbumArtistColumn = ({ isRowHovered, song }: ItemDetailListCellProps) => {
const name = song.albumArtistName?.trim() ?? '';
const hasArtists = name.length > 0 || (song.albumArtists?.length ?? 0) > 0;
if (!hasArtists) return <>&nbsp;</>;
return (
<JoinedArtists
artistName={song.albumArtistName ?? ''}
artists={song.albumArtists ?? []}
linkProps={JOINED_ARTISTS_MUTED_PROPS.linkProps}
readOnly={!isRowHovered}
rootTextProps={JOINED_ARTISTS_MUTED_PROPS.rootTextProps}
/>
);
};
@@ -1,3 +0,0 @@
import { ItemDetailListCellProps } from './types';
export const AlbumColumn = ({ song }: ItemDetailListCellProps) => song.album ?? <>&nbsp;</>;
@@ -1,23 +0,0 @@
import { ItemDetailListCellProps } from './types';
import {
JOINED_ARTISTS_MUTED_PROPS,
JoinedArtists,
} from '/@/renderer/features/albums/components/joined-artists';
export const ArtistColumn = ({ isRowHovered, song }: ItemDetailListCellProps) => {
const name = song.artistName?.trim() ?? '';
const hasArtists = name.length > 0 || (song.artists?.length ?? 0) > 0;
if (!hasArtists) return <>&nbsp;</>;
return (
<JoinedArtists
artistName={song.artistName ?? ''}
artists={song.artists ?? []}
linkProps={JOINED_ARTISTS_MUTED_PROPS.linkProps}
readOnly={!isRowHovered}
rootTextProps={JOINED_ARTISTS_MUTED_PROPS.rootTextProps}
/>
);
};
@@ -1,3 +0,0 @@
import { ItemDetailListCellProps } from './types';
export const BitDepthColumn = ({ song }: ItemDetailListCellProps) => song.bitDepth;
@@ -1,4 +0,0 @@
import { ItemDetailListCellProps } from './types';
export const BitRateColumn = ({ song }: ItemDetailListCellProps) =>
song.bitRate != null ? `${song.bitRate} kbps` : <>&nbsp;</>;
@@ -1,3 +0,0 @@
import { ItemDetailListCellProps } from './types';
export const BpmColumn = ({ song }: ItemDetailListCellProps) => song.bpm ?? <>&nbsp;</>;
@@ -1,4 +0,0 @@
import { ItemDetailListCellProps } from './types';
export const ChannelsColumn = ({ song }: ItemDetailListCellProps) =>
song.channels != null ? String(song.channels) : <>&nbsp;</>;
@@ -1,3 +0,0 @@
import { ItemDetailListCellProps } from './types';
export const CodecColumn = ({ song }: ItemDetailListCellProps) => song.container ?? <>&nbsp;</>;
@@ -1,3 +0,0 @@
import { ItemDetailListCellProps } from './types';
export const CommentColumn = ({ song }: ItemDetailListCellProps) => song.comment ?? <>&nbsp;</>;
@@ -1,7 +0,0 @@
import { ItemDetailListCellProps } from './types';
export const ComposerColumn = ({ song }: ItemDetailListCellProps) => {
const composers = song.participants?.composer;
if (!composers?.length) return <>&nbsp;</>;
return composers.map((a) => a.name).join(', ');
};
@@ -1,6 +0,0 @@
import { ItemDetailListCellProps } from './types';
import { formatDateAbsolute } from '/@/renderer/utils/format';
export const DateAddedColumn = ({ song }: ItemDetailListCellProps) =>
song.createdAt ? formatDateAbsolute(song.createdAt) : <>&nbsp;</>;
@@ -1,11 +0,0 @@
import { ItemDetailListCellProps } from './types';
interface DefaultColumnProps extends ItemDetailListCellProps {
columnId: string;
}
export const DefaultColumn = ({ columnId, song }: DefaultColumnProps) => {
const raw = (song as Record<string, unknown>)[columnId];
if (raw === undefined || raw === null || typeof raw === 'object') return <>&nbsp;</>;
return String(raw);
};
@@ -1,3 +0,0 @@
import { ItemDetailListCellProps } from './types';
export const DiscNumberColumn = ({ song }: ItemDetailListCellProps) => String(song.discNumber ?? 1);
@@ -1,5 +0,0 @@
import formatDuration from 'format-duration';
import { ItemDetailListCellProps } from './types';
export const DurationColumn = ({ song }: ItemDetailListCellProps) => formatDuration(song.duration);
@@ -1,54 +0,0 @@
import { ItemDetailListCellProps } from './types';
import { useIsMutatingCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
import { useIsMutatingDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { LibraryItem } from '/@/shared/types/domain-types';
export const FavoriteColumn = ({
controls,
internalState,
isMutatingFavorite,
onFavoriteClick,
song,
}: ItemDetailListCellProps) => {
const isMutatingCreateFavorite = useIsMutatingCreateFavorite();
const isMutatingDeleteFavorite = useIsMutatingDeleteFavorite();
const isMutating = isMutatingFavorite ?? (isMutatingCreateFavorite || isMutatingDeleteFavorite);
const isFavorite = song.userFavorite ?? false;
return (
<ActionIcon
disabled={isMutating}
icon="favorite"
iconProps={{
color: isFavorite ? 'primary' : 'muted',
fill: isFavorite ? 'primary' : undefined,
size: 'xs',
}}
onClick={(event) => {
event.stopPropagation();
event.preventDefault();
const index = internalState?.findItemIndex(song.id) ?? -1;
if (controls?.onFavorite) {
controls.onFavorite({
event,
favorite: !isFavorite,
index,
internalState: internalState ?? undefined,
item: song,
itemType: LibraryItem.SONG,
});
} else {
onFavoriteClick?.(song);
}
}}
onDoubleClick={(event) => {
event.stopPropagation();
event.preventDefault();
}}
size="xs"
variant="subtle"
/>
);
};
@@ -1,12 +0,0 @@
.group {
flex-wrap: nowrap;
gap: var(--theme-spacing-sm) var(--theme-spacing-xs);
min-width: 0;
padding: var(--theme-spacing-xs) 0;
overflow: hidden;
}
.group a {
cursor: pointer;
user-select: none;
}
@@ -1,46 +0,0 @@
import { useMemo } from 'react';
import { generatePath, Link } from 'react-router';
import styles from './genre-badge-column.module.css';
import { ItemDetailListCellProps } from './types';
import { AppRoute } from '/@/renderer/router/routes';
import { Badge } from '/@/shared/components/badge/badge';
import { Group } from '/@/shared/components/group/group';
import { stringToColor } from '/@/shared/utils/string-to-color';
const MAX_GENRES = 4;
export const GenreBadgeColumn = ({ song }: ItemDetailListCellProps) => {
const genres = song.genres;
const genresWithStyle = useMemo(() => {
if (!genres) return [];
return genres.slice(0, MAX_GENRES).map((genre) => {
const { color, isLight } = stringToColor(genre.name);
const path = generatePath(AppRoute.LIBRARY_GENRES_DETAIL, { genreId: genre.id });
return { ...genre, color, isLight, path };
});
}, [genres]);
if (!genresWithStyle.length) return <>&nbsp;</>;
return (
<Group className={styles.group} wrap="nowrap">
{genresWithStyle.map((genre) => (
<Badge
component={Link}
key={genre.id}
state={{ item: genre }}
style={{
backgroundColor: genre.color,
color: genre.isLight ? 'black' : 'white',
}}
to={genre.path}
>
{genre.name}
</Badge>
))}
</Group>
);
};
@@ -1,40 +0,0 @@
import { Fragment } from 'react';
import { generatePath, Link } from 'react-router';
import { ItemDetailListCellProps } from '/@/renderer/components/item-list/item-detail-list/columns/types';
import { AppRoute } from '/@/renderer/router/routes';
import { Text } from '/@/shared/components/text/text';
const TEXT_PROPS = { isMuted: true, isNoSelect: true, size: 'sm' as const } as const;
export const GenreColumn = ({ isRowHovered, song }: ItemDetailListCellProps) => {
const genres = song.genres ?? [];
if (!genres.length) return <>&nbsp;</>;
return (
<>
{genres.map((genre, index) => (
<Fragment key={genre.id}>
{isRowHovered ? (
<Text
component={Link}
isLink
state={{ item: genre }}
to={generatePath(AppRoute.LIBRARY_GENRES_DETAIL, {
genreId: genre.id,
})}
{...TEXT_PROPS}
>
{genre.name}
</Text>
) : (
<Text component="span" {...TEXT_PROPS}>
{genre.name}
</Text>
)}
{index < genres.length - 1 && ', '}
</Fragment>
))}
</>
);
};
@@ -1,50 +0,0 @@
.image-container {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.compact-container {
flex: 1 1 0;
width: 100%;
min-width: 0;
height: 100%;
min-height: 0;
max-height: 100%;
aspect-ratio: unset;
padding-top: var(--theme-spacing-xs);
padding-bottom: var(--theme-spacing-xs);
overflow: hidden;
border-radius: var(--theme-radius-md);
}
.play-button-overlay {
position: absolute;
top: 50%;
left: 50%;
z-index: 10;
opacity: 0.6;
transform: translate(-50%, -50%);
transition: opacity 0.2s ease-in-out;
}
.play-button-overlay:hover {
opacity: 1;
}
.play-button-overlay button {
width: 24px;
height: 24px;
}
.compact-image {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
border-radius: var(--theme-radius-md);
}
@@ -1,71 +0,0 @@
import clsx from 'clsx';
import { useState } from 'react';
import styles from './image-column.module.css';
import { ItemDetailListCellProps } from './types';
import { ItemImage } from '/@/renderer/components/item-image/item-image';
import { PlayButton } from '/@/renderer/features/shared/components/play-button';
import {
LONG_PRESS_PLAY_BEHAVIOR,
PlayTooltip,
} from '/@/renderer/features/shared/components/play-button-group';
import { usePlayButtonBehavior } from '/@/renderer/store';
import { LibraryItem } from '/@/shared/types/domain-types';
import { Play } from '/@/shared/types/types';
export const ImageColumn = ({
controls,
internalState,
rowIndex = 0,
song,
}: ItemDetailListCellProps) => {
const playButtonBehavior = usePlayButtonBehavior();
const [isHovered, setIsHovered] = useState(false);
const handlePlay = (playType: Play) => {
if (!song || !controls?.onDoubleClick) {
return;
}
controls.onDoubleClick({
event: null,
index: rowIndex,
internalState,
item: song,
itemType: LibraryItem.SONG,
meta: { playType, singleSongOnly: true },
});
};
return (
<div
className={styles.imageContainer}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<ItemImage
className={styles.compactImage}
containerClassName={styles.compactContainer}
explicitStatus={song.explicitStatus}
id={song.imageId}
itemType={LibraryItem.SONG}
serverId={song._serverId}
type="table"
/>
{isHovered && (
<div className={clsx(styles.playButtonOverlay)}>
<PlayTooltip disabled={false} type={playButtonBehavior}>
<PlayButton
fill
onClick={() => handlePlay(playButtonBehavior)}
onLongPress={() =>
handlePlay(LONG_PRESS_PLAY_BEHAVIOR[playButtonBehavior])
}
/>
</PlayTooltip>
</div>
)}
</div>
);
};
@@ -1,127 +0,0 @@
import React, { type ReactNode } from 'react';
import type { ItemDetailListCellProps } from './types';
import { ActionsColumn } from './actions-column';
import { AlbumArtistColumn } from './album-artist-column';
import { AlbumColumn } from './album-column';
import { ArtistColumn } from './artist-column';
import { BitDepthColumn } from './bit-depth-column';
import { BitRateColumn } from './bit-rate-column';
import { BpmColumn } from './bpm-column';
import { ChannelsColumn } from './channels-column';
import { CodecColumn } from './codec-column';
import { CommentColumn } from './comment-column';
import { ComposerColumn } from './composer-column';
import { DateAddedColumn } from './date-added-column';
import { DefaultColumn } from './default-column';
import { DiscNumberColumn } from './disc-number-column';
import { DurationColumn } from './duration-column';
import { FavoriteColumn } from './favorite-column';
import { GenreBadgeColumn } from './genre-badge-column';
import { GenreColumn } from './genre-column';
import { ImageColumn } from './image-column';
import { LastPlayedColumn } from './last-played-column';
import { PathColumn } from './path-column';
import { PlayCountColumn } from './play-count-column';
import { RatingColumn } from './rating-column';
import { ReleaseDateColumn } from './release-date-column';
import { RowIndexColumn } from './row-index-column';
import { SampleRateColumn } from './sample-rate-column';
import { SizeColumn } from './size-column';
import { TitleArtistColumn } from './title-artist-column';
import { TitleColumn } from './title-column';
import { TitleCombinedColumn } from './title-combined-column';
import { TrackNumberColumn } from './track-number-column';
import { YearColumn } from './year-column';
import { TableColumn } from '/@/shared/types/types';
type CellComponent = (props: ItemDetailListCellProps) => ReactNode;
const COLUMN_MAP: Partial<Record<TableColumn, CellComponent>> = {
[TableColumn.ACTIONS]: ActionsColumn,
[TableColumn.ALBUM]: AlbumColumn,
[TableColumn.ALBUM_ARTIST]: AlbumArtistColumn,
[TableColumn.ARTIST]: ArtistColumn,
[TableColumn.BIT_DEPTH]: BitDepthColumn,
[TableColumn.BIT_RATE]: BitRateColumn,
[TableColumn.BPM]: BpmColumn,
[TableColumn.CHANNELS]: ChannelsColumn,
[TableColumn.CODEC]: CodecColumn,
[TableColumn.COMMENT]: CommentColumn,
[TableColumn.COMPOSER]: ComposerColumn,
[TableColumn.DATE_ADDED]: DateAddedColumn,
[TableColumn.DISC_NUMBER]: DiscNumberColumn,
[TableColumn.DURATION]: DurationColumn,
[TableColumn.GENRE]: GenreColumn,
[TableColumn.GENRE_BADGE]: GenreBadgeColumn,
[TableColumn.IMAGE]: ImageColumn,
[TableColumn.LAST_PLAYED]: LastPlayedColumn,
[TableColumn.PATH]: PathColumn,
[TableColumn.PLAY_COUNT]: PlayCountColumn,
[TableColumn.RELEASE_DATE]: ReleaseDateColumn,
[TableColumn.ROW_INDEX]: RowIndexColumn,
[TableColumn.SAMPLE_RATE]: SampleRateColumn,
[TableColumn.SIZE]: SizeColumn,
[TableColumn.TITLE]: TitleColumn,
[TableColumn.TITLE_ARTIST]: TitleArtistColumn,
[TableColumn.TITLE_COMBINED]: TitleCombinedColumn,
[TableColumn.TRACK_NUMBER]: TrackNumberColumn,
[TableColumn.USER_FAVORITE]: FavoriteColumn,
[TableColumn.USER_RATING]: RatingColumn,
[TableColumn.YEAR]: YearColumn,
};
export type DetailListCellComponentProps = ItemDetailListCellProps & { columnId?: string };
export function getDetailListCellComponent(
columnId: string | TableColumn,
): (props: DetailListCellComponentProps) => ReactNode {
const Component = COLUMN_MAP[columnId as TableColumn];
if (Component) {
return Component as (props: DetailListCellComponentProps) => ReactNode;
}
return (props: DetailListCellComponentProps) =>
React.createElement(DefaultColumn, {
columnId: props.columnId ?? (columnId as string),
song: props.song,
});
}
export type { ItemDetailListCellProps } from './types';
export {
ActionsColumn,
AlbumArtistColumn,
AlbumColumn,
ArtistColumn,
BitDepthColumn,
BitRateColumn,
BpmColumn,
ChannelsColumn,
CodecColumn,
CommentColumn,
ComposerColumn,
DateAddedColumn,
DefaultColumn,
DiscNumberColumn,
DurationColumn,
FavoriteColumn,
GenreBadgeColumn,
GenreColumn,
ImageColumn,
LastPlayedColumn,
PathColumn,
PlayCountColumn,
RatingColumn,
ReleaseDateColumn,
RowIndexColumn,
SampleRateColumn,
SizeColumn,
TitleArtistColumn,
TitleColumn,
TitleCombinedColumn,
TrackNumberColumn,
YearColumn,
};
@@ -1,6 +0,0 @@
import { ItemDetailListCellProps } from './types';
import { formatDateRelative } from '/@/renderer/utils/format';
export const LastPlayedColumn = ({ song }: ItemDetailListCellProps) =>
song.lastPlayedAt ? formatDateRelative(song.lastPlayedAt) : <>&nbsp;</>;
@@ -1,3 +0,0 @@
import { ItemDetailListCellProps } from './types';
export const PathColumn = ({ song }: ItemDetailListCellProps) => song.path ?? <>&nbsp;</>;
@@ -1,4 +0,0 @@
import { ItemDetailListCellProps } from './types';
export const PlayCountColumn = ({ song }: ItemDetailListCellProps) =>
song.playCount ? String(song.playCount) : <>&nbsp;</>;
@@ -1,29 +0,0 @@
import { ItemDetailListCellProps } from './types';
import { useIsMutatingRating } from '/@/renderer/features/shared/mutations/set-rating-mutation';
import { Rating } from '/@/shared/components/rating/rating';
import { LibraryItem } from '/@/shared/types/domain-types';
export const RatingColumn = ({ controls, internalState, song }: ItemDetailListCellProps) => {
const isMutatingRating = useIsMutatingRating();
const value = song.userRating ?? 0;
return (
<Rating
onChange={(rating) => {
const index = internalState?.findItemIndex(song.id) ?? -1;
controls?.onRating?.({
event: null,
index,
internalState: internalState ?? undefined,
item: song,
itemType: LibraryItem.SONG,
rating,
});
}}
readOnly={isMutatingRating}
size="xs"
value={value}
/>
);
};
@@ -1,6 +0,0 @@
import { ItemDetailListCellProps } from './types';
import { formatDateAbsoluteUTC } from '/@/renderer/utils/format';
export const ReleaseDateColumn = ({ song }: ItemDetailListCellProps) =>
song.releaseDate ? formatDateAbsoluteUTC(song.releaseDate) : <>&nbsp;</>;
@@ -1,5 +0,0 @@
.icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
}
@@ -1,23 +0,0 @@
import styles from './row-index-column.module.css';
import { ItemDetailListCellProps } from './types';
import { useIsCurrentSong } from '/@/renderer/features/player/hooks/use-is-current-song';
import { usePlayerStatus } from '/@/renderer/store';
import { Icon } from '/@/shared/components/icon/icon';
import { PlayerStatus } from '/@/shared/types/types';
export const RowIndexColumn = ({ rowIndex, song }: ItemDetailListCellProps) => {
const status = usePlayerStatus();
const { isActive } = useIsCurrentSong(song);
const isPlaying = isActive && status === PlayerStatus.PLAYING;
if (isActive) {
return (
<div className={styles.iconWrapper}>
<Icon fill="primary" icon={isPlaying ? 'mediaPlay' : 'mediaPause'} />
</div>
);
}
return <>{String((rowIndex ?? 0) + 1)}</>;
};
@@ -1,4 +0,0 @@
import { ItemDetailListCellProps } from './types';
export const SampleRateColumn = ({ song }: ItemDetailListCellProps) =>
song.sampleRate ? `${song.sampleRate} Hz` : <>&nbsp;</>;
@@ -1,6 +0,0 @@
import { ItemDetailListCellProps } from './types';
import { formatSizeString } from '/@/renderer/utils/format';
export const SizeColumn = ({ song }: ItemDetailListCellProps) =>
song.size ? formatSizeString(song.size) : <>&nbsp;</>;
@@ -1,18 +0,0 @@
import clsx from 'clsx';
import styles from './title-column.module.css';
import { ItemDetailListCellProps } from '/@/renderer/components/item-list/item-detail-list/columns/types';
import { useIsCurrentSong } from '/@/renderer/features/player/hooks/use-is-current-song';
import { ExplicitIndicator } from '/@/shared/components/explicit-indicator/explicit-indicator';
export const TitleArtistColumn = ({ song }: ItemDetailListCellProps) => {
const { isActive } = useIsCurrentSong(song);
return (
<span className={clsx({ [styles.active]: isActive })}>
<ExplicitIndicator explicitStatus={song.explicitStatus} />
{[song.name, song.artistName].filter(Boolean).join(' — ') ?? <>&nbsp;</>}
</span>
);
};
@@ -1,3 +0,0 @@
.active {
color: var(--theme-colors-primary);
}

Some files were not shown because too many files have changed in this diff Show More