Compare commits

...

6 Commits

Author SHA1 Message Date
Hosted Weblate 4228084810 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translated using Weblate (French)

Currently translated at 90.7% (1034 of 1139 strings)

Translated using Weblate (Ukrainian)

Currently translated at 29.0% (331 of 1139 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (1139 of 1139 strings)

Translated using Weblate (Ukrainian)

Currently translated at 26.5% (302 of 1139 strings)

Translated using Weblate (Ukrainian)

Currently translated at 22.0% (251 of 1139 strings)

Translated using Weblate (Danish)

Currently translated at 90.0% (1026 of 1139 strings)

Translated using Weblate (Danish)

Currently translated at 28.1% (321 of 1139 strings)

Translated using Weblate (Ukrainian)

Currently translated at 9.5% (109 of 1139 strings)

Translated using Weblate (Ukrainian)

Currently translated at 5.7% (66 of 1139 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (1139 of 1139 strings)

Translated using Weblate (Czech)

Currently translated at 99.7% (1136 of 1139 strings)

Translated using Weblate (Danish)

Currently translated at 4.8% (55 of 1139 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (1139 of 1139 strings)

Translated using Weblate (Danish)

Currently translated at 0.4% (5 of 1139 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (1139 of 1139 strings)

Translated using Weblate (Polish)

Currently translated at 99.9% (1138 of 1139 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (1135 of 1135 strings)

Translated using Weblate (Dutch)

Currently translated at 99.8% (1133 of 1135 strings)

Translated using Weblate (Dutch)

Currently translated at 84.4% (959 of 1135 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (1135 of 1135 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (1135 of 1135 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (1135 of 1135 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (1131 of 1131 strings)

Translated using Weblate (German)

Currently translated at 78.9% (893 of 1131 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (1131 of 1131 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 92.7% (1047 of 1129 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (1129 of 1129 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (1129 of 1129 strings)

Translated using Weblate (Japanese)

Currently translated at 85.1% (961 of 1129 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (1129 of 1129 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (1129 of 1129 strings)

Co-authored-by: Alexander Welsing <kontakt@a-wels.de>
Co-authored-by: Denisa Alicia Rissa <denisarissa@gmail.com>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Fordas <fordas15@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: KosmoMoustache <kosmomoustache@users.noreply.hosted.weblate.org>
Co-authored-by: Ondo <SparkyOndo@proton.me>
Co-authored-by: York <goog10216922@gmail.com>
Co-authored-by: Yurii <04_hours.lambing@icloud.com>
Co-authored-by: bokse <weblate@bokse.nl>
Co-authored-by: haha4ni <haha4ni@hotmail.com>
Co-authored-by: karigane <169052233+karigane-cha@users.noreply.github.com>
Co-authored-by: skajmer <skajmer@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/ca/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/cs/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/da/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/de/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/es/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/fr/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/ja/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/nl/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/pl/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/uk/
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/zh_Hant/
Translation: feishin/Translation
2026-02-10 09:22:15 +01:00
jeffvli b514c7972d update to v1.5.0 2026-02-10 00:22:00 -08:00
jeffvli 83d9042a47 decouple playlist song sort order from search results (#1650) 2026-02-09 22:28:37 -08:00
jeffvli a28c403093 adjust wording and toggles for analytics/updates (#1654) 2026-02-09 22:19:49 -08:00
jeffvli 2927fa5ff7 set default font to Inter
- prefer to use monospace font due to new item detail columns
2026-02-09 22:00:46 -08:00
Jeff f39a7f8d6f Add album detail list view (#1681) 2026-02-09 21:56:08 -08:00
110 changed files with 5796 additions and 478 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "feishin",
"version": "1.4.2",
"version": "1.5.0",
"description": "A modern self-hosted music player.",
"keywords": [
"subsonic",
+26 -8
View File
@@ -15,7 +15,8 @@
"nowPlaying": "ara sona",
"shared": "$t(entity.playlist, {\"count\": 2}) compartides",
"favorites": "$t(entity.favorite, {\"count\": 2})",
"radio": "$t(entity.radioStation, {\"count\": 2})"
"radio": "$t(entity.radioStation, {\"count\": 2})",
"collections": "col·leccions"
},
"albumArtistDetail": {
"relatedArtists": "$t(entity.artist, {\"count\": 2}) similars",
@@ -191,6 +192,14 @@
},
"radioList": {
"title": "emissores de ràdio"
},
"windowBar": {
"paused": "(en pausa) ",
"privateMode": "(mode privat)"
},
"collections": {
"overrideExisting": "sobreescriu existents",
"saveAsCollection": "desa com a col·lecció"
}
},
"common": {
@@ -289,8 +298,8 @@
"restartRequired": "cal reiniciar",
"sampleRate": "freqüència de mostreig",
"setting_one": "configuració",
"setting_many": "",
"setting_other": "",
"setting_many": "configuracions",
"setting_other": "configuracions",
"trackGain": "guany de pista",
"trackPeak": "pic de pista",
"gap": "espera",
@@ -316,7 +325,8 @@
"example": "exemple",
"mood": "estat d'ànim",
"filter_single": "senzill",
"filter_multiple": "multi"
"filter_multiple": "multi",
"rename": "reanomena"
},
"entity": {
"album_one": "àlbum",
@@ -540,7 +550,6 @@
"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",
@@ -862,7 +871,14 @@
"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"
"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"
},
"table": {
"column": {
@@ -924,7 +940,8 @@
"alternateRowColors": "colors de fila alternants",
"horizontalBorders": "vores de fila",
"rowHoverHighlight": "ressalta en passar el cursor per la fila",
"verticalBorders": "vores de columna"
"verticalBorders": "vores de columna",
"showHeader": "mostra l'encapçalament"
},
"label": {
"actions": "$t(common.action, {\"count\": 2})",
@@ -1013,7 +1030,8 @@
"lastPlayed": "última reproducció",
"path": "ruta",
"songCount": "nombre de cançons",
"explicitStatus": "$t(common.explicitStatus)"
"explicitStatus": "$t(common.explicitStatus)",
"sortName": "ordena per nom"
},
"player": {
"muted": "silenciat",
+18 -4
View File
@@ -98,7 +98,6 @@
"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)",
@@ -284,7 +283,7 @@
"releaseChannel_optionLatest": "nejnovější",
"releaseChannel_optionBeta": "beta",
"releaseChannel": "kanál vydání",
"releaseChannel_description": "vyberte si mezi stabilními vydáními nebo beta vydáními pro automatické aktualizace",
"releaseChannel_description": "vyberte si mezi stabilními, beta nebo alpha (nočními) 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",
@@ -380,7 +379,13 @@
"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ě"
"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í)"
},
"action": {
"editPlaylist": "upravit $t(entity.playlist, {\"count\": 1})",
@@ -899,7 +904,11 @@
"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í"
"groupingTypePrimary": "primární typy vydání",
"favoriteSongs": "oblíbené skladby",
"topSongsCommunity": "komunita",
"topSongsPersonal": "osobní",
"favoriteSongsFrom": "oblíbené skladby od umělce {{title}}"
},
"itemDetail": {
"copiedPath": "cesta úspěšně zkopírována",
@@ -933,6 +942,11 @@
"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": {
+1299 -2
View File
File diff suppressed because it is too large Load Diff
+22 -6
View File
@@ -33,7 +33,8 @@
"createRadioStation": "$t(entity.radioStation, {\"count\": 1}) erstellen",
"deleteRadioStation": "$t(entity.radioStation, {\"count\": 1}) löschen",
"selectAll": "alle auswählen",
"openApplicationDirectory": "Anwendungsverzeichnis öffnen"
"openApplicationDirectory": "Anwendungsverzeichnis öffnen",
"addOrRemoveFromSelection": "Zur Auswahl hinzufügen oder entfernen"
},
"common": {
"backward": "zurück",
@@ -571,7 +572,8 @@
"shared": "$t(entity.playlist, {\"count\": 2}) geteilt",
"myLibrary": "meine bibliothek",
"favorites": "$t(entity.favorite, {\"count\": 2})",
"radio": "$t(entity.radioStation, {\"count\": 2})"
"radio": "$t(entity.radioStation, {\"count\": 2})",
"collections": "Sammlungen"
},
"setting": {
"playbackTab": "Wiedergabe",
@@ -629,7 +631,9 @@
"topSongs": "Toplieder",
"relatedArtists": "ähnliche $t(entity.artist, {\"count\": 2})",
"groupingTypeAll": "alle Veröffentlichungsformate",
"groupingTypePrimary": "primäre Veröffentlichungsformate"
"groupingTypePrimary": "primäre Veröffentlichungsformate",
"favoriteSongs": "Lieblingssongs",
"favoriteSongsFrom": "Liebslingssongs von {{title}}"
},
"manageServers": {
"title": "Servers verwalten",
@@ -655,6 +659,13 @@
},
"radioList": {
"title": "Radiosender"
},
"windowBar": {
"paused": "(Pausiert) ",
"privateMode": "(Privater Modus)"
},
"collections": {
"saveAsCollection": "Als Sammlung speichern"
}
},
"player": {
@@ -693,7 +704,8 @@
"addNextShuffled": "als Nächstes (zufällige Wiedergabe)",
"holdToShuffle": "Halten für Zufallswiedergabe",
"restoreQueueFromServer": "Wiedergabeliste von Server wiederherstellen",
"saveQueueToServer": "Wiedergabeliste auf Server speichern"
"saveQueueToServer": "Wiedergabeliste auf Server speichern",
"lyrics": "Songtexte"
},
"setting": {
"audioDevice_description": "wählen Sie das Audiogerät aus, das für die Wiedergabe verwendet werden soll (nur Webplayer)",
@@ -709,7 +721,6 @@
"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",
@@ -974,7 +985,12 @@
"translationTargetLanguage_description": "die gewünschte Sprache der Übersetzung",
"translationTargetLanguage": "Zielsprache der Übersetzung",
"queryBuilderCustomFields": "benutzerdefiniertes Feld",
"queryBuilderCustomFields_inputTag": "Tag"
"queryBuilderCustomFields_inputTag": "Tag",
"homeFeatureStyle_optionMultiple": "mehrere",
"imageResolution": "Bildauflösung",
"imageResolution_optionTable": "Tabelle",
"imageResolution_optionSidebar": "Seitenleiste",
"preservePitch": "Tonhöhe erhalten"
},
"dragDropZone": {
"error_oneFileOnly": "Bitte wähle nur 1 Datei",
+5 -1
View File
@@ -713,6 +713,8 @@
"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",
@@ -749,7 +751,8 @@
"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",
"disableAutomaticUpdates": "disable automatic updates",
"automaticUpdates": "Automatic updates",
"automaticUpdates_description": "Check for and install updates automatically",
"releaseChannel_optionAlpha": "alpha (nightly)",
"releaseChannel_optionBeta": "beta",
"releaseChannel_optionLatest": "latest",
@@ -1145,6 +1148,7 @@
"year": "$t(common.year)"
},
"view": {
"detail": "detail",
"grid": "grid",
"list": "list",
"table": "table"
+18 -4
View File
@@ -95,7 +95,6 @@
"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",
@@ -283,7 +282,7 @@
"releaseChannel_optionLatest": "Última versión",
"releaseChannel_optionBeta": "Beta",
"releaseChannel": "Canal de lanzamiento",
"releaseChannel_description": "Elige entre lanzamientos estables o beta para las actualizaciones automáticas",
"releaseChannel_description": "Elige entre lanzamientos estables, beta, o alpha (nightly) 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",
@@ -380,7 +379,13 @@
"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"
"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)"
},
"action": {
"editPlaylist": "editar $t(entity.playlist, {\"count\": 1})",
@@ -792,7 +797,11 @@
"about": "Sobre {{artist}}",
"appearsOn": "Aparece en",
"groupingTypeAll": "Todos los tipos de lanzamiento",
"groupingTypePrimary": "Tipos de lanzamiento principales"
"groupingTypePrimary": "Tipos de lanzamiento principales",
"favoriteSongs": "Canciones favoritas",
"favoriteSongsFrom": "Canciones favoritas de {{title}}",
"topSongsPersonal": "Personal",
"topSongsCommunity": "Comunidad"
},
"itemDetail": {
"copiedPath": "Ruta copiada correctamente",
@@ -826,6 +835,11 @@
"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": {
-1
View File
@@ -419,7 +419,6 @@
"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,7 +76,6 @@
"hotkey_volumeDown": "کم کردن صدا",
"audioPlayer_description": "پخش‌کنندهٔ صدا را برای پخش انتخاب کنید",
"hotkey_globalSearch": "جست و جوی سراسری",
"disableAutomaticUpdates": "غیرفعال کردن به‌‌روزرسانی خودکار",
"exitToTray_description": "خروج از اپلیکیشن به system tray",
"replayGainMode_optionAlbum": "$t(entity.album, {\"count\": 1})",
"discordUpdateInterval_description": "فاصلهٔ بین هر به روزرسانی به ثانیه (حداقل ۱۵ ثانیه)",
-1
View File
@@ -375,7 +375,6 @@
"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)",
+10 -9
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": "",
"setting_many": "paramètres",
"setting_other": "paramètres",
"version": "version",
"title": "titre",
@@ -202,7 +202,9 @@
"countSelected": "{{count}} sélectionnée",
"example": "exemple",
"mood": "humeur",
"retry": "réessayer"
"retry": "réessayer",
"filter_single": "unique",
"filter_multiple": "multiple"
},
"error": {
"remotePortWarning": "redémarrer le serveur pour appliquer le nouveau port",
@@ -270,7 +272,7 @@
"albumArtist": "$t(entity.albumArtist, {\"count\": 1})",
"comment": "commentaire",
"recentlyUpdated": "mis à jour récemment",
"channels": "$t(common.channel_other)",
"channels": "$t(common.channel, {\"count\": 2})",
"owner": "$t(common.owner)",
"genre": "$t(entity.genre, {\"count\": 1})",
"albumCount": "$t(entity.album, {\"count\": 2}) total",
@@ -425,7 +427,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})"
@@ -487,7 +489,6 @@
"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",
@@ -717,7 +718,7 @@
"releaseChannel_optionLatest": "dernière",
"releaseChannel_optionBeta": "bêta",
"releaseChannel": "canal de diffusion",
"releaseChannel_description": "choisissez entre les versions stables ou les versions bêta pour les mises à jour automatiques",
"releaseChannel_description": "choisissez entre les versions stables, bêta, ou alpha (nightly) 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",
@@ -1022,9 +1023,9 @@
"artist": "$t(entity.artist, {\"count\": 1})",
"album": "$t(entity.album, {\"count\": 1})",
"biography": "$t(common.biography)",
"channels": "$t(common.channel_other)",
"channels": "$t(common.channel, {\"count\": 2})",
"bitrate": "$t(common.bitrate)",
"actions": "$t(common.action_other)",
"actions": "$t(common.action, {\"count\": 2})",
"favorite": "$t(common.favorite)",
"albumArtist": "$t(entity.albumArtist, {\"count\": 1})",
"rating": "$t(common.rating)",
@@ -1066,7 +1067,7 @@
"artist": "$t(entity.artist, {\"count\": 1})",
"genre": "$t(entity.genre, {\"count\": 1})",
"songCount": "$t(entity.track, {\"count\": 2})",
"channels": "$t(common.channel_other)",
"channels": "$t(common.channel, {\"count\": 2})",
"size": "$t(common.size)",
"codec": "$t(common.codec)",
"owner": "propriétaire",
-1
View File
@@ -666,7 +666,6 @@
"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,7 +648,6 @@
"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,7 +202,6 @@
"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",
+4 -5
View File
@@ -95,7 +95,6 @@
"hotkey_globalSearch": "グローバル検索",
"gaplessAudio_description": "MPV 向けのギャップレス再生を設定します",
"remoteUsername_description": "リモートコントロール サーバーのユーザ名を設定します。 ユーザー名とパスワードの両方が空の場合、認証は無効になります",
"disableAutomaticUpdates": "自動更新を無効化",
"exitToTray_description": "アプリケーション終了ボタンが押された際、システムトレイに格納します",
"followLyric_description": "現在の再生位置に歌詞をスクロールします",
"hotkey_favoritePreviousSong": "$t(common.previousSong) をお気に入り",
@@ -631,10 +630,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": "無効なサーバー",
@@ -923,8 +922,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": "インスタントミックスを優先する",
+348 -17
View File
@@ -119,7 +119,7 @@
"size": "grootte",
"reload": "herlaad",
"setting_one": "instelling",
"setting_other": "",
"setting_other": "instellingen",
"close": "sluiten",
"additionalParticipants": "andere deelnemers",
"newVersion": "een nieuwe versie is geïnstalleerd ({{version}})",
@@ -157,7 +157,9 @@
"example": "voorbeeld",
"mood": "stemming",
"retry": "opnieuw proberen",
"filter_single": "single"
"filter_single": "single",
"rename": "hernoemen",
"filter_multiple": "meerdere"
},
"filter": {
"rating": "rating",
@@ -202,7 +204,8 @@
"songCount": "aantal nummers",
"toYear": "tot jaar",
"trackNumber": "track",
"explicitStatus": "$t(common.explicitStatus)"
"explicitStatus": "$t(common.explicitStatus)",
"sortName": "sorteernaam"
},
"page": {
"contextMenu": {
@@ -238,8 +241,8 @@
"version": "versie {{version}}",
"settings": "$t(common.setting, {\"count\": 2})",
"manageServers": "beheer servers",
"expandSidebar": "sidebar uitklappen",
"collapseSidebar": "sidebar inklappen",
"expandSidebar": "zijbalk uitklappen",
"collapseSidebar": "zijbalk inklappen",
"openBrowserDevtools": "open browser devtools",
"quit": "$t(common.quit)",
"goBack": "terug",
@@ -298,7 +301,11 @@
"viewAllTracks": "bekijk alle $t(entity.track, {\"count\": 2})",
"recentReleases": "recente uitgaven",
"groupingTypeAll": "alle soorten publicaties",
"groupingTypePrimary": "primaire publicatiesoorten"
"groupingTypePrimary": "primaire publicatiesoorten",
"favoriteSongs": "favoriete nummers",
"topSongsCommunity": "community",
"topSongsPersonal": "persoonlijk",
"favoriteSongsFrom": "favoriete nummers van {{title}}"
},
"manageServers": {
"title": "beheer servers",
@@ -384,7 +391,8 @@
"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})"
"radio": "$t(entity.radioStation, {\"count\": 2})",
"collections": "verzamelingen"
},
"trackList": {
"artistTracks": "nummers van {{artist}}",
@@ -396,6 +404,14 @@
},
"folderList": {
"title": "$t(entity.folder, {\"count\": 2})"
},
"windowBar": {
"paused": "(Gepauzeerd) ",
"privateMode": "(Privémodus)"
},
"collections": {
"overrideExisting": "bestaande overschrijven",
"saveAsCollection": "sla op als verzameling"
}
},
"error": {
@@ -440,12 +456,12 @@
"artist_other": "artiesten",
"folderWithCount_one": "{{count}} folder",
"folderWithCount_other": "{{count}} folders",
"albumArtist_one": "album artiest",
"albumArtist_other": "album artiesten",
"albumArtist_one": "albumartiest",
"albumArtist_other": "albumartiesten",
"track_one": "track",
"track_other": "tracks",
"albumArtistCount_one": "{{count}} album artiest",
"albumArtistCount_other": "{{count}} album artiesten",
"albumArtistCount_one": "{{count}} albumartiest",
"albumArtistCount_other": "{{count}} albumartiesten",
"albumWithCount_one": "{{count}} album",
"albumWithCount_other": "{{count}} albums",
"favorite_one": "favoriet",
@@ -473,11 +489,107 @@
"table": {
"column": {
"rating": "rating",
"size": "$t(common.size)"
"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})"
},
"config": {
"label": {
"rating": "$t(common.rating)"
"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"
}
}
},
@@ -503,8 +615,8 @@
"exportImportSettings_importBtn": "importeer instellingen",
"exportImportSettings_importModalTitle": "importeer feishing-instellingen",
"exportImportSettings_importSuccess": "instellingen zijn succesvol geïmporteerd!",
"exportImportSettings_notValidJSON": "het ingevoerde bestand is geen valide JSON",
"exportImportSettings_offendingKeyError": "\"{{offendingKey}}\" is incorrect - {{reason}}",
"exportImportSettings_notValidJSON": "het ingevoerde bestand is geen geldige JSON",
"exportImportSettings_offendingKeyError": "\"{{offendingKey}}\" is onjuist - {{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",
@@ -573,7 +685,6 @@
"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",
@@ -760,7 +871,73 @@
"sidebarPlaylistList": "afspeellijsten zijbalk",
"sidePlayQueueStyle_description": "de stijl van de wachtrij aan de zijkant",
"sidePlayQueueStyle_optionAttached": "aangekoppeld",
"sidePlayQueueStyle_optionDetached": "afgekoppeld"
"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})"
},
"form": {
"addServer": {
@@ -964,5 +1141,159 @@
"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"
}
}
}
}
+21 -10
View File
@@ -73,9 +73,9 @@
"delete": "usuń",
"cancel": "anuluj",
"forceRestartRequired": "zrestartuj aby zastosować zmiany... zamknij powiadomienie aby zrestartować",
"setting_one": "ustawienia",
"setting_few": "",
"setting_many": "",
"setting_one": "ustawienie",
"setting_few": "ustawienia",
"setting_many": "ustawień",
"version": "wersja",
"title": "tytuł",
"filter_one": "filtr",
@@ -176,14 +176,14 @@
"playlist_few": "playlisty",
"playlist_many": "playlist",
"artist_one": "wykonawca",
"artist_few": "wykonawców",
"artist_few": "wykonawcy",
"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": "wykonawców albumu",
"albumArtist_many": "wykonawcy albumów",
"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ępny",
"upNext": "następne",
"lyrics": "tekst",
"related": "powiązane",
"visualizer": "wizualizer",
@@ -577,7 +577,11 @@
"appearsOn": "pojawia się na",
"viewAllTracks": "zobacz wszystko $t(entity.track, {\"count\": 2})",
"groupingTypeAll": "wszystkie typy wydań",
"groupingTypePrimary": "główne typy wydań"
"groupingTypePrimary": "główne typy wydań",
"favoriteSongs": "ulubione piosenki",
"topSongsCommunity": "społeczność",
"topSongsPersonal": "osobiste",
"favoriteSongsFrom": "ulubione piosenki z {{title}}"
},
"itemDetail": {
"copyPath": "kopiuj ścieżkę do schowka",
@@ -611,6 +615,11 @@
"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": {
@@ -684,7 +693,6 @@
"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)",
@@ -728,7 +736,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",
@@ -997,7 +1005,10 @@
"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"
"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)"
},
"table": {
"config": {
-1
View File
@@ -230,7 +230,6 @@
"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,7 +523,6 @@
"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,7 +727,6 @@
"disableLibraryUpdateOnStartup": "отключить проверку новых версий при запуске приложения",
"minimizeToTray_description": "сворачивать приложение в панель уведомлений",
"audioPlayer_description": "укажите, какой аудиоплеер использовать для воспроизведения",
"disableAutomaticUpdates": "отключить проверку обновлений",
"exitToTray_description": "При закрытии приложения - оно останется в панели уведомлений",
"fontType_optionCustom": "пользовательский",
"remotePassword": "пароль к серверу удалённого управления",
-1
View File
@@ -541,7 +541,6 @@
"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,7 +548,6 @@
"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,7 +88,6 @@
"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,7 +609,6 @@
"customCssEnable": "தனிப்பயன் சிஎச்எச் ஐ இயக்கவும்",
"customCssNotice": "எச்சரிக்கை: சில சுத்திகரிப்பு (URL () மற்றும் உள்ளடக்கத்தை அனுமதிக்காதது :) இருக்கும்போது, தனிப்பயன் சிஎச்எச் ஐப் பயன்படுத்துவது இடைமுகத்தை மாற்றுவதன் மூலம் ஆபத்துக்களை ஏற்படுத்தக்கூடும்",
"contextMenu_description": "நீங்கள் ஒரு உருப்படியை வலது சொடுக்கு செய்யும் போது பட்டியலில் காட்டப்பட்டுள்ள உருப்படிகளை மறைக்க உங்களை அனுமதிக்கிறது. சரிபார்க்கப்படாத உருப்படிகள் மறைக்கப்படும்",
"disableAutomaticUpdates": "தானியங்கி புதுப்பிப்புகளை முடக்கு",
"discordApplicationId_description": "{{discord}} பணக்கார இருப்புக்கான பயன்பாட்டு ஐடி (இயல்புநிலை {{defaultId}})",
"discordIdleStatus": "பணக்கார இருப்பு செயலற்ற நிலையைக் காட்டுங்கள்",
"discordIdleStatus_description": "இயக்கப்பட்டால், பிளேயர் சும்மா இருக்கும்போது நிலையைப் புதுப்பிக்கவும்",
-1
View File
@@ -534,7 +534,6 @@
"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}})",
+419 -1
View File
@@ -1,5 +1,423 @@
{
"action": {
"addToFavorites": "додати до $t(entity.favorite, {\"count\": 2})"
"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": "не вдалося створити спільний доступ (чи ввімкнено спільний доступ?)"
}
},
"player": {
"skip": "пропустити"
}
}
-1
View File
@@ -229,7 +229,6 @@
"audioPlayer_description": "选择用于播放的音频播放器",
"globalMediaHotkeys": "全局媒体快捷键",
"gaplessAudio_description": "调整 mpv 无缝音频设置",
"disableAutomaticUpdates": "禁用自动更新",
"followLyric_description": "滚动歌词到当前播放位置",
"audioExclusiveMode": "音频独占模式",
"font": "字体",
+125 -16
View File
@@ -296,7 +296,8 @@
"myLibrary": "我的媒體庫",
"shared": "已分享 $t(entity.playlist, {\"count\": 2})",
"favorites": "$t(entity.favorite, {\"count\": 2})",
"radio": "$t(entity.radioStation, {\"count\": 2})"
"radio": "$t(entity.radioStation, {\"count\": 2})",
"collections": "收藏"
},
"trackList": {
"title": "$t(entity.track, {\"count\": 2})",
@@ -314,7 +315,11 @@
"viewAll": "檢視所有",
"viewAllTracks": "檢視所有$t(entity.track, {\"count\": 2})",
"groupingTypeAll": "所有發佈類型",
"groupingTypePrimary": "主要發佈類型"
"groupingTypePrimary": "主要發佈類型",
"favoriteSongs": "最愛歌曲",
"favoriteSongsFrom": "{{title}} 的最愛歌曲",
"topSongsCommunity": "社群",
"topSongsPersonal": "個人"
},
"manageServers": {
"title": "管理伺服器",
@@ -342,7 +347,17 @@
"title": "電台"
},
"windowBar": {
"paused": "(暫停) "
"paused": "(暫停) ",
"privateMode": "(私人模式)"
},
"collections": {
"overrideExisting": "複寫現有的",
"saveAsCollection": "儲存為收藏"
},
"releasenotes": {
"commitsSinceStable": "提交自 {{stable}}",
"noNewCommits": "在此區間內沒有新的提交",
"noStableReleaseToCompare": "沒有穩定的發行可供比較"
}
},
"player": {
@@ -398,7 +413,7 @@
"accentColor": "強調色",
"accentColor_description": "設定應用程式的強調色",
"applicationHotkeys": "應用程式快捷鍵",
"applicationHotkeys_description": "設定應用程式快捷鍵。切換勾選框來設為全快捷鍵(僅桌面端)",
"applicationHotkeys_description": "設定應用程式快捷鍵。切換勾選框來設為全快捷鍵(僅桌面端)",
"audioDevice": "音訊設備",
"audioDevice_description": "選擇用於播放的音訊設備(僅 web 播放器)",
"audioExclusiveMode": "音訊獨占模式",
@@ -409,7 +424,6 @@
"crossfadeStyle_description": "選擇用於音訊播放器的淡入淡出風格",
"customFontPath": "自定字體路徑",
"customFontPath_description": "設定應用程式使用的自定字體路徑",
"disableAutomaticUpdates": "禁用自動更新",
"disableLibraryUpdateOnStartup": "禁用啟動時檢查新版本",
"discordApplicationId": "{{discord}} 應用程式 id",
"discordApplicationId_description": "{{discord}} rich presence 應用程式 id(預設為 {{defaultId}}",
@@ -431,10 +445,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": "暫停",
@@ -558,7 +572,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",
@@ -618,7 +632,7 @@
"artistBackgroundBlur_description": "調整套用至藝人背景圖片的模糊程度",
"releaseChannel_optionLatest": "最新版本",
"releaseChannel_optionBeta": "測試版",
"releaseChannel_description": "選擇自動更新時要使用穩定版本或是測試版本",
"releaseChannel_description": "選擇自動更新時要使用穩定、測試或是 alpha (每日建構版) 版本",
"discordDisplayType": "{{discord}} presence 顯示類型",
"discordDisplayType_description": "變更您在狀態中正在聆聽的內容",
"discordDisplayType_songname": "歌曲名稱",
@@ -630,8 +644,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": "經過匿名處理的使用情況資料將傳送給開發者,以協助改進應用程式",
@@ -715,7 +729,24 @@
"pathReplace_description": "替換您伺服器的預設檔案路徑",
"pathReplace_optionRemovePrefix": "移除前綴",
"pathReplace_optionAddPrefix": "增加前綴",
"sidebarPlaylistSorting": "側邊欄播放清單排序"
"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 (每日建構版)"
},
"table": {
"config": {
@@ -748,7 +779,8 @@
"alternateRowColors": "隔行上色",
"horizontalBorders": "行邊框線",
"rowHoverHighlight": "滑鼠懸停Highlight",
"verticalBorders": "列邊框線"
"verticalBorders": "列邊框線",
"showHeader": "顯示標題"
},
"label": {
"actions": "$t(common.action, {\"count\": 2})",
@@ -783,7 +815,9 @@
"genreBadge": "$t(entity.genre, {\"count\": 1}) (徽章)",
"image": "圖片",
"bitDepth": "$t(common.bitDepth)",
"sampleRate": "$t(common.sampleRate)"
"sampleRate": "$t(common.sampleRate)",
"composer": "作曲者",
"titleArtist": "$t(common.title) (藝人)"
},
"view": {
"table": "表格",
@@ -926,7 +960,8 @@
"title": "標題",
"toYear": "從年份",
"trackNumber": "曲目",
"explicitStatus": "$t(common.explicitStatus)"
"explicitStatus": "$t(common.explicitStatus)",
"sortName": "排序名稱"
},
"form": {
"addServer": {
@@ -1168,6 +1203,80 @@
"gravity": "重力",
"peakFadeTime": "峰值淡出時間 (毫秒)",
"peakHoldTime": "峰值停留時間 (毫秒)",
"radialSpectrum": "圓形頻譜"
"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"
}
}
}
}
@@ -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',
ALBUM_LIST: 'People, Tags, Studios, SortName, UserData, ProviderIds, ChildCount',
FOLDER: 'Genres, DateCreated, MediaSources, UserData, ParentId',
GENRE: 'ItemCounts',
PLAYLIST_DETAIL: 'Genres, DateCreated, MediaSources, ChildCount, ParentId, SortName',
@@ -1112,7 +1112,7 @@ export const JellyfinController: InternalControllerEndpoint = {
GenreIds: query.genreIds?.join(','),
IncludeItemTypes: 'Audio',
IsFavorite: query.favorite,
Limit: query.limit,
Limit: query.limit === -1 ? undefined : 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,
Limit: query.limit === -1 ? undefined : query.limit,
ParentId: getLibraryId(query.musicFolderId),
Recursive: true,
SearchTerm: query.searchTerm,
@@ -1,84 +0,0 @@
.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 {
}
@@ -1,146 +0,0 @@
// 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;
// };
@@ -192,9 +192,10 @@ export const useDefaultItemListControls = (args?: UseDefaultItemListControlsArgs
onColumnReordered?.(columnIdFrom, columnIdTo, edge);
},
onColumnResized: ({ columnId, width }: { columnId: TableColumn; width: number }) => {
onColumnResized?.(columnId, width);
},
onColumnResized: onColumnResized
? ({ columnId, width }: { columnId: TableColumn; width: number }) =>
onColumnResized(columnId, width)
: undefined,
onDoubleClick: ({ internalState, item, itemType, meta }: DefaultItemControlProps) => {
if (!item || !internalState) {
@@ -241,11 +242,13 @@ 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
// For single-song actions (e.g. image play button), or NEXT/LAST/..., only add the clicked song
// For row double-click with NOW/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 ||
@@ -7,14 +7,19 @@ import { ItemListKey, TableColumn } from '/@/shared/types/types';
interface UseItemListColumnReorderProps {
itemListKey: ItemListKey;
tableKey?: 'detail' | 'main';
}
export const useItemListColumnReorder = ({ itemListKey }: UseItemListColumnReorderProps) => {
export const useItemListColumnReorder = ({
itemListKey,
tableKey = 'main',
}: UseItemListColumnReorderProps) => {
const { setList } = useSettingsStoreActions();
const handleColumnReordered = useCallback(
(columnIdFrom: TableColumn, columnIdTo: TableColumn, edge: Edge | null) => {
const columns = useSettingsStore.getState().lists[itemListKey]?.table.columns;
const list = useSettingsStore.getState().lists[itemListKey];
const columns = tableKey === 'detail' ? list?.detail?.columns : list?.table?.columns;
if (!columns) {
return;
@@ -83,13 +88,20 @@ export const useItemListColumnReorder = ({ itemListKey }: UseItemListColumnReord
// Insert the column at the new position
newColumns.splice(newIndex, 0, updatedMovedColumn);
setList(itemListKey, {
table: {
columns: newColumns,
},
});
if (tableKey === 'detail') {
type SetListData = Parameters<
ReturnType<typeof useSettingsStoreActions>['setList']
>[1];
setList(itemListKey, { detail: { columns: newColumns } } as SetListData);
} else {
setList(itemListKey, {
table: {
columns: newColumns,
},
});
}
},
[itemListKey, setList],
[itemListKey, setList, tableKey],
);
return { handleColumnReordered };
@@ -5,11 +5,18 @@ import { ItemListKey, TableColumn } from '/@/shared/types/types';
interface UseItemListColumnResizeProps {
itemListKey: ItemListKey;
tableKey?: 'detail' | 'main';
}
export const useItemListColumnResize = ({ itemListKey }: UseItemListColumnResizeProps) => {
export const useItemListColumnResize = ({
itemListKey,
tableKey = 'main',
}: UseItemListColumnResizeProps) => {
const { setList } = useSettingsStoreActions();
const columns = useSettingsStore((state) => state.lists[itemListKey]?.table.columns);
const columns = useSettingsStore((state) => {
const list = state.lists[itemListKey];
return tableKey === 'detail' ? list?.detail?.columns : list?.table?.columns;
});
const handleColumnResized = useCallback(
(columnId: TableColumn, width: number) => {
@@ -19,13 +26,20 @@ export const useItemListColumnResize = ({ itemListKey }: UseItemListColumnResize
column.id === columnId ? { ...column, width } : column,
);
setList(itemListKey, {
table: {
columns: updatedColumns,
},
});
if (tableKey === 'detail') {
type SetListData = Parameters<
ReturnType<typeof useSettingsStoreActions>['setList']
>[1];
setList(itemListKey, { detail: { columns: updatedColumns } } as SetListData);
} else {
setList(itemListKey, {
table: {
columns: updatedColumns,
},
});
}
},
[columns, itemListKey, setList],
[columns, itemListKey, setList, tableKey],
);
return { handleColumnResized };
@@ -1,26 +1,29 @@
import { useCallback, useMemo } from 'react';
import { useSearchParams } from 'react-router';
import { useLocation, useNavigationType } from 'react-router';
import { parseIntParam, setSearchParam } from '/@/renderer/utils/query-params';
import { useScrollStore } from '/@/renderer/store/scroll.store';
interface UseItemListScrollPersistProps {
enabled: boolean;
}
export const useItemListScrollPersist = ({ enabled }: UseItemListScrollPersistProps) => {
const [searchParams, setSearchParams] = useSearchParams();
const location = useLocation();
const navigationType = useNavigationType();
const setOffset = useScrollStore((s) => s.setOffset);
const getOffset = useScrollStore((s) => s.getOffset);
const scrollOffset = useMemo(() => parseIntParam(searchParams, 'scrollOffset'), [searchParams]);
const scrollOffset = useMemo(() => {
if (navigationType !== 'POP') return undefined;
return getOffset(location.key);
}, [getOffset, location.key, navigationType]);
const handleOnScrollEnd = useCallback(
(offset: number) => {
if (!enabled) return;
setSearchParams((prev) => setSearchParam(prev, 'scrollOffset', offset), {
replace: true,
});
setOffset(location.key, offset);
},
[enabled, setSearchParams],
[enabled, location.key, setOffset],
);
return { handleOnScrollEnd, scrollOffset };
@@ -0,0 +1,38 @@
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"
/>
);
};
@@ -0,0 +1,23 @@
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}
/>
);
};
@@ -0,0 +1,3 @@
import { ItemDetailListCellProps } from './types';
export const AlbumColumn = ({ song }: ItemDetailListCellProps) => song.album ?? <>&nbsp;</>;
@@ -0,0 +1,23 @@
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}
/>
);
};
@@ -0,0 +1,3 @@
import { ItemDetailListCellProps } from './types';
export const BitDepthColumn = ({ song }: ItemDetailListCellProps) => song.bitDepth;
@@ -0,0 +1,4 @@
import { ItemDetailListCellProps } from './types';
export const BitRateColumn = ({ song }: ItemDetailListCellProps) =>
song.bitRate != null ? `${song.bitRate} kbps` : <>&nbsp;</>;
@@ -0,0 +1,3 @@
import { ItemDetailListCellProps } from './types';
export const BpmColumn = ({ song }: ItemDetailListCellProps) => song.bpm ?? <>&nbsp;</>;
@@ -0,0 +1,4 @@
import { ItemDetailListCellProps } from './types';
export const ChannelsColumn = ({ song }: ItemDetailListCellProps) =>
song.channels != null ? String(song.channels) : <>&nbsp;</>;
@@ -0,0 +1,3 @@
import { ItemDetailListCellProps } from './types';
export const CodecColumn = ({ song }: ItemDetailListCellProps) => song.container ?? <>&nbsp;</>;
@@ -0,0 +1,3 @@
import { ItemDetailListCellProps } from './types';
export const CommentColumn = ({ song }: ItemDetailListCellProps) => song.comment ?? <>&nbsp;</>;
@@ -0,0 +1,7 @@
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(', ');
};
@@ -0,0 +1,6 @@
import { ItemDetailListCellProps } from './types';
import { formatDateAbsolute } from '/@/renderer/utils/format';
export const DateAddedColumn = ({ song }: ItemDetailListCellProps) =>
song.createdAt ? formatDateAbsolute(song.createdAt) : <>&nbsp;</>;
@@ -0,0 +1,11 @@
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);
};
@@ -0,0 +1,3 @@
import { ItemDetailListCellProps } from './types';
export const DiscNumberColumn = ({ song }: ItemDetailListCellProps) => String(song.discNumber ?? 1);
@@ -0,0 +1,5 @@
import formatDuration from 'format-duration';
import { ItemDetailListCellProps } from './types';
export const DurationColumn = ({ song }: ItemDetailListCellProps) => formatDuration(song.duration);
@@ -0,0 +1,54 @@
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"
/>
);
};
@@ -0,0 +1,12 @@
.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;
}
@@ -0,0 +1,46 @@
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>
);
};
@@ -0,0 +1,40 @@
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>
))}
</>
);
};
@@ -0,0 +1,50 @@
.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);
}
@@ -0,0 +1,71 @@
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>
);
};
@@ -0,0 +1,127 @@
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,
};
@@ -0,0 +1,6 @@
import { ItemDetailListCellProps } from './types';
import { formatDateRelative } from '/@/renderer/utils/format';
export const LastPlayedColumn = ({ song }: ItemDetailListCellProps) =>
song.lastPlayedAt ? formatDateRelative(song.lastPlayedAt) : <>&nbsp;</>;
@@ -0,0 +1,3 @@
import { ItemDetailListCellProps } from './types';
export const PathColumn = ({ song }: ItemDetailListCellProps) => song.path ?? <>&nbsp;</>;
@@ -0,0 +1,4 @@
import { ItemDetailListCellProps } from './types';
export const PlayCountColumn = ({ song }: ItemDetailListCellProps) =>
song.playCount ? String(song.playCount) : <>&nbsp;</>;
@@ -0,0 +1,29 @@
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}
/>
);
};
@@ -0,0 +1,6 @@
import { ItemDetailListCellProps } from './types';
import { formatDateAbsoluteUTC } from '/@/renderer/utils/format';
export const ReleaseDateColumn = ({ song }: ItemDetailListCellProps) =>
song.releaseDate ? formatDateAbsoluteUTC(song.releaseDate) : <>&nbsp;</>;
@@ -0,0 +1,5 @@
.icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
}
@@ -0,0 +1,23 @@
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)}</>;
};
@@ -0,0 +1,4 @@
import { ItemDetailListCellProps } from './types';
export const SampleRateColumn = ({ song }: ItemDetailListCellProps) =>
song.sampleRate ? `${song.sampleRate} Hz` : <>&nbsp;</>;
@@ -0,0 +1,6 @@
import { ItemDetailListCellProps } from './types';
import { formatSizeString } from '/@/renderer/utils/format';
export const SizeColumn = ({ song }: ItemDetailListCellProps) =>
song.size ? formatSizeString(song.size) : <>&nbsp;</>;
@@ -0,0 +1,18 @@
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>
);
};
@@ -0,0 +1,3 @@
.active {
color: var(--theme-colors-primary);
}
@@ -0,0 +1,18 @@
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 TitleColumn = ({ song }: ItemDetailListCellProps) => {
const { isActive } = useIsCurrentSong(song);
return (
<span className={clsx({ [styles.active]: isActive })}>
<ExplicitIndicator explicitStatus={song.explicitStatus} />
{song.name ?? <>&nbsp;</>}
</span>
);
};
@@ -0,0 +1,18 @@
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 TitleCombinedColumn = ({ 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>
);
};
@@ -0,0 +1,7 @@
import { ItemDetailListCellProps } from './types';
export const TrackNumberColumn = ({ song }: ItemDetailListCellProps) => {
const disc = song.discNumber ?? 1;
const track = song.trackNumber.toString().padStart(2, '0');
return `${disc}-${track}`;
};
@@ -0,0 +1,14 @@
import { ItemListStateActions } from '/@/renderer/components/item-list/helpers/item-list-state';
import { ItemControls } from '/@/renderer/components/item-list/types';
import { Song } from '/@/shared/types/domain-types';
export interface ItemDetailListCellProps {
controls?: ItemControls;
internalState?: ItemListStateActions;
isMutatingFavorite?: boolean;
isRowHovered?: boolean;
onFavoriteClick?: (song: Song) => void;
rowIndex?: number;
size?: 'compact' | 'default' | 'large';
song: Song;
}
@@ -0,0 +1,4 @@
import { ItemDetailListCellProps } from './types';
export const YearColumn = ({ song }: ItemDetailListCellProps) =>
song.releaseYear ? String(song.releaseYear) : <>&nbsp;</>;
@@ -0,0 +1,556 @@
.container {
position: relative;
flex: 1 1 auto;
width: 100%;
min-height: 0;
overflow: auto;
}
.placeholder {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
.wrapper {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
min-height: 0;
}
.detail-list-header {
display: grid;
flex-shrink: 0;
grid-template-columns: 240px 1fr;
gap: var(--theme-spacing-md);
padding: 0 var(--theme-spacing-md);
font-size: var(--theme-font-size-sm);
user-select: none;
background-color: var(--theme-colors-background);
border-bottom: 1px solid var(--theme-colors-border);
}
.header-left {
display: flex;
align-items: center;
min-width: 0;
overflow: hidden;
}
.header-left-album-name {
overflow: hidden;
text-overflow: ellipsis;
font-size: var(--theme-font-size-sm);
font-weight: 500;
color: var(--theme-colors-foreground);
white-space: nowrap;
}
.header-right {
min-width: 0;
overflow: hidden;
}
.tracks-table-header {
display: flex;
flex-wrap: nowrap;
align-items: center;
width: 100%;
min-width: 0;
}
.tracks-table-header-size-compact {
height: 32px;
min-height: 32px;
}
.tracks-table-header-size-default {
height: 40px;
min-height: 40px;
}
.tracks-table-header-size-large {
height: 48px;
min-height: 48px;
}
.track-header-cell {
position: relative;
display: flex;
align-items: center;
min-width: 0;
min-height: 60%;
padding-right: var(--theme-spacing-sm);
padding-left: var(--theme-spacing-sm);
overflow: visible;
}
.track-header-cell-no-h-padding {
padding-right: 0;
padding-left: 0;
}
.track-header-cell-with-vertical-border {
border-right: 1px solid var(--theme-colors-border);
}
.track-header-cell-dragging {
cursor: grabbing;
opacity: 0.5;
}
.track-header-cell-dragged-over-left::before {
position: absolute;
top: 0;
bottom: 0;
left: 0;
z-index: 10;
width: 3px;
content: '';
background-color: var(--theme-colors-primary);
}
.track-header-cell-dragged-over-right::after {
position: absolute;
top: 0;
right: 0;
bottom: 0;
z-index: 10;
width: 3px;
content: '';
background-color: var(--theme-colors-primary);
}
.track-header-cell:hover .resize-handle {
opacity: 1;
}
.track-header-cell:hover .resize-handle::before {
background-color: var(--theme-colors-border);
}
.resize-handle {
position: absolute;
top: 0;
bottom: 0;
z-index: 10;
width: 2px;
margin-right: -4px;
cursor: col-resize;
background: var(--theme-colors-border);
opacity: 0;
transition: opacity 0.3s ease;
}
/* .resize-handle::before {
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 2px;
content: '';
background-color: transparent;
transition: background-color 0.15s ease;
} */
.resize-handle-left {
left: 0;
margin-right: 0;
margin-left: -4px;
}
.resize-handle-left::before {
right: auto;
left: 0;
}
.resize-handle-right {
right: 0;
margin-right: 0;
}
.resize-handle-dragging {
opacity: 1;
}
.resize-handle:hover {
opacity: 1;
}
.row {
display: grid;
grid-template-columns: 240px 1fr;
gap: var(--theme-spacing-md);
padding: var(--theme-spacing-md);
border-bottom: 1px solid var(--theme-colors-border);
}
.skeleton-column-wrapper {
box-sizing: border-box;
min-width: 0;
}
.image-wrapper {
position: relative;
display: block;
width: 100%;
aspect-ratio: 1;
overflow: hidden;
color: inherit;
text-decoration: none;
border-radius: var(--theme-radius-md);
&::before {
position: absolute;
top: 0;
left: 0;
z-index: 5;
width: 100%;
height: 100%;
pointer-events: none;
content: '';
background-color: rgb(0 0 0);
opacity: 0;
transition: opacity 0.2s ease-in-out;
}
&:hover {
@mixin dark {
&::before {
opacity: 0.7;
}
}
@mixin light {
&::before {
opacity: 0.5;
}
}
}
&:hover .favorite-badge,
&:hover .rating-badge {
opacity: 0;
}
}
.favorite-badge {
position: absolute;
top: -50px;
left: -50px;
z-index: 1;
width: 80px;
height: 80px;
pointer-events: none;
background-color: var(--theme-colors-primary);
box-shadow: 0 0 10px 8px rgb(0 0 0 / 80%);
opacity: 1;
transform: rotate(-45deg);
transition: opacity 0.2s ease-in-out;
}
.rating-badge {
position: absolute;
top: var(--theme-spacing-sm);
right: var(--theme-spacing-sm);
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
padding: var(--theme-spacing-xs) var(--theme-spacing-sm);
font-size: var(--theme-font-size-md);
font-weight: 600;
color: var(--theme-colors-foreground);
text-shadow: 0 1px 2px rgb(0 0 0 / 80%);
pointer-events: none;
background-color: var(--theme-colors-primary);
border-radius: var(--theme-radius-md);
box-shadow: 0 2px 8px rgb(0 0 0 / 50%);
opacity: 1;
transition: opacity 0.2s ease-in-out;
}
.row .image {
object-fit: var(--theme-image-fit);
border-radius: var(--theme-radius-md);
}
.row .metadata {
display: flex;
flex-direction: column;
gap: var(--theme-spacing-xs);
align-items: center;
overflow: hidden;
text-overflow: ellipsis;
font-size: var(--theme-font-size-md);
text-align: center;
}
.row .title {
font-weight: 500;
color: inherit;
text-decoration: none;
}
.row .title:hover {
text-decoration: underline;
}
.row .artist {
font-size: var(--theme-font-size-sm);
color: var(--theme-colors-foreground-muted);
text-decoration: none;
}
.row .artist-plain-text:hover {
text-decoration: underline;
}
.row .metadata-link {
color: inherit;
text-decoration: none;
}
.row .metadata-link:hover {
color: var(--theme-colors-foreground);
text-decoration: underline;
}
.row .metadata-extra {
display: flex;
flex-direction: column;
gap: var(--theme-spacing-xs);
align-items: center;
width: 100%;
font-size: var(--theme-font-size-sm);
color: var(--theme-colors-foreground-muted);
text-align: center;
}
.row .metadata-line {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
text-wrap-style: balance;
white-space: nowrap;
}
.row .metadata-line-clamp-2 {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
white-space: normal;
}
.row .right {
min-width: 0;
overflow: hidden;
}
.row .tracks-table {
display: flex;
flex-direction: column;
width: 100%;
font-size: var(--theme-font-size-sm);
}
.row .track-row {
display: flex;
flex-wrap: nowrap;
align-items: center;
min-width: 0;
overflow: hidden;
}
.row .track-header-cell {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.row .track-cell {
min-width: 0;
padding-right: var(--theme-spacing-sm);
padding-left: var(--theme-spacing-sm);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.row .track-row-size-compact {
height: 32px;
min-height: 32px;
max-height: 32px;
}
.row .track-row-size-default {
height: 40px;
min-height: 40px;
max-height: 40px;
}
.row .track-row-size-large {
height: 48px;
min-height: 48px;
max-height: 48px;
}
.row .track-cell-muted {
color: var(--theme-colors-foreground-muted);
}
.row .track-cell-with-vertical-border {
border-right: 1px solid transparent;
}
.row .track-cell-vertical-border-visible {
border-right-color: var(--theme-colors-border);
}
.row .track-cell-image {
display: flex;
align-self: stretch;
min-height: 0;
max-height: 100%;
padding-right: var(--theme-spacing-sm);
padding-left: var(--theme-spacing-sm);
overflow: hidden;
}
.row .track-cell-no-h-padding {
padding-right: 0;
padding-left: 0;
}
.track-row-dragging {
opacity: 0.5;
}
.track-row.track-row-alternate-even {
background-color: var(--theme-colors-background);
}
.track-row.track-row-alternate-odd {
@mixin dark {
background-color: darken(var(--theme-colors-background), 30%);
}
@mixin light {
background-color: darken(var(--theme-colors-background), 2%);
}
}
.track-row.track-row-selected {
@mixin dark {
background-color: lighten(var(--theme-colors-surface), 5%);
}
@mixin light {
background-color: darken(var(--theme-colors-surface), 5%);
}
}
.track-row.track-row-with-horizontal-border {
border-top: 1px solid transparent;
}
.track-row.track-row-horizontal-border-visible {
border-top-color: var(--theme-colors-border);
}
.track-row.track-row-hover-highlight-enabled {
position: relative;
}
.track-row.track-row-hover-highlight-enabled .track-cell {
position: relative;
z-index: 2;
}
.track-row.track-row-hover-highlight-enabled:hover::before {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1;
pointer-events: none;
content: '';
background-color: var(--theme-colors-surface);
opacity: 0.7;
}
.skeleton-image-container {
justify-content: center;
}
.skeleton-image {
width: 100%;
aspect-ratio: 1;
border-radius: var(--theme-radius-md);
}
.skeleton-title-container {
justify-content: center;
}
.skeleton-title {
width: 75%;
height: 1.25rem;
}
.skeleton-artist-container {
justify-content: center;
}
.skeleton-artist {
width: 50%;
height: 1rem;
}
.skeleton-tracks {
display: flex;
flex-direction: column;
gap: 0;
}
.skeleton-track-row {
display: grid;
grid-template-columns: 40px 1fr 8rem;
gap: var(--theme-spacing-sm);
align-items: center;
padding-right: var(--theme-spacing-sm);
padding-left: var(--theme-spacing-sm);
}
.skeleton-tracks-size-compact .skeleton-track-row {
height: 32px;
padding-top: 0;
padding-bottom: 0;
}
.skeleton-tracks-size-default .skeleton-track-row {
height: 40px;
padding-top: 0;
padding-bottom: 0;
}
.skeleton-tracks-size-large .skeleton-track-row {
height: 48px;
padding-top: 0;
padding-bottom: 0;
}
.skeleton-track-cell {
width: 100%;
height: 1rem;
}
.skeleton-track-cell-title {
width: 100%;
min-width: 0;
height: 1rem;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,65 @@
import { TableColumn } from '/@/shared/types/types';
const FIXED_TRACK_COLUMN_WIDTHS: Partial<Record<TableColumn, number>> = {
[TableColumn.ACTIONS]: 32,
[TableColumn.BIT_DEPTH]: 80,
[TableColumn.BIT_RATE]: 80,
[TableColumn.BPM]: 56,
[TableColumn.CHANNELS]: 80,
[TableColumn.CODEC]: 80,
[TableColumn.DATE_ADDED]: 128,
[TableColumn.DISC_NUMBER]: 36,
[TableColumn.DURATION]: 72,
[TableColumn.RELEASE_DATE]: 128,
[TableColumn.SAMPLE_RATE]: 90,
[TableColumn.TRACK_NUMBER]: 56,
[TableColumn.USER_FAVORITE]: 32,
[TableColumn.USER_RATING]: 64,
[TableColumn.YEAR]: 56,
};
const HOVER_ONLY_COLUMNS: TableColumn[] = [
TableColumn.ACTIONS,
TableColumn.USER_FAVORITE,
TableColumn.USER_RATING,
];
const NO_HORIZONTAL_PADDING_COLUMNS: TableColumn[] = [
TableColumn.ACTIONS,
TableColumn.USER_FAVORITE,
TableColumn.USER_RATING,
];
export function getTrackColumnFixed(columnId: TableColumn): {
fixedWidth: number;
isFixedColumn: boolean;
} {
const width = FIXED_TRACK_COLUMN_WIDTHS[columnId];
return width !== undefined
? { fixedWidth: width, isFixedColumn: true }
: { fixedWidth: 0, isFixedColumn: false };
}
export function isNoHorizontalPaddingColumn(columnId: TableColumn): boolean {
return NO_HORIZONTAL_PADDING_COLUMNS.includes(columnId);
}
export function isTrackColumnHoverOnly(columnId: TableColumn): boolean {
return HOVER_ONLY_COLUMNS.includes(columnId);
}
export function shouldShowHoverOnlyColumnContent(
columnId: TableColumn,
isRowHovered: boolean,
song: { userFavorite?: boolean | null; userRating?: null | number },
): boolean {
if (!HOVER_ONLY_COLUMNS.includes(columnId)) {
return true;
}
return (
isRowHovered ||
(columnId === TableColumn.USER_FAVORITE && song.userFavorite !== false) ||
(columnId === TableColumn.USER_RATING && song.userRating != null)
);
}
@@ -54,6 +54,7 @@ const ImageColumnBase = (props: ItemTableListInnerColumn) => {
itemType: props.itemType,
meta: {
playType,
singleSongOnly: true,
},
});
return;
@@ -58,6 +58,7 @@ export const DefaultTitleCombinedColumn = (props: ItemTableListInnerColumn) => {
itemType: props.itemType,
meta: {
playType,
singleSongOnly: true,
},
});
return;
@@ -200,6 +201,7 @@ export const QueueSongTitleCombinedColumn = (props: ItemTableListInnerColumn) =>
itemType: props.itemType,
meta: {
playType,
singleSongOnly: true,
},
});
return;
@@ -7,8 +7,8 @@ import { useDragDrop } from '/@/renderer/hooks/use-drag-drop';
import { Folder, LibraryItem, QueueSong, Song } from '/@/shared/types/domain-types';
import { DragOperation, DragTarget, DragTargetMap } from '/@/shared/types/drag-and-drop';
interface DragDropState {
dragRef: null | React.Ref<HTMLDivElement>;
interface DragDropState<TElement extends HTMLElement = HTMLDivElement> {
dragRef: null | React.Ref<TElement>;
isDraggedOver: 'bottom' | 'top' | null;
isDragging: boolean;
}
@@ -23,7 +23,7 @@ interface UseItemDragDropStateProps {
playlistId?: string;
}
export const useItemDragDropState = ({
export const useItemDragDropState = <TElement extends HTMLElement = HTMLDivElement>({
enableDrag,
internalState,
isDataRow,
@@ -31,14 +31,14 @@ export const useItemDragDropState = ({
itemType,
playerContext,
playlistId,
}: UseItemDragDropStateProps): DragDropState => {
}: UseItemDragDropStateProps): DragDropState<TElement> => {
const shouldEnableDrag = enableDrag && isDataRow && !!item;
const {
isDraggedOver,
isDragging: isDraggingLocal,
ref: dragRef,
} = useDragDrop<HTMLDivElement>({
} = useDragDrop<TElement>({
drag: {
getId: () => {
if (!item || !isDataRow) {
@@ -26,6 +26,11 @@
padding: var(--theme-spacing-xs) var(--theme-spacing-xl);
}
.container.no-horizontal-padding {
padding-right: 0;
padding-left: 0;
}
.container.center {
align-items: center;
text-align: center;
@@ -205,6 +210,11 @@
padding: 0 var(--theme-spacing-xl);
}
.header-container.no-horizontal-padding {
padding-right: 0;
padding-left: 0;
}
.header-dragging {
cursor: grabbing;
opacity: 0.5;
@@ -26,6 +26,7 @@ import styles from './item-table-list-column.module.css';
import i18n from '/@/i18n/i18n';
import { useItemSelectionState } from '/@/renderer/components/item-list/helpers/item-list-state';
import { isNoHorizontalPaddingColumn } from '/@/renderer/components/item-list/item-detail-list/utils';
import { ActionsColumn } from '/@/renderer/components/item-list/item-table-list/columns/actions-column';
import { AlbumArtistsColumn } from '/@/renderer/components/item-list/item-table-list/columns/album-artists-column';
import { AlbumColumn } from '/@/renderer/components/item-list/item-table-list/columns/album-column';
@@ -479,6 +480,7 @@ export const TableColumnTextContainer = (
[styles.dragging]: isDataRow && isDragging,
[styles.large]: props.size === 'large',
[styles.left]: props.columns[props.columnIndex].align === 'start',
[styles.noHorizontalPadding]: isNoHorizontalPaddingColumn(props.type),
[styles.paddingLg]: props.cellPadding === 'lg',
[styles.paddingMd]: props.cellPadding === 'md',
[styles.paddingSm]: props.cellPadding === 'sm',
@@ -632,6 +634,7 @@ export const TableColumnContainer = (
[styles.dragging]: isDataRow && isDragging,
[styles.large]: props.size === 'large',
[styles.left]: props.columns[props.columnIndex].align === 'start',
[styles.noHorizontalPadding]: isNoHorizontalPaddingColumn(props.type),
[styles.paddingLg]: props.cellPadding === 'lg',
[styles.paddingMd]: props.cellPadding === 'md',
[styles.paddingSm]: props.cellPadding === 'sm',
@@ -850,6 +853,7 @@ export const TableColumnHeaderContainer = (
[styles.headerDraggedOverLeft]: isDraggedOver === 'left',
[styles.headerDraggedOverRight]: isDraggedOver === 'right',
[styles.headerDragging]: isDragging,
[styles.noHorizontalPadding]: isNoHorizontalPaddingColumn(props.type),
[styles.paddingLg]: props.cellPadding === 'lg',
[styles.paddingMd]: props.cellPadding === 'md',
[styles.paddingSm]: props.cellPadding === 'sm',
@@ -881,7 +885,7 @@ export const TableColumnHeaderContainer = (
);
};
const columnLabelMap: Record<TableColumn, ReactNode | string> = {
export const columnLabelMap: Record<TableColumn, ReactNode | string> = {
[TableColumn.ACTIONS]: (
<Flex className={styles.headerIconWrapper}>
<Icon fill="default" icon="ellipsisHorizontal" />
+1 -1
View File
@@ -98,7 +98,7 @@ export interface ItemListTableComponentProps<TQuery> extends ItemListComponentPr
enableRowHoverHighlight?: boolean;
enableSelection?: boolean;
enableVerticalBorders?: boolean;
size?: 'compact' | 'default';
size?: 'compact' | 'default' | 'large';
}
export interface ItemTableListColumnConfig {
@@ -233,8 +233,8 @@ export const AlbumDetailHeader = forwardRef<HTMLDivElement>((_props, ref) => {
{metadataItems.map((item, index) => (
<Fragment key={item.id}>
{index > 0 && (
<Text fw={400} isMuted isNoSelect>
<Text isMuted isNoSelect>
<Separator />
</Text>
)}
<Text fw={400}>{item.value}</Text>
@@ -36,6 +36,18 @@ const AlbumListPaginatedTable = lazy(() =>
})),
);
const AlbumListInfiniteDetail = lazy(() =>
import('/@/renderer/features/albums/components/album-list-infinite-detail').then((module) => ({
default: module.AlbumListInfiniteDetail,
})),
);
const AlbumListPaginatedDetail = lazy(() =>
import('/@/renderer/features/albums/components/album-list-paginated-detail').then((module) => ({
default: module.AlbumListPaginatedDetail,
})),
);
const AlbumListFilters = () => {
return (
<ListWithSidebarContainer.SidebarPortal>
@@ -62,13 +74,16 @@ export const AlbumListContent = () => {
};
const AlbumListSuspenseContainer = () => {
const { display, grid, itemsPerPage, pagination, table } = useListSettings(ItemListKey.ALBUM);
const { detail, display, grid, itemsPerPage, pagination, table } = useListSettings(
ItemListKey.ALBUM,
);
const { customFilters } = useListContext();
return (
<Suspense fallback={<Spinner container />}>
<AlbumListView
detail={detail}
display={display}
grid={grid}
itemsPerPage={itemsPerPage}
@@ -83,13 +98,17 @@ const AlbumListSuspenseContainer = () => {
export type OverrideAlbumListQuery = Omit<Partial<AlbumListQuery>, 'limit' | 'startIndex'>;
export const AlbumListView = ({
detail,
display,
grid,
itemsPerPage,
overrideQuery,
pagination,
table,
}: ItemListSettings & { overrideQuery?: OverrideAlbumListQuery }) => {
}: ItemListSettings & {
detail?: ItemListSettings['detail'];
overrideQuery?: OverrideAlbumListQuery;
}) => {
const server = useCurrentServer();
const { pageKey } = useListContext();
@@ -179,6 +198,32 @@ export const AlbumListView = ({
return null;
}
}
case ListDisplayType.DETAIL: {
switch (pagination) {
case ListPaginationType.INFINITE: {
return (
<AlbumListInfiniteDetail
enableHeader={detail?.enableHeader}
itemsPerPage={itemsPerPage}
query={mergedQuery}
serverId={server.id}
/>
);
}
case ListPaginationType.PAGINATED: {
return (
<AlbumListPaginatedDetail
enableHeader={detail?.enableHeader}
itemsPerPage={itemsPerPage}
query={mergedQuery}
serverId={server.id}
/>
);
}
default:
return null;
}
}
}
return null;
@@ -1,7 +1,10 @@
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { ALBUM_TABLE_COLUMNS } from '/@/renderer/components/item-list/item-table-list/default-columns';
import {
ALBUM_TABLE_COLUMNS,
SONG_TABLE_COLUMNS,
} from '/@/renderer/components/item-list/item-table-list/default-columns';
import { useListContext } from '/@/renderer/context/list-context';
import { useAlbumListFilters } from '/@/renderer/features/albums/hooks/use-album-list-filters';
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
@@ -92,8 +95,15 @@ export const AlbumListHeaderFilters = ({ toggleGenreTarget }: { toggleGenreTarge
<ListRefreshButton listKey={pageKey as ItemListKey} />
</Group>
<Group gap="sm" wrap="nowrap">
<ListDisplayTypeToggleButton listKey={ItemListKey.ALBUM} />
<ListDisplayTypeToggleButton enableDetail listKey={ItemListKey.ALBUM} />
<ListConfigMenu
detailConfig={{
optionsConfig: {
autoFitColumns: { hidden: true },
},
tableColumnsData: SONG_TABLE_COLUMNS,
tableKey: 'detail',
}}
listKey={ItemListKey.ALBUM}
tableColumnsData={ALBUM_TABLE_COLUMNS}
/>
@@ -0,0 +1,69 @@
import { UseSuspenseQueryOptions } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { useItemListInfiniteLoader } from '/@/renderer/components/item-list/helpers/item-list-infinite-loader';
import { useItemListColumnReorder } from '/@/renderer/components/item-list/helpers/use-item-list-column-reorder';
import { useItemListColumnResize } from '/@/renderer/components/item-list/helpers/use-item-list-column-resize';
import { ItemDetailList } from '/@/renderer/components/item-list/item-detail-list/item-detail-list';
import { ItemListComponentProps } from '/@/renderer/components/item-list/types';
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
import {
AlbumListQuery,
AlbumListSort,
LibraryItem,
SortOrder,
} from '/@/shared/types/domain-types';
import { ItemListKey } from '/@/shared/types/types';
interface AlbumListInfiniteDetailProps extends ItemListComponentProps<AlbumListQuery> {
enableHeader?: boolean;
}
export const AlbumListInfiniteDetail = ({
enableHeader = true,
itemsPerPage = 100,
query = {
sortBy: AlbumListSort.NAME,
sortOrder: SortOrder.ASC,
},
serverId,
}: AlbumListInfiniteDetailProps) => {
const listCountQuery = albumQueries.listCount({
query: { ...query },
serverId: serverId,
}) as UseSuspenseQueryOptions<number, Error, number, readonly unknown[]>;
const listQueryFn = api.controller.getAlbumList;
const { handleColumnReordered } = useItemListColumnReorder({
itemListKey: ItemListKey.ALBUM,
tableKey: 'detail',
});
const { handleColumnResized } = useItemListColumnResize({
itemListKey: ItemListKey.ALBUM,
tableKey: 'detail',
});
const { getItem, itemCount, loadedItems, onRangeChanged } = useItemListInfiniteLoader({
eventKey: ItemListKey.ALBUM,
itemsPerPage,
itemType: LibraryItem.ALBUM,
listCountQuery,
listQueryFn,
query,
serverId,
});
return (
<ItemDetailList
data={loadedItems}
enableHeader={enableHeader}
getItem={getItem}
itemCount={itemCount}
onColumnReordered={handleColumnReordered}
onColumnResized={handleColumnResized}
onRangeChanged={onRangeChanged}
/>
);
};
@@ -0,0 +1,80 @@
import { UseSuspenseQueryOptions } from '@tanstack/react-query';
import { api } from '/@/renderer/api';
import { useItemListPaginatedLoader } from '/@/renderer/components/item-list/helpers/item-list-paginated-loader';
import { useItemListColumnReorder } from '/@/renderer/components/item-list/helpers/use-item-list-column-reorder';
import { useItemListColumnResize } from '/@/renderer/components/item-list/helpers/use-item-list-column-resize';
import { ItemDetailList } from '/@/renderer/components/item-list/item-detail-list/item-detail-list';
import { ItemListWithPagination } from '/@/renderer/components/item-list/item-list-pagination/item-list-pagination';
import { useItemListPagination } from '/@/renderer/components/item-list/item-list-pagination/use-item-list-pagination';
import { ItemListComponentProps } from '/@/renderer/components/item-list/types';
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
import {
AlbumListQuery,
AlbumListSort,
LibraryItem,
SortOrder,
} from '/@/shared/types/domain-types';
import { ItemListKey } from '/@/shared/types/types';
interface AlbumListPaginatedDetailProps extends ItemListComponentProps<AlbumListQuery> {
enableHeader?: boolean;
}
export const AlbumListPaginatedDetail = ({
enableHeader = true,
itemsPerPage = 100,
query = {
sortBy: AlbumListSort.NAME,
sortOrder: SortOrder.ASC,
},
serverId,
}: AlbumListPaginatedDetailProps) => {
const listCountQuery = albumQueries.listCount({
query: { ...query },
serverId: serverId,
}) as UseSuspenseQueryOptions<number, Error, number, readonly unknown[]>;
const listQueryFn = api.controller.getAlbumList;
const { handleColumnReordered } = useItemListColumnReorder({
itemListKey: ItemListKey.ALBUM,
tableKey: 'detail',
});
const { handleColumnResized } = useItemListColumnResize({
itemListKey: ItemListKey.ALBUM,
tableKey: 'detail',
});
const { currentPage, onChange } = useItemListPagination();
const { data, pageCount, totalItemCount } = useItemListPaginatedLoader({
currentPage,
eventKey: ItemListKey.ALBUM,
itemsPerPage,
itemType: LibraryItem.ALBUM,
listCountQuery,
listQueryFn,
query,
serverId,
});
return (
<ItemListWithPagination
currentPage={currentPage}
itemsPerPage={itemsPerPage}
onChange={onChange}
pageCount={pageCount}
totalItemCount={totalItemCount}
>
<ItemDetailList
currentPage={currentPage}
enableHeader={enableHeader}
items={data || []}
onColumnReordered={handleColumnReordered}
onColumnResized={handleColumnResized}
/>
</ItemListWithPagination>
);
};
@@ -1,21 +1,28 @@
import { Fragment } from 'react';
import { Fragment, memo } from 'react';
import { generatePath, Link } from 'react-router';
import { AppRoute } from '/@/renderer/router/routes';
import { Text, TextProps } from '/@/shared/components/text/text';
import { AlbumArtist, RelatedAlbumArtist, RelatedArtist } from '/@/shared/types/domain-types';
export const JOINED_ARTISTS_MUTED_PROPS = {
linkProps: { fw: 400, isMuted: true },
rootTextProps: { fw: 400, isMuted: true, size: 'sm' as const },
} as const;
interface JoinedArtistsProps {
artistName: string;
artists: AlbumArtist[] | RelatedAlbumArtist[] | RelatedArtist[];
linkProps?: Partial<Omit<TextProps, 'children' | 'component' | 'to'>>;
readOnly?: boolean;
rootTextProps?: Partial<Omit<TextProps, 'children' | 'component'>>;
}
export const JoinedArtists = ({
const JoinedArtistsComponent = ({
artistName,
artists,
linkProps,
readOnly = false,
rootTextProps,
}: JoinedArtistsProps) => {
const parts: (
@@ -111,7 +118,7 @@ export const JoinedArtists = ({
{artists.map((artist, index) => (
<Fragment key={artist.id || `artist-${index}`}>
{index > 0 && ', '}
{artist.id ? (
{artist.id && !readOnly ? (
<Text
component={Link}
fw={500}
@@ -124,7 +131,7 @@ export const JoinedArtists = ({
{artist.name}
</Text>
) : (
<Text fw={500} {...linkProps}>
<Text component="span" fw={500} {...linkProps}>
{artist.name}
</Text>
)}
@@ -152,7 +159,7 @@ export const JoinedArtists = ({
const { artist, text } = part;
if (artist.id) {
if (artist.id && !readOnly) {
return (
<Text
component={Link}
@@ -169,7 +176,7 @@ export const JoinedArtists = ({
);
}
return (
<Text fw={500} key={`${artist.name}-${index}`} {...linkProps}>
<Text component="span" fw={500} key={`${artist.name}-${index}`} {...linkProps}>
{text}
</Text>
);
@@ -180,7 +187,7 @@ export const JoinedArtists = ({
{unmatchedArtists.map((artist, index) => (
<Fragment key={artist.id}>
{index > 0 && ', '}
{artist.id ? (
{artist.id && !readOnly ? (
<Text
component={Link}
fw={500}
@@ -192,6 +199,10 @@ export const JoinedArtists = ({
>
{artist.name}
</Text>
) : artist.id ? (
<Text component="span" fw={500} {...linkProps}>
{artist.name}
</Text>
) : (
<Text component="span" isMuted>
{artist.name}
@@ -205,6 +216,8 @@ export const JoinedArtists = ({
);
};
export const JoinedArtists = memo(JoinedArtistsComponent);
function escapeRegex(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
@@ -19,6 +19,7 @@ import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer, useShowRatings } from '/@/renderer/store';
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { formatDurationString } from '/@/renderer/utils';
import { SEPARATOR_STRING } from '/@/shared/api/utils';
import { Group } from '/@/shared/components/group/group';
import { Stack } from '/@/shared/components/stack/stack';
import { Text } from '/@/shared/components/text/text';
@@ -160,7 +161,11 @@ export const AlbumArtistDetailHeader = forwardRef((_props, ref: Ref<HTMLDivEleme
.filter((i) => i.enabled)
.map((item, index) => (
<Fragment key={`item-${item.id}-${index}`}>
{index > 0 && <Text isNoSelect></Text>}
{index > 0 && (
<Text isMuted isNoSelect>
{SEPARATOR_STRING}
</Text>
)}
<Text isMuted={item.secondary}>{item.value}</Text>
</Fragment>
))}
@@ -1,14 +1,20 @@
import { useMemo } from 'react';
import { usePlayerSong } from '/@/renderer/store';
import { QueueSong } from '/@/shared/types/domain-types';
import { QueueSong, Song } from '/@/shared/types/domain-types';
export const useIsCurrentSong = (song: QueueSong) => {
export const useIsCurrentSong = (song: QueueSong | Song) => {
const currentSong = usePlayerSong();
const isActive = useMemo(() => {
return song._uniqueId === currentSong?._uniqueId;
}, [song._uniqueId, currentSong?._uniqueId]);
const queueSong = song as QueueSong;
if (queueSong._uniqueId != null && queueSong._uniqueId !== '') {
return queueSong._uniqueId === currentSong?._uniqueId;
}
return song.id === currentSong?.id;
}, [song, currentSong?.id, currentSong?._uniqueId]);
return { isActive };
};
@@ -38,6 +38,7 @@ export const PlaylistDetailSongListGrid = forwardRef<any, PlaylistDetailSongList
if (searchTerm) {
items = searchLibraryItems(items, searchTerm, LibraryItem.SONG);
return items;
}
return sortSongList(items, query.sortBy, query.sortOrder);
@@ -65,6 +65,7 @@ export const PlaylistDetailSongListTable = forwardRef<any, PlaylistDetailSongLis
if (searchTerm) {
items = searchLibraryItems(items, searchTerm, LibraryItem.SONG);
return items;
}
return sortSongList(items, query.sortBy, query.sortOrder);
@@ -10,11 +10,11 @@ import { Switch } from '/@/shared/components/switch/switch';
export const AnalyticsSettings = memo(() => {
const { t } = useTranslation();
const handleToggleAnalytics = (disable: boolean) => {
if (disable) {
localStorage.setItem('umami.disabled', '1');
} else {
const handleSetSendAnalytics = (send: boolean) => {
if (send) {
localStorage.removeItem('umami.disabled');
} else {
localStorage.setItem('umami.disabled', '1');
}
};
@@ -22,12 +22,13 @@ export const AnalyticsSettings = memo(() => {
{
control: (
<Switch
defaultChecked={localStorage.getItem('umami.disabled') === '1'}
onChange={(e) => handleToggleAnalytics(e.currentTarget.checked)}
aria-label={t('setting.analyticsEnable', { postProcess: 'sentenceCase' })}
defaultChecked={localStorage.getItem('umami.disabled') !== '1'}
onChange={(e) => handleSetSendAnalytics(e.currentTarget.checked)}
/>
),
description: t('setting.analyticsDisable_description', { postProcess: 'sentenceCase' }),
title: t('setting.analyticsDisable', { postProcess: 'sentenceCase' }),
description: t('setting.analyticsEnable_description', { postProcess: 'sentenceCase' }),
title: t('setting.analyticsEnable', { postProcess: 'sentenceCase' }),
},
];
@@ -71,26 +71,27 @@ export const UpdateSettings = memo(() => {
{
control: (
<Switch
aria-label="Disable automatic updates"
defaultChecked={settings.disableAutoUpdate}
aria-label={t('setting.automaticUpdates', { postProcess: 'sentenceCase' })}
defaultChecked={!settings.disableAutoUpdate}
disabled={disableAutoUpdates()}
onChange={(e) => {
if (!e) return;
localSettings?.set('disable_auto_updates', e.currentTarget.checked);
const enabled = e.currentTarget.checked;
localSettings?.set('disable_auto_updates', !enabled);
setSettings({
window: {
disableAutoUpdate: e.currentTarget.checked,
disableAutoUpdate: !enabled,
},
});
}}
/>
),
description: t('setting.disableAutomaticUpdates', {
description: t('setting.automaticUpdates', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: disableAutoUpdates(),
title: t('setting.disableAutomaticUpdates', { postProcess: 'sentenceCase' }),
title: t('setting.automaticUpdates', { postProcess: 'sentenceCase' }),
},
];
@@ -16,10 +16,11 @@ export const DisplayTypeToggleButton = ({
}: DisplayTypeToggleButtonProps) => {
const { t } = useTranslation();
const isGrid = displayType === ListDisplayType.GRID;
const isDetail = displayType === ListDisplayType.DETAIL;
return (
<ActionIcon
icon={isGrid ? 'layoutGrid' : 'layoutTable'}
icon={isGrid ? 'layoutGrid' : isDetail ? 'layoutDetail' : 'layoutTable'}
iconProps={{
size: 'lg',
}}
@@ -27,7 +28,9 @@ export const DisplayTypeToggleButton = ({
tooltip={{
label: isGrid
? t('table.config.view.grid', { postProcess: 'sentenceCase' })
: t('table.config.view.table', { postProcess: 'sentenceCase' }),
: isDetail
? t('table.config.view.detail', { postProcess: 'sentenceCase' })
: t('table.config.view.table', { postProcess: 'sentenceCase' }),
}}
variant="subtle"
{...buttonProps}
@@ -37,6 +37,15 @@ const DISPLAY_TYPES = [
),
value: ListDisplayType.GRID,
},
{
label: (
<Group align="center" justify="center" p="sm">
<Icon icon="layoutDetail" size="lg" />
{i18n.t('table.config.view.detail', { postProcess: 'sentenceCase' }) as string}
</Group>
),
value: ListDisplayType.DETAIL,
},
// {
// disabled: true,
// label: (
@@ -63,6 +72,12 @@ export const ListConfigBooleanControl = ({
);
};
export interface ListConfigMenuDetailConfig {
optionsConfig?: ListConfigMenuOptionsConfig['detail'];
tableColumnsData: { label: string; value: string }[];
tableKey: 'detail';
}
export interface ListConfigMenuDisplayTypeConfig {
disabled?: boolean;
hidden?: boolean;
@@ -75,6 +90,9 @@ export interface ListConfigMenuOptionConfig {
}
export interface ListConfigMenuOptionsConfig {
detail?: {
[key: string]: ListConfigMenuOptionConfig;
};
grid?: {
[key: string]: ListConfigMenuOptionConfig;
};
@@ -85,6 +103,7 @@ export interface ListConfigMenuOptionsConfig {
interface ListConfigMenuProps {
buttonProps?: ActionIconProps;
detailConfig?: ListConfigMenuDetailConfig;
displayTypes?: ListConfigMenuDisplayTypeConfig[];
listKey: ItemListKey;
optionsConfig?: ListConfigMenuOptionsConfig;
@@ -172,6 +191,20 @@ const Config = ({
...props
}: ListConfigMenuProps & { displayType: ListDisplayType }) => {
switch (displayType) {
case ListDisplayType.DETAIL:
if (props.detailConfig) {
return (
<TableConfig
enablePinColumnButtons={false}
listKey={props.listKey}
optionsConfig={props.detailConfig.optionsConfig}
tableColumnsData={props.detailConfig.tableColumnsData}
tableKey="detail"
/>
);
}
return null;
case ListDisplayType.GRID:
return (
<GridConfig
@@ -3,21 +3,47 @@ import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store';
import { ItemListKey, ListDisplayType } from '/@/shared/types/types';
interface ListDisplayTypeToggleButtonProps {
enableDetail?: boolean;
listKey: ItemListKey;
}
export const ListDisplayTypeToggleButton = ({ listKey }: ListDisplayTypeToggleButtonProps) => {
export const ListDisplayTypeToggleButton = ({
enableDetail = false,
listKey,
}: ListDisplayTypeToggleButtonProps) => {
const displayType = useSettingsStore(
(state) => state.lists[listKey]?.display,
) as ListDisplayType;
const { setList } = useSettingsStoreActions();
const handleToggleDisplayType = () => {
const newDisplayType =
displayType === ListDisplayType.GRID ? ListDisplayType.TABLE : ListDisplayType.GRID;
let newDisplayType: ListDisplayType;
if (enableDetail) {
if (displayType === ListDisplayType.DETAIL) {
newDisplayType = ListDisplayType.TABLE;
} else if (displayType === ListDisplayType.TABLE) {
newDisplayType = ListDisplayType.GRID;
} else if (displayType === ListDisplayType.GRID) {
newDisplayType = ListDisplayType.DETAIL;
} else {
newDisplayType = ListDisplayType.GRID;
}
} else {
if (displayType === ListDisplayType.GRID) {
newDisplayType = ListDisplayType.TABLE;
} else if (displayType === ListDisplayType.TABLE) {
newDisplayType = ListDisplayType.GRID;
} else {
newDisplayType = ListDisplayType.GRID;
}
}
setList(listKey, {
display: newDisplayType,
});
return;
};
return <DisplayTypeToggleButton displayType={displayType} onToggle={handleToggleDisplayType} />;
@@ -21,7 +21,12 @@ import {
ListConfigBooleanControl,
ListConfigTable,
} from '/@/renderer/features/shared/components/list-config-menu';
import { ItemListSettings, useSettingsStore, useSettingsStoreActions } from '/@/renderer/store';
import {
type DataTableProps,
ItemListSettings,
useSettingsStore,
useSettingsStoreActions,
} from '/@/renderer/store';
import { ActionIcon, ActionIconGroup } from '/@/shared/components/action-icon/action-icon';
import { Badge } from '/@/shared/components/badge/badge';
import { Checkbox } from '/@/shared/components/checkbox/checkbox';
@@ -39,6 +44,7 @@ import { dndUtils, DragData, DragOperation, DragTarget } from '/@/shared/types/d
import { ItemListKey, ListPaginationType } from '/@/shared/types/types';
interface TableConfigProps {
enablePinColumnButtons?: boolean;
extraOptions?: {
component: React.ReactNode;
id: string;
@@ -52,19 +58,37 @@ interface TableConfigProps {
};
};
tableColumnsData: { label: string; value: string }[];
tableKey?: 'detail' | 'main';
}
export const TableConfig = ({
enablePinColumnButtons = true,
extraOptions,
listKey,
optionsConfig,
tableColumnsData,
tableKey = 'main',
}: TableConfigProps) => {
const { t } = useTranslation();
const list = useSettingsStore((state) => state.lists[listKey]) as ItemListSettings;
const { setList } = useSettingsStoreActions();
const table = tableKey === 'detail' ? (list?.detail ?? list?.table) : list?.table;
const setTableUpdate = useCallback(
(patch: Partial<DataTableProps>) => {
if (tableKey === 'detail') {
setList(listKey, { detail: patch } as Parameters<
ReturnType<typeof useSettingsStoreActions>['setList']
>[1]);
} else {
setList(listKey, { table: patch });
}
},
[listKey, setList, tableKey],
);
const advancedSettings = useMemo(() => {
const allOptions = [
{
@@ -152,12 +176,12 @@ export const TableConfig = ({
},
]}
onChange={(value) =>
setList(listKey, {
table: { size: value as 'compact' | 'default' },
setTableUpdate({
size: value as 'compact' | 'default' | 'large',
})
}
size="sm"
value={list.table.size}
value={table?.size ?? 'default'}
w="100%"
/>
),
@@ -169,8 +193,8 @@ export const TableConfig = ({
{
component: (
<ListConfigBooleanControl
onChange={(e) => setList(listKey, { table: { enableHeader: e } })}
value={list.table.enableHeader}
onChange={(e) => setTableUpdate({ enableHeader: e })}
value={table.enableHeader}
/>
),
id: 'enableHeader',
@@ -181,10 +205,8 @@ export const TableConfig = ({
{
component: (
<ListConfigBooleanControl
onChange={(e) =>
setList(listKey, { table: { enableRowHoverHighlight: e } })
}
value={list.table.enableRowHoverHighlight}
onChange={(e) => setTableUpdate({ enableRowHoverHighlight: e })}
value={table.enableRowHoverHighlight}
/>
),
id: 'enableRowHoverHighlight',
@@ -195,10 +217,8 @@ export const TableConfig = ({
{
component: (
<ListConfigBooleanControl
onChange={(e) =>
setList(listKey, { table: { enableAlternateRowColors: e } })
}
value={list.table.enableAlternateRowColors}
onChange={(e) => setTableUpdate({ enableAlternateRowColors: e })}
value={table.enableAlternateRowColors}
/>
),
id: 'enableAlternateRowColors',
@@ -209,10 +229,8 @@ export const TableConfig = ({
{
component: (
<ListConfigBooleanControl
onChange={(e) =>
setList(listKey, { table: { enableHorizontalBorders: e } })
}
value={list.table.enableHorizontalBorders}
onChange={(e) => setTableUpdate({ enableHorizontalBorders: e })}
value={table.enableHorizontalBorders}
/>
),
id: 'enableHorizontalBorders',
@@ -223,8 +241,8 @@ export const TableConfig = ({
{
component: (
<ListConfigBooleanControl
onChange={(e) => setList(listKey, { table: { enableVerticalBorders: e } })}
value={list.table.enableVerticalBorders}
onChange={(e) => setTableUpdate({ enableVerticalBorders: e })}
value={table.enableVerticalBorders}
/>
),
id: 'enableVerticalBorders',
@@ -235,8 +253,10 @@ export const TableConfig = ({
{
component: (
<ListConfigBooleanControl
onChange={(e) => setList(listKey, { table: { autoFitColumns: e } })}
value={list.table.autoFitColumns}
onChange={(e) => setTableUpdate({ autoFitColumns: e })}
value={
tableKey === 'main' ? (table as DataTableProps).autoFitColumns : false
}
/>
),
id: 'autoFitColumns',
@@ -256,7 +276,18 @@ export const TableConfig = ({
return option;
})
.filter((option): option is NonNullable<typeof option> => option !== null);
}, [extraOptions, listKey, optionsConfig, setList, t, list]);
}, [
t,
list.pagination,
list.itemsPerPage,
table,
tableKey,
extraOptions,
setList,
listKey,
setTableUpdate,
optionsConfig,
]);
return (
<>
@@ -264,8 +295,9 @@ export const TableConfig = ({
<Divider />
<TableColumnConfig
data={tableColumnsData}
onChange={(columns) => setList(listKey, { table: { columns } })}
value={list.table.columns}
enablePinColumnButtons={enablePinColumnButtons}
onChange={(columns) => setTableUpdate({ columns })}
value={table.columns}
/>
</>
);
@@ -273,10 +305,12 @@ export const TableConfig = ({
const TableColumnConfig = ({
data,
enablePinColumnButtons,
onChange,
value,
}: {
data: { label: string; value: string }[];
enablePinColumnButtons: boolean;
onChange: (value: ItemTableListColumnConfig[]) => void;
value: ItemTableListColumnConfig[];
}) => {
@@ -473,6 +507,7 @@ const TableColumnConfig = ({
<div style={{ userSelect: 'none' }}>
{filteredColumns.map(({ item, matches }) => (
<TableColumnItem
enablePinColumnButtons={enablePinColumnButtons}
handleAlignCenter={handleAlignCenter}
handleAlignLeft={handleAlignLeft}
handleAlignRight={handleAlignRight}
@@ -516,6 +551,7 @@ const DragHandle = ({
const TableColumnItem = memo(
({
enablePinColumnButtons,
handleAlignCenter,
handleAlignLeft,
handleAlignRight,
@@ -531,6 +567,7 @@ const TableColumnItem = memo(
label,
matches,
}: {
enablePinColumnButtons: boolean;
handleAlignCenter: (item: ItemTableListColumnConfig) => void;
handleAlignLeft: (item: ItemTableListColumnConfig) => void;
handleAlignRight: (item: ItemTableListColumnConfig) => void;
@@ -667,32 +704,34 @@ const TableColumnItem = memo(
variant="subtle"
/>
</ActionIconGroup>
<ActionIconGroup className={styles.group}>
<ActionIcon
icon="arrowLeftToLine"
iconProps={{ size: 'md' }}
onClick={() => handlePinToLeft(item)}
size="xs"
tooltip={{
label: t('table.config.general.pinToLeft', {
postProcess: 'sentenceCase',
}),
}}
variant={item.pinned === 'left' ? 'filled' : 'subtle'}
/>
<ActionIcon
icon="arrowRightToLine"
iconProps={{ size: 'md' }}
onClick={() => handlePinToRight(item)}
size="xs"
tooltip={{
label: t('table.config.general.pinToRight', {
postProcess: 'sentenceCase',
}),
}}
variant={item.pinned === 'right' ? 'filled' : 'subtle'}
/>
</ActionIconGroup>
{enablePinColumnButtons && (
<ActionIconGroup className={styles.group}>
<ActionIcon
icon="arrowLeftToLine"
iconProps={{ size: 'md' }}
onClick={() => handlePinToLeft(item)}
size="xs"
tooltip={{
label: t('table.config.general.pinToLeft', {
postProcess: 'sentenceCase',
}),
}}
variant={item.pinned === 'left' ? 'filled' : 'subtle'}
/>
<ActionIcon
icon="arrowRightToLine"
iconProps={{ size: 'md' }}
onClick={() => handlePinToRight(item)}
size="xs"
tooltip={{
label: t('table.config.general.pinToRight', {
postProcess: 'sentenceCase',
}),
}}
variant={item.pinned === 'right' ? 'filled' : 'subtle'}
/>
</ActionIconGroup>
)}
<ActionIconGroup className={styles.group}>
<ActionIcon
icon="alignLeft"
@@ -772,6 +811,7 @@ const TableColumnItem = memo(
(prevProps, nextProps) => {
// Custom comparison function for better memoization
return (
prevProps.enablePinColumnButtons === nextProps.enablePinColumnButtons &&
prevProps.item.id === nextProps.item.id &&
prevProps.item.isEnabled === nextProps.item.isEnabled &&
prevProps.item.autoSize === nextProps.item.autoSize &&
@@ -520,6 +520,28 @@ export const applyFavoriteOptimisticUpdates = (
}
});
const songListQueryKey = queryKeys.songs.list(variables.apiClientProps.serverId);
const songListQueries = queryClient.getQueriesData({
exact: false,
queryKey: songListQueryKey,
});
songListQueries.forEach(([queryKey, data]) => {
if (data) {
pendingUpdates.push({
previousData: data,
queryKey,
updater: (prev: undefined | { items: Song[] }) => {
if (!prev) return prev;
const updatedItems = updateItemInArray(prev.items, itemIdSet, (item) =>
createFavoriteUpdater<Song>(item),
);
return updatedItems ? { ...prev, items: updatedItems } : prev;
},
});
}
});
const topSongsQueryKey = queryKeys.albumArtists.topSongs(
variables.apiClientProps.serverId,
);
@@ -679,6 +701,7 @@ export const applyFavoriteOptimisticUpdatesDeferred = (
queryKeys.playlists.songList(variables.apiClientProps.serverId),
'playlist-song-list',
);
collectQueries(queryKeys.songs.list(variables.apiClientProps.serverId), 'song-list');
collectQueries(
queryKeys.albumArtists.topSongs(variables.apiClientProps.serverId),
'top-songs',
@@ -742,6 +765,7 @@ export const applyFavoriteOptimisticUpdatesDeferred = (
case 'album-list':
case 'artist-list':
case 'playlist-song-list':
case 'song-list':
case 'top-songs': {
const updatedItems = updateItemInArray(
prev.items || [],
@@ -519,6 +519,28 @@ export const applyRatingOptimisticUpdates = (
}
});
const songListQueryKey = queryKeys.songs.list(variables.apiClientProps.serverId);
const songListQueries = queryClient.getQueriesData({
exact: false,
queryKey: songListQueryKey,
});
songListQueries.forEach(([queryKey, data]) => {
if (data) {
pendingUpdates.push({
previousData: data,
queryKey,
updater: (prev: undefined | { items: Song[] }) => {
if (!prev) return prev;
const updatedItems = updateItemInArray(prev.items, itemIdSet, (item) =>
createRatingUpdater<Song>(item),
);
return updatedItems ? { ...prev, items: updatedItems } : prev;
},
});
}
});
const topSongsQueryKey = queryKeys.albumArtists.topSongs(
variables.apiClientProps.serverId,
);
@@ -652,6 +674,7 @@ export const applyRatingOptimisticUpdatesDeferred = (
queryKeys.songs.detail(variables.apiClientProps.serverId),
'song-detail',
);
collectQueries(queryKeys.songs.list(variables.apiClientProps.serverId), 'song-list');
collectQueries(
queryKeys.albumArtists.topSongs(variables.apiClientProps.serverId),
'top-songs',
@@ -712,6 +735,7 @@ export const applyRatingOptimisticUpdatesDeferred = (
case 'album-artist-list':
case 'album-list':
case 'artist-list':
case 'song-list':
case 'top-songs': {
const updatedItems = updateItemInArray(
prev.items || [],

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