Compare commits

...

7 Commits

Author SHA1 Message Date
jeffvli ffe59b2c78 refactor scrobbling to use duration instead of progress (#2010)
- add scrobble status debug and indicator
- add force / reset scrobble
2026-05-12 22:04:46 -07:00
jeffvli 4226da94ec add Mantine progress component 2026-05-12 21:43:35 -07:00
jeffvli 6e634972c9 add transparent icon color variant 2026-05-12 21:43:27 -07:00
Hosted Weblate 984b85e0e9 Translated using Weblate
Currently translated at 75.6% (914 of 1208 strings) (Chinese (Traditional Han script))
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/zh_Hant/

Translated using Weblate

Currently translated at 100.0% (1208 of 1208 strings) (Spanish)
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/es/

Translated using Weblate

Currently translated at 100.0% (1208 of 1208 strings) (Czech)
Translation: feishin/Translation
Translate-URL: https://hosted.weblate.org/projects/feishin/translation/cs/

Co-authored-by: Fjuro <fjuro@users.noreply.hosted.weblate.org>
Co-authored-by: Fordas <fordas15@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: linger <linger0517@gmail.com>
2026-05-13 02:48:17 +00:00
jeffvli 9bb38e1df3 apply additional name sort to RELEASE_DATE album list sort (#2022) 2026-05-12 19:48:03 -07:00
jeffvli 4ecd8271a2 add transcode and playback filters to env settings (#2018) 2026-05-12 19:39:28 -07:00
jeffvli ce7a319d2f fix entity locales to titleCase 2026-05-12 18:50:03 -07:00
27 changed files with 743 additions and 227 deletions
+3
View File
@@ -66,6 +66,9 @@ These variables override app settings **on first run** when no persisted setting
| `playback.scrobble.scrobbleAtDuration` | `240` | `FS_PLAYBACK_SCROBBLE_AT_DURATION` | Seconds of playback before scrobble. | | `playback.scrobble.scrobbleAtDuration` | `240` | `FS_PLAYBACK_SCROBBLE_AT_DURATION` | Seconds of playback before scrobble. |
| `playback.scrobble.scrobbleAtPercentage` | `75` | `FS_PLAYBACK_SCROBBLE_AT_PERCENTAGE` | Percentage of track before scrobble. | | `playback.scrobble.scrobbleAtPercentage` | `75` | `FS_PLAYBACK_SCROBBLE_AT_PERCENTAGE` | Percentage of track before scrobble. |
| `playback.transcode.enabled` | `false` | `FS_PLAYBACK_TRANSCODE_ENABLED` | `true` / `false` — Enable transcoding. | | `playback.transcode.enabled` | `false` | `FS_PLAYBACK_TRANSCODE_ENABLED` | `true` / `false` — Enable transcoding. |
| `playback.transcode.format` | *(unset)* | `FS_PLAYBACK_TRANSCODE_FORMAT` | Transcode format string (codec/container), e.g. server-specific value. Empty = use default. |
| `playback.transcode.bitrate` | *(unset)* | `FS_PLAYBACK_TRANSCODE_BITRATE` | Transcode bitrate (number, kbps or as defined by server). |
| `playback.filters` | `[]` | `FS_PLAYBACK_FILTERS` | JSON array of player filters: each object needs `id`, `field`, `operator`, `value`; optional `isEnabled`. Invalid JSON or shape is ignored. |
--- ---
+3
View File
@@ -58,6 +58,9 @@ window.FS_PLAYBACK_SCROBBLE_NOTIFY = "${FS_PLAYBACK_SCROBBLE_NOTIFY}";
window.FS_PLAYBACK_SCROBBLE_AT_DURATION = "${FS_PLAYBACK_SCROBBLE_AT_DURATION}"; window.FS_PLAYBACK_SCROBBLE_AT_DURATION = "${FS_PLAYBACK_SCROBBLE_AT_DURATION}";
window.FS_PLAYBACK_SCROBBLE_AT_PERCENTAGE = "${FS_PLAYBACK_SCROBBLE_AT_PERCENTAGE}"; window.FS_PLAYBACK_SCROBBLE_AT_PERCENTAGE = "${FS_PLAYBACK_SCROBBLE_AT_PERCENTAGE}";
window.FS_PLAYBACK_TRANSCODE_ENABLED = "${FS_PLAYBACK_TRANSCODE_ENABLED}"; window.FS_PLAYBACK_TRANSCODE_ENABLED = "${FS_PLAYBACK_TRANSCODE_ENABLED}";
window.FS_PLAYBACK_TRANSCODE_FORMAT = "${FS_PLAYBACK_TRANSCODE_FORMAT}";
window.FS_PLAYBACK_TRANSCODE_BITRATE = "${FS_PLAYBACK_TRANSCODE_BITRATE}";
window.FS_PLAYBACK_FILTERS = "${FS_PLAYBACK_FILTERS}";
window.FS_DISCORD_ENABLED = "${FS_DISCORD_ENABLED}"; window.FS_DISCORD_ENABLED = "${FS_DISCORD_ENABLED}";
window.FS_DISCORD_CLIENT_ID = "${FS_DISCORD_CLIENT_ID}"; window.FS_DISCORD_CLIENT_ID = "${FS_DISCORD_CLIENT_ID}";
+11 -11
View File
@@ -344,30 +344,30 @@
}, },
"entity": { "entity": {
"album_one": "Àlbum", "album_one": "Àlbum",
"album_many": "àlbums", "album_many": "Àlbums",
"album_other": "Àlbums", "album_other": "Àlbums",
"albumWithCount_one": "{{count}} àlbum", "albumWithCount_one": "{{count}} àlbum",
"albumWithCount_many": "{{count}} àlbums", "albumWithCount_many": "{{count}} àlbums",
"albumWithCount_other": "{{count}} àlbums", "albumWithCount_other": "{{count}} àlbums",
"albumArtist_one": "Artista de l'àlbum", "albumArtist_one": "Artista de l'àlbum",
"albumArtist_many": "artistes de l'àlbum", "albumArtist_many": "Artistes de l'àlbum",
"albumArtist_other": "Artistes de l'àlbum", "albumArtist_other": "Artistes de l'àlbum",
"albumArtistCount_one": "{{count}} artista de l'àlbum", "albumArtistCount_one": "{{count}} artista de l'àlbum",
"albumArtistCount_many": "{{count}} artistes de l'àlbum", "albumArtistCount_many": "{{count}} artistes de l'àlbum",
"albumArtistCount_other": "{{count}} artistes de l'àlbum", "albumArtistCount_other": "{{count}} artistes de l'àlbum",
"artist_one": "Artista", "artist_one": "Artista",
"artist_many": "artistes", "artist_many": "Artistes",
"artist_other": "Artistes", "artist_other": "Artistes",
"artistWithCount_one": "{{count}} artista", "artistWithCount_one": "{{count}} artista",
"artistWithCount_many": "{{count}} artistes", "artistWithCount_many": "{{count}} artistes",
"artistWithCount_other": "{{count}} artistes", "artistWithCount_other": "{{count}} artistes",
"playlist_one": "Llista de reproducció", "playlist_one": "Llista de reproducció",
"playlist_many": "llistes de reproducció", "playlist_many": "Llistes de reproducció",
"playlist_other": "Llistes de reproducció", "playlist_other": "Llistes de reproducció",
"playlistWithCount_one": "{{count}} llista de reproducció", "playlistWithCount_one": "{{count}} llista de reproducció",
"playlistWithCount_many": "{{count}} llistes de reproducció", "playlistWithCount_many": "{{count}} llistes de reproducció",
"playlistWithCount_other": "{{count}} llistes de reproducció", "playlistWithCount_other": "{{count}} llistes de reproducció",
"smartPlaylist": "$t(entity.playlist, {\"count\": 1}) intel·ligent", "smartPlaylist": "$t(entity.playlist, {\"count\": 1}) Intel·ligent",
"play_one": "{{count}} reproducció", "play_one": "{{count}} reproducció",
"play_many": "{{count}} reproduccions", "play_many": "{{count}} reproduccions",
"play_other": "{{count}} reproduccions", "play_other": "{{count}} reproduccions",
@@ -378,25 +378,25 @@
"genreWithCount_many": "{{count}} gèneres", "genreWithCount_many": "{{count}} gèneres",
"genreWithCount_other": "{{count}} gèneres", "genreWithCount_other": "{{count}} gèneres",
"track_one": "Pista", "track_one": "Pista",
"track_many": "pistes", "track_many": "Pistes",
"track_other": "Pistes", "track_other": "Pistes",
"trackWithCount_one": "{{count}} pista", "trackWithCount_one": "{{count}} pista",
"trackWithCount_many": "{{count}} pistes", "trackWithCount_many": "{{count}} pistes",
"trackWithCount_other": "{{count}} pistes", "trackWithCount_other": "{{count}} pistes",
"folder_one": "Carpeta", "folder_one": "Carpeta",
"folder_many": "carpetes", "folder_many": "Carpetes",
"folder_other": "Carpetes", "folder_other": "Carpetes",
"genre_one": "Gènere", "genre_one": "Gènere",
"genre_many": "gèneres", "genre_many": "Gèneres",
"genre_other": "Gèneres", "genre_other": "Gèneres",
"song_one": "Cançó", "song_one": "Cançó",
"song_many": "cançons", "song_many": "Cançons",
"song_other": "Cançons", "song_other": "Cançons",
"favorite_one": "Preferit", "favorite_one": "Preferit",
"favorite_many": "preferits", "favorite_many": "Preferits",
"favorite_other": "Preferits", "favorite_other": "Preferits",
"radioStation_one": "Emissora de ràdio", "radioStation_one": "Emissora de ràdio",
"radioStation_many": "emissores de ràdio", "radioStation_many": "Emissores de ràdio",
"radioStation_other": "Emissores de ràdio", "radioStation_other": "Emissores de ràdio",
"radioStationWithCount_one": "{{count}} emissora de ràdio", "radioStationWithCount_one": "{{count}} emissora de ràdio",
"radioStationWithCount_many": "{{count}} emissores de ràdio", "radioStationWithCount_many": "{{count}} emissores de ràdio",
+16 -16
View File
@@ -246,7 +246,7 @@
"albumBackground_description": "Přidá obrázek alba na pozadí pro stránky alba obsahující obrázky alba", "albumBackground_description": "Přidá obrázek alba na pozadí pro stránky alba obsahující obrázky alba",
"albumBackgroundBlur": "Velikost rozostření obrázku alba na pozadí", "albumBackgroundBlur": "Velikost rozostření obrázku alba na pozadí",
"albumBackgroundBlur_description": "Upraví množství rozostření použité na obrázek alba na pozadí", "albumBackgroundBlur_description": "Upraví množství rozostření použité na obrázek alba na pozadí",
"playerbarOpenDrawer": "Lišta přehrávače jako přEPínač celé obrazovky", "playerbarOpenDrawer": "Lišta přehrávače jako přepínač celé obrazovky",
"playerbarOpenDrawer_description": "Umožňuje kliknutí na lištu přehrávače pro otevření celoobrazovkového přehrávače", "playerbarOpenDrawer_description": "Umožňuje kliknutí na lištu přehrávače pro otevření celoobrazovkového přehrávače",
"artistConfiguration": "Nastavení stránky umělce alba", "artistConfiguration": "Nastavení stránky umělce alba",
"artistConfiguration_description": "Nastavit, které položky na stránce umělce alba budou zobrazeny a v jakém pořadí", "artistConfiguration_description": "Nastavit, které položky na stránce umělce alba budou zobrazeny a v jakém pořadí",
@@ -689,7 +689,7 @@
"rating": "Hodnocení", "rating": "Hodnocení",
"favorite": "Oblíbené", "favorite": "Oblíbené",
"playCount": "Přehrání", "playCount": "Přehrání",
"albumCount": "$t(entity.album, {\"count\": 2})", "albumCount": "Alba",
"releaseYear": "Rok", "releaseYear": "Rok",
"lastPlayed": "Naposledy přehráno", "lastPlayed": "Naposledy přehráno",
"biography": "Biografie", "biography": "Biografie",
@@ -697,20 +697,20 @@
"bitrate": "Datový tok", "bitrate": "Datový tok",
"title": "Název", "title": "Název",
"bpm": "BPM", "bpm": "BPM",
"dateAdded": "Datum přIDání", "dateAdded": "Datum přidání",
"artist": "$t(entity.artist, {\"count\": 1})", "artist": "Umělec",
"songCount": "$t(entity.track, {\"count\": 2})", "songCount": "Skladby",
"trackNumber": "Skladba", "trackNumber": "Skladba",
"genre": "$t(entity.genre, {\"count\": 1})", "genre": "Žánr",
"albumArtist": "Umělec alba", "albumArtist": "Umělec alba",
"path": "Cesta", "path": "Cesta",
"discNumber": "Disk", "discNumber": "Disk",
"channels": "$t(common.channel, {\"count\": 2})", "channels": "Kanály",
"size": "$t(common.size)", "size": "Velikost",
"codec": "$t(common.codec)", "codec": "Kodek",
"owner": "Majitel", "owner": "Majitel",
"bitDepth": "$t(common.bitDepth)", "bitDepth": "Bitová hloubka",
"sampleRate": "$t(common.sampleRate)" "sampleRate": "Vzorkovací frekvence"
} }
}, },
"error": { "error": {
@@ -762,10 +762,10 @@
"search": "Hledat", "search": "Hledat",
"bitrate": "Datový tok", "bitrate": "Datový tok",
"genre": "$t(entity.genre, {\"count\": 1})", "genre": "$t(entity.genre, {\"count\": 1})",
"recentlyAdded": "Nedávno přIDáno", "recentlyAdded": "Nedávno přidáno",
"note": "Poznámka", "note": "Poznámka",
"name": "Název", "name": "Název",
"dateAdded": "Datum přIDání", "dateAdded": "Datum přidání",
"releaseDate": "Datum vydání", "releaseDate": "Datum vydání",
"albumCount": "Počet $t(entity.album, {\"count\": 2})", "albumCount": "Počet $t(entity.album, {\"count\": 2})",
"communityRating": "Komunitní hodnocení", "communityRating": "Komunitní hodnocení",
@@ -887,7 +887,7 @@
}, },
"home": { "home": {
"mostPlayed": "Nejpřehrávanější", "mostPlayed": "Nejpřehrávanější",
"newlyAdded": "Nově přIDáno", "newlyAdded": "Nově přidáno",
"title": "$t(common.home)", "title": "$t(common.home)",
"explore": "Procházet z vaší knihovny", "explore": "Procházet z vaší knihovny",
"recentlyPlayed": "Nedávno přehráno", "recentlyPlayed": "Nedávno přehráno",
@@ -1030,7 +1030,7 @@
"input_password": "Heslo", "input_password": "Heslo",
"input_legacyAuthentication": "Zapnout zastaralé ověřování", "input_legacyAuthentication": "Zapnout zastaralé ověřování",
"input_name": "Název serveru", "input_name": "Název serveru",
"success": "Server úspěšně přIDán", "success": "Server úspěšně přidán",
"input_savePassword": "Uložit heslo", "input_savePassword": "Uložit heslo",
"ignoreSsl": "Ignorovat ssl $t(common.restartRequired)", "ignoreSsl": "Ignorovat ssl $t(common.restartRequired)",
"ignoreCors": "Ignorovat cors $t(common.restartRequired)", "ignoreCors": "Ignorovat cors $t(common.restartRequired)",
@@ -1042,7 +1042,7 @@
"input_remoteUrlPlaceholder": "Volitelné: veřejná adresa url pro externí funkce" "input_remoteUrlPlaceholder": "Volitelné: veřejná adresa url pro externí funkce"
}, },
"addToPlaylist": { "addToPlaylist": {
"success": "PřIDáno $t(entity.trackWithCount, {\"count\": {{message}} }) do $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })", "success": "$t(entity.trackWithCount, {\"count\": {{message}} }) přidáno do $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
"title": "Přidat do $t(entity.playlist, {\"count\": 1})", "title": "Přidat do $t(entity.playlist, {\"count\": 1})",
"input_skipDuplicates": "Přeskočit duplicity", "input_skipDuplicates": "Přeskočit duplicity",
"input_playlists": "$t(entity.playlist, {\"count\": 2})", "input_playlists": "$t(entity.playlist, {\"count\": 2})",
+6 -5
View File
@@ -171,14 +171,14 @@
"entity": { "entity": {
"album_one": "Album", "album_one": "Album",
"album_other": "Albums", "album_other": "Albums",
"albumArtist_one": "Album artist", "albumArtist_one": "Album Artist",
"albumArtist_other": "Album artists", "albumArtist_other": "Album Artists",
"albumArtistCount_one": "{{count}} album artist", "albumArtistCount_one": "{{count}} album artist",
"albumArtistCount_other": "{{count}} album artists", "albumArtistCount_other": "{{count}} album artists",
"albumWithCount_one": "{{count}} album", "albumWithCount_one": "{{count}} album",
"albumWithCount_other": "{{count}} albums", "albumWithCount_other": "{{count}} albums",
"radioStation_one": "Radio station", "radioStation_one": "Radio Station",
"radioStation_other": "Radio stations", "radioStation_other": "Radio Stations",
"radioStationWithCount_one": "{{count}} radio station", "radioStationWithCount_one": "{{count}} radio station",
"radioStationWithCount_other": "{{count}} radio stations", "radioStationWithCount_other": "{{count}} radio stations",
"artist_one": "Artist", "artist_one": "Artist",
@@ -696,7 +696,8 @@
"sleepTimer_off": "Off", "sleepTimer_off": "Off",
"sleepTimer_timeRemaining": "{{time}} remaining", "sleepTimer_timeRemaining": "{{time}} remaining",
"sleepTimer_setCustom": "Set timer", "sleepTimer_setCustom": "Set timer",
"sleepTimer_cancel": "Cancel timer" "sleepTimer_cancel": "Cancel timer",
"scrobbleForceSubmit": "Force scrobble"
}, },
"queryBuilder": { "queryBuilder": {
"standardTags": "Standard tags", "standardTags": "Standard tags",
+34 -34
View File
@@ -263,7 +263,7 @@
"lastfmApiKey": "Clave API para {{lastfm}}", "lastfmApiKey": "Clave API para {{lastfm}}",
"discordServeImage": "Servir imágenes de {{discord}} desde el servidor", "discordServeImage": "Servir imágenes de {{discord}} desde el servidor",
"discordServeImage_description": "Comparte el arte de la portada para el estado de actividad de {{discord}} desde el propio servidor, solo disponible para Jellyfin y Navidrome. {{discord}} usa un bot para obtener las imágenes, por lo que tu servidor debe ser alcanzable desde el internet público", "discordServeImage_description": "Comparte el arte de la portada para el estado de actividad de {{discord}} desde el propio servidor, solo disponible para Jellyfin y Navidrome. {{discord}} usa un bot para obtener las imágenes, por lo que tu servidor debe ser alcanzable desde el internet público",
"lastfm": "Mostrar enlaces de last.fm", "lastfm": "Mostrar enlaces de Last.fm",
"lastfm_description": "Muestra enlaces a Last.fm en las páginas de artistas/álbumes", "lastfm_description": "Muestra enlaces a Last.fm en las páginas de artistas/álbumes",
"musicbrainz": "Mostrar enlaces de MusicBrainz", "musicbrainz": "Mostrar enlaces de MusicBrainz",
"musicbrainz_description": "Muestra enlaces a MusicBrainz en las páginas de artistas/álbumes, donde exista MusicBrainz ID", "musicbrainz_description": "Muestra enlaces a MusicBrainz en las páginas de artistas/álbumes, donde exista MusicBrainz ID",
@@ -342,7 +342,7 @@
"playerFilters": "Filtrar las canciones de la cola", "playerFilters": "Filtrar las canciones de la cola",
"playerFilters_description": "Omite la adición de canciones a la cola basado en los siguientes criterios", "playerFilters_description": "Omite la adición de canciones a la cola basado en los siguientes criterios",
"playerbarSlider_description": "La forma de onda no es recomendable en una conexión a Internet lenta o medida", "playerbarSlider_description": "La forma de onda no es recomendable en una conexión a Internet lenta o medida",
"autoDJ": "DJ automático", "autoDJ": "DJ Automático",
"autoDJ_description": "Añade canciones similares a las de la cola automáticamente", "autoDJ_description": "Añade canciones similares a las de la cola automáticamente",
"autoDJ_itemCount": "Recuento de elementos", "autoDJ_itemCount": "Recuento de elementos",
"autoDJ_itemCount_description": "El número de elementos que se ha intentado añadir a la cola cuando DJ automático está activado", "autoDJ_itemCount_description": "El número de elementos que se ha intentado añadir a la cola cuando DJ automático está activado",
@@ -533,7 +533,7 @@
"comingSoon": "Próximamente…", "comingSoon": "Próximamente…",
"reset": "Restablecer", "reset": "Restablecer",
"disable": "Desactivar", "disable": "Desactivar",
"sortOrder": "Ordenar", "sortOrder": "Orden",
"none": "Ninguno", "none": "Ninguno",
"menu": "Menú", "menu": "Menú",
"restartRequired": "Reinicio requerido", "restartRequired": "Reinicio requerido",
@@ -700,7 +700,7 @@
"artists": "$t(entity.artist, {\"count\": 2})", "artists": "$t(entity.artist, {\"count\": 2})",
"albumArtists": "$t(entity.albumArtist, {\"count\": 2})", "albumArtists": "$t(entity.albumArtist, {\"count\": 2})",
"shared": "Compartido $t(entity.playlist, {\"count\": 2})", "shared": "Compartido $t(entity.playlist, {\"count\": 2})",
"myLibrary": "Mi biblioteca", "myLibrary": "Mi Biblioteca",
"favorites": "$t(entity.favorite, {\"count\": 2})", "favorites": "$t(entity.favorite, {\"count\": 2})",
"radio": "$t(entity.radioStation, {\"count\": 2})", "radio": "$t(entity.radioStation, {\"count\": 2})",
"collections": "Colecciones" "collections": "Colecciones"
@@ -761,7 +761,7 @@
"genres": "$t(entity.genre, {\"count\": 2})" "genres": "$t(entity.genre, {\"count\": 2})"
}, },
"fullscreenPlayer": { "fullscreenPlayer": {
"upNext": "Siguiente", "upNext": "A continuación",
"config": { "config": {
"dynamicBackground": "Fondo dinámico", "dynamicBackground": "Fondo dinámico",
"synchronized": "Sincronizado", "synchronized": "Sincronizado",
@@ -800,7 +800,7 @@
"updates": "Actualización", "updates": "Actualización",
"cache": "Caché", "cache": "Caché",
"application": "Aplicación", "application": "Aplicación",
"queryBuilder": "Generador de consultas", "queryBuilder": "Generador de Consultas",
"theme": "Tema", "theme": "Tema",
"controls": "Controles", "controls": "Controles",
"remote": "Remoto", "remote": "Remoto",
@@ -811,9 +811,9 @@
"transcoding": "Transcodificación", "transcoding": "Transcodificación",
"discord": "Discord", "discord": "Discord",
"sidebar": "Barra lateral", "sidebar": "Barra lateral",
"playerFilters": "Filtros del reproductor", "playerFilters": "Filtros del Reproductor",
"logger": "Registrador", "logger": "Registrador",
"lyricsDisplay": "Mostrar letras" "lyricsDisplay": "Mostrar Letras"
}, },
"albumArtistList": { "albumArtistList": {
"title": "$t(entity.albumArtist, {\"count\": 2})" "title": "$t(entity.albumArtist, {\"count\": 2})"
@@ -926,10 +926,10 @@
"ignoreSsl": "Ignorar SSL ($t(common.restartRequired))", "ignoreSsl": "Ignorar SSL ($t(common.restartRequired))",
"ignoreCors": "Ignorar CORS ($t(common.restartRequired))", "ignoreCors": "Ignorar CORS ($t(common.restartRequired))",
"error_savePassword": "Un error ocurrió cuando se intentó guardar la contraseña", "error_savePassword": "Un error ocurrió cuando se intentó guardar la contraseña",
"input_preferInstantMix": "Preferir mix instantáneo", "input_preferInstantMix": "Preferir Mix Instantáneo",
"input_preferInstantMixDescription": "Usa solo el mix instantáneo para obtener canciones similares. Útil si tienes complementos que modifican este comportamiento", "input_preferInstantMixDescription": "Usa solo el mix instantáneo para obtener canciones similares. Útil si tienes complementos que modifican este comportamiento",
"input_remoteUrl": "URL pública", "input_remoteUrl": "URL Pública",
"input_preferRemoteUrl": "Preferir URL pública", "input_preferRemoteUrl": "Preferir URL Pública",
"input_remoteUrlPlaceholder": "Opcional: URL pública para características externas" "input_remoteUrlPlaceholder": "Opcional: URL pública para características externas"
}, },
"addToPlaylist": { "addToPlaylist": {
@@ -1020,7 +1020,7 @@
"album": "Álbum", "album": "Álbum",
"favorite": "Favorito", "favorite": "Favorito",
"playCount": "Reproducciones", "playCount": "Reproducciones",
"albumCount": "$t(entity.album, {\"count\": 2})", "albumCount": "Álbumes",
"releaseYear": "Año", "releaseYear": "Año",
"lastPlayed": "Última reproducción", "lastPlayed": "Última reproducción",
"biography": "Biografía", "biography": "Biografía",
@@ -1029,19 +1029,19 @@
"title": "Título", "title": "Título",
"bpm": "BPM", "bpm": "BPM",
"dateAdded": "Fecha de adición", "dateAdded": "Fecha de adición",
"artist": "$t(entity.artist, {\"count\": 1})", "artist": "Artista",
"songCount": "$t(entity.track, {\"count\": 2})", "songCount": "Pistas",
"trackNumber": "Pista", "trackNumber": "Pista",
"genre": "$t(entity.genre, {\"count\": 1})", "genre": "Género",
"albumArtist": "Artista del álbum", "albumArtist": "Artista del álbum",
"path": "Ruta", "path": "Ruta",
"discNumber": "Disco", "discNumber": "Disco",
"channels": "$t(common.channel, {\"count\": 2})", "channels": "Canales",
"size": "$t(common.size)", "size": "Tamaño",
"codec": "$t(common.codec)", "codec": "decs",
"owner": "Propietario", "owner": "Propietario",
"bitDepth": "$t(common.bitDepth)", "bitDepth": "Profundidad de Bit",
"sampleRate": "$t(common.sampleRate)" "sampleRate": "Frecuencia de Muestreo"
}, },
"config": { "config": {
"label": { "label": {
@@ -1080,7 +1080,7 @@
"sampleRate": "$t(common.sampleRate)", "sampleRate": "$t(common.sampleRate)",
"titleArtist": "$t(common.title) (artista)", "titleArtist": "$t(common.title) (artista)",
"composer": "Compositor", "composer": "Compositor",
"albumGroup": "Grupo del álbum" "albumGroup": "Grupo del Álbum"
}, },
"general": { "general": {
"gap": "$t(common.gap)", "gap": "$t(common.gap)",
@@ -1123,27 +1123,27 @@
} }
}, },
"entity": { "entity": {
"smartPlaylist": "$t(entity.playlist, {\"count\": 1}) inteligente", "smartPlaylist": "$t(entity.playlist, {\"count\": 1}) Inteligente",
"genre_one": "Género", "genre_one": "Género",
"genre_many": "géneros", "genre_many": "Géneros",
"genre_other": "Géneros", "genre_other": "Géneros",
"playlistWithCount_one": "{{count}} lista de reproducción", "playlistWithCount_one": "{{count}} lista de reproducción",
"playlistWithCount_many": "{{count}} listas de reproducción", "playlistWithCount_many": "{{count}} listas de reproducción",
"playlistWithCount_other": "{{count}} listas de reproducción", "playlistWithCount_other": "{{count}} listas de reproducción",
"playlist_one": "Lista de reproducción", "playlist_one": "Lista de reproducción",
"playlist_many": "listas de reproducción", "playlist_many": "Listas de reproducción",
"playlist_other": "Listas de reproducción", "playlist_other": "Listas de reproducción",
"artist_one": "Artista", "artist_one": "Artista",
"artist_many": "artistas", "artist_many": "Artistas",
"artist_other": "Artistas", "artist_other": "Artistas",
"folderWithCount_one": "{{count}} carpeta", "folderWithCount_one": "{{count}} carpeta",
"folderWithCount_many": "{{count}} carpetas", "folderWithCount_many": "{{count}} carpetas",
"folderWithCount_other": "{{count}} carpetas", "folderWithCount_other": "{{count}} carpetas",
"albumArtist_one": "Artista del álbum", "albumArtist_one": "Artista del álbum",
"albumArtist_many": "artistas del álbum", "albumArtist_many": "Artistas del álbum",
"albumArtist_other": "Artistas del álbum", "albumArtist_other": "Artistas del álbum",
"track_one": "Pista", "track_one": "Pista",
"track_many": "pistas", "track_many": "Pistas",
"track_other": "Pistas", "track_other": "Pistas",
"albumArtistCount_one": "{{count}} artista del álbum", "albumArtistCount_one": "{{count}} artista del álbum",
"albumArtistCount_many": "{{count}} artistas del álbum", "albumArtistCount_many": "{{count}} artistas del álbum",
@@ -1152,16 +1152,16 @@
"albumWithCount_many": "{{count}} álbumes", "albumWithCount_many": "{{count}} álbumes",
"albumWithCount_other": "{{count}} álbumes", "albumWithCount_other": "{{count}} álbumes",
"favorite_one": "Favorito", "favorite_one": "Favorito",
"favorite_many": "favoritos", "favorite_many": "Favoritos",
"favorite_other": "Favoritos", "favorite_other": "Favoritos",
"artistWithCount_one": "{{count}} artista", "artistWithCount_one": "{{count}} artista",
"artistWithCount_many": "{{count}} artistas", "artistWithCount_many": "{{count}} artistas",
"artistWithCount_other": "{{count}} artistas", "artistWithCount_other": "{{count}} artistas",
"folder_one": "Carpeta", "folder_one": "Carpeta",
"folder_many": "carpetas", "folder_many": "Carpetas",
"folder_other": "Carpetas", "folder_other": "Carpetas",
"album_one": "Álbum", "album_one": "Álbum",
"album_many": "álbumes", "album_many": "Álbumes",
"album_other": "Álbumes", "album_other": "Álbumes",
"genreWithCount_one": "{{count}} género", "genreWithCount_one": "{{count}} género",
"genreWithCount_many": "{{count}} géneros", "genreWithCount_many": "{{count}} géneros",
@@ -1173,7 +1173,7 @@
"play_many": "{{count}} reproducciones", "play_many": "{{count}} reproducciones",
"play_other": "{{count}} reproducciones", "play_other": "{{count}} reproducciones",
"song_one": "Canción", "song_one": "Canción",
"song_many": "canciones", "song_many": "Canciones",
"song_other": "Canciones", "song_other": "Canciones",
"radioStation_one": "Estación de radio", "radioStation_one": "Estación de radio",
"radioStation_many": "Estaciones de radio", "radioStation_many": "Estaciones de radio",
@@ -1197,16 +1197,16 @@
}, },
"secondary": { "secondary": {
"audiobook": "Audiolibro", "audiobook": "Audiolibro",
"audioDrama": "Audio drama", "audioDrama": "Audio Drama",
"compilation": "Compilación", "compilation": "Compilación",
"djMix": "Mezcla del DJ", "djMix": "Mezcla del DJ",
"fieldRecording": "Grabación de campo", "fieldRecording": "Grabación de Campo",
"interview": "Entrevista", "interview": "Entrevista",
"live": "En vivo", "live": "En vivo",
"mixtape": "Recopilatorio", "mixtape": "Recopilatorio",
"remix": "Remix", "remix": "Remix",
"soundtrack": "Banda sonora", "soundtrack": "Banda sonora",
"spokenWord": "Palabra hablada", "spokenWord": "Palabra Hablada",
"demo": "Maqueta" "demo": "Maqueta"
} }
}, },
+11 -11
View File
@@ -1011,25 +1011,25 @@
}, },
"entity": { "entity": {
"genre_one": "Genre", "genre_one": "Genre",
"genre_many": "genres", "genre_many": "Genres",
"genre_other": "Genres", "genre_other": "Genres",
"playlistWithCount_one": "{{count}} liste de lecture", "playlistWithCount_one": "{{count}} liste de lecture",
"playlistWithCount_many": "{{count}} listes de lecture", "playlistWithCount_many": "{{count}} listes de lecture",
"playlistWithCount_other": "{{count}} listes de lecture", "playlistWithCount_other": "{{count}} listes de lecture",
"playlist_one": "Liste de lecture", "playlist_one": "Liste de lecture",
"playlist_many": "listes de lecture", "playlist_many": "Listes de lecture",
"playlist_other": "Listes de lecture", "playlist_other": "Listes de lecture",
"artist_one": "Artiste", "artist_one": "Artiste",
"artist_many": "artistes", "artist_many": "Artistes",
"artist_other": "Artistes", "artist_other": "Artistes",
"folderWithCount_one": "{{count}} dossier", "folderWithCount_one": "{{count}} dossier",
"folderWithCount_many": "{{count}} dossiers", "folderWithCount_many": "{{count}} dossiers",
"folderWithCount_other": "{{count}} dossiers", "folderWithCount_other": "{{count}} dossiers",
"albumArtist_one": "Artiste d'album", "albumArtist_one": "Artiste d'album",
"albumArtist_many": "artistes d'albums", "albumArtist_many": "Artistes d'albums",
"albumArtist_other": "Artistes d'albums", "albumArtist_other": "Artistes d'albums",
"track_one": "Piste", "track_one": "Piste",
"track_many": "pistes", "track_many": "Pistes",
"track_other": "Pistes", "track_other": "Pistes",
"albumArtistCount_one": "{{count}} artiste de l'album", "albumArtistCount_one": "{{count}} artiste de l'album",
"albumArtistCount_many": "{{count}} artistes d'albums", "albumArtistCount_many": "{{count}} artistes d'albums",
@@ -1038,17 +1038,17 @@
"albumWithCount_many": "{{count}} albums", "albumWithCount_many": "{{count}} albums",
"albumWithCount_other": "{{count}} albums", "albumWithCount_other": "{{count}} albums",
"favorite_one": "Favori", "favorite_one": "Favori",
"favorite_many": "favoris", "favorite_many": "Favoris",
"favorite_other": "Favoris", "favorite_other": "Favoris",
"artistWithCount_one": "{{count}} artiste", "artistWithCount_one": "{{count}} artiste",
"artistWithCount_many": "{{count}} artistes", "artistWithCount_many": "{{count}} artistes",
"artistWithCount_other": "{{count}} artistes", "artistWithCount_other": "{{count}} artistes",
"folder_one": "Dossier", "folder_one": "Dossier",
"folder_many": "dossiers", "folder_many": "Dossiers",
"folder_other": "Dossiers", "folder_other": "Dossiers",
"smartPlaylist": "$t(entity.playlist, {\"count\": 1}) intelligente", "smartPlaylist": "$t(entity.playlist, {\"count\": 1}) Intelligente",
"album_one": "Album", "album_one": "Album",
"album_many": "albums", "album_many": "Albums",
"album_other": "Albums", "album_other": "Albums",
"genreWithCount_one": "{{count}} genre", "genreWithCount_one": "{{count}} genre",
"genreWithCount_many": "{{count}} genres", "genreWithCount_many": "{{count}} genres",
@@ -1060,10 +1060,10 @@
"play_many": "{{count}} écoutes", "play_many": "{{count}} écoutes",
"play_other": "{{count}} écoutes", "play_other": "{{count}} écoutes",
"song_one": "Titre", "song_one": "Titre",
"song_many": "titres", "song_many": "Titres",
"song_other": "Titres", "song_other": "Titres",
"radioStation_one": "Station radio", "radioStation_one": "Station radio",
"radioStation_many": "stations radio", "radioStation_many": "Stations radio",
"radioStation_other": "Stations radio", "radioStation_other": "Stations radio",
"radioStationWithCount_one": "{{count}} station radio", "radioStationWithCount_one": "{{count}} station radio",
"radioStationWithCount_many": "{{count}} stations radio", "radioStationWithCount_many": "{{count}} stations radio",
+2 -2
View File
@@ -192,8 +192,8 @@
"play_other": "{{count}} lejátszások", "play_other": "{{count}} lejátszások",
"trackWithCount_one": "{{count}} sáv", "trackWithCount_one": "{{count}} sáv",
"trackWithCount_other": "{{count}} sávok", "trackWithCount_other": "{{count}} sávok",
"radioStation_one": "Rádió állomás", "radioStation_one": "Rádióállomás",
"radioStation_other": "Rádió állomások", "radioStation_other": "Rádióállomások",
"radioStationWithCount_one": "{{count}} rádióállomás", "radioStationWithCount_one": "{{count}} rádióállomás",
"radioStationWithCount_other": "{{count}} rádióállomások" "radioStationWithCount_other": "{{count}} rádióállomások"
}, },
+3 -3
View File
@@ -160,7 +160,7 @@
}, },
"entity": { "entity": {
"album_other": "Album", "album_other": "Album",
"albumArtist_other": "Artis album", "albumArtist_other": "Artis Album",
"albumArtistCount_other": "{{count}} artis album", "albumArtistCount_other": "{{count}} artis album",
"albumWithCount_other": "{{count}} album", "albumWithCount_other": "{{count}} album",
"artist_other": "Artis", "artist_other": "Artis",
@@ -170,14 +170,14 @@
"folderWithCount_other": "{{count}} folder", "folderWithCount_other": "{{count}} folder",
"genre_other": "Genre", "genre_other": "Genre",
"genreWithCount_other": "{{count}} genre", "genreWithCount_other": "{{count}} genre",
"playlist_other": "Daftar putar", "playlist_other": "Daftar Putar",
"play_other": "Putar {{count}}", "play_other": "Putar {{count}}",
"playlistWithCount_other": "{{count}} daftar putar", "playlistWithCount_other": "{{count}} daftar putar",
"smartPlaylist": "$t(entity.playlist, {\"count\": 1}) pintar", "smartPlaylist": "$t(entity.playlist, {\"count\": 1}) pintar",
"track_other": "Pista", "track_other": "Pista",
"song_other": "Lagu", "song_other": "Lagu",
"trackWithCount_other": "{{count}} pista", "trackWithCount_other": "{{count}} pista",
"radioStation_other": "Stasiun radio", "radioStation_other": "Stasiun Radio",
"radioStationWithCount_other": "{{count}} stasiun radio" "radioStationWithCount_other": "{{count}} stasiun radio"
}, },
"error": { "error": {
+13 -13
View File
@@ -930,25 +930,25 @@
}, },
"entity": { "entity": {
"genre_one": "Genere", "genre_one": "Genere",
"genre_many": "generi", "genre_many": "Generi",
"genre_other": "Generi", "genre_other": "Generi",
"playlistWithCount_one": "{{count}} playlist", "playlistWithCount_one": "{{count}} playlist",
"playlistWithCount_many": "{{count}} playlist", "playlistWithCount_many": "{{count}} playlist",
"playlistWithCount_other": "{{count}} playlist", "playlistWithCount_other": "{{count}} playlist",
"playlist_one": "Playlist", "playlist_one": "Playlist",
"playlist_many": "playlist", "playlist_many": "Playlist",
"playlist_other": "Playlist", "playlist_other": "Playlist",
"artist_one": "Artista", "artist_one": "Artista",
"artist_many": "artisti", "artist_many": "Artisti",
"artist_other": "Artisti", "artist_other": "Artisti",
"folderWithCount_one": "{{count}} cartella", "folderWithCount_one": "{{count}} cartella",
"folderWithCount_many": "{{count}} cartelle", "folderWithCount_many": "{{count}} cartelle",
"folderWithCount_other": "{{count}} cartelle", "folderWithCount_other": "{{count}} cartelle",
"albumArtist_one": "Artista album", "albumArtist_one": "Artista Album",
"albumArtist_many": "artisti album", "albumArtist_many": "Artisti Album",
"albumArtist_other": "Artisti album", "albumArtist_other": "Artisti Album",
"track_one": "Traccia", "track_one": "Traccia",
"track_many": "tracce", "track_many": "Tracce",
"track_other": "Tracce", "track_other": "Tracce",
"albumArtistCount_one": "{{count}} artista album", "albumArtistCount_one": "{{count}} artista album",
"albumArtistCount_many": "{{count}} artisti album", "albumArtistCount_many": "{{count}} artisti album",
@@ -957,17 +957,17 @@
"albumWithCount_many": "{{count}} album", "albumWithCount_many": "{{count}} album",
"albumWithCount_other": "{{count}} album", "albumWithCount_other": "{{count}} album",
"favorite_one": "Preferito", "favorite_one": "Preferito",
"favorite_many": "preferiti", "favorite_many": "Preferiti",
"favorite_other": "Preferiti", "favorite_other": "Preferiti",
"artistWithCount_one": "{{count}} artista", "artistWithCount_one": "{{count}} artista",
"artistWithCount_many": "{{count}} artisti", "artistWithCount_many": "{{count}} artisti",
"artistWithCount_other": "{{count}} artisti", "artistWithCount_other": "{{count}} artisti",
"folder_one": "Cartella", "folder_one": "Cartella",
"folder_many": "cartelle", "folder_many": "Cartelle",
"folder_other": "Cartelle", "folder_other": "Cartelle",
"smartPlaylist": "$t(entity.playlist, {\"count\": 1}) smart", "smartPlaylist": "$t(entity.playlist, {\"count\": 1}) Smart",
"album_one": "Album", "album_one": "Album",
"album_many": "album", "album_many": "Album",
"album_other": "Album", "album_other": "Album",
"genreWithCount_one": "{{count}} genere", "genreWithCount_one": "{{count}} genere",
"genreWithCount_many": "{{count}} generi", "genreWithCount_many": "{{count}} generi",
@@ -979,10 +979,10 @@
"play_many": "{{count}} riproduzioni", "play_many": "{{count}} riproduzioni",
"play_other": "{{count}} riproduzioni", "play_other": "{{count}} riproduzioni",
"song_one": "Traccia", "song_one": "Traccia",
"song_many": "tracce", "song_many": "Tracce",
"song_other": "Tracce", "song_other": "Tracce",
"radioStation_one": "Stazione radio", "radioStation_one": "Stazione radio",
"radioStation_many": "stazioni radio", "radioStation_many": "Stazioni radio",
"radioStation_other": "Stazioni radio", "radioStation_other": "Stazioni radio",
"radioStationWithCount_one": "{{count}} stazione radio", "radioStationWithCount_one": "{{count}} stazione radio",
"radioStationWithCount_many": "{{count}} stazioni radio", "radioStationWithCount_many": "{{count}} stazioni radio",
+9 -9
View File
@@ -764,31 +764,31 @@
}, },
"entity": { "entity": {
"albumArtist_one": "Artista do álbum", "albumArtist_one": "Artista do álbum",
"albumArtist_many": "artistas do álbum", "albumArtist_many": "Artistas do álbum",
"albumArtist_other": "Artistas do álbum", "albumArtist_other": "Artistas do álbum",
"albumArtistCount_one": "{{count}} artista do álbum", "albumArtistCount_one": "{{count}} artista do álbum",
"albumArtistCount_many": "{{count}} artistas do álbum", "albumArtistCount_many": "{{count}} artistas do álbum",
"albumArtistCount_other": "{{count}} artistas do álbum", "albumArtistCount_other": "{{count}} artistas do álbum",
"album_one": "Álbum", "album_one": "Álbum",
"album_many": "álbuns", "album_many": "Álbuns",
"album_other": "Álbuns", "album_other": "Álbuns",
"artist_one": "Artista", "artist_one": "Artista",
"artist_many": "artistas", "artist_many": "Artistas",
"artist_other": "Artistas", "artist_other": "Artistas",
"albumWithCount_one": "{{count}} álbum", "albumWithCount_one": "{{count}} álbum",
"albumWithCount_many": "{{count}} álbuns", "albumWithCount_many": "{{count}} álbuns",
"albumWithCount_other": "{{count}} álbuns", "albumWithCount_other": "{{count}} álbuns",
"favorite_one": "Favorito", "favorite_one": "Favorito",
"favorite_many": "favoritos", "favorite_many": "Favoritos",
"favorite_other": "Favoritos", "favorite_other": "Favoritos",
"artistWithCount_one": "{{count}} artista", "artistWithCount_one": "{{count}} artista",
"artistWithCount_many": "{{count}} artistas", "artistWithCount_many": "{{count}} artistas",
"artistWithCount_other": "{{count}} artistas", "artistWithCount_other": "{{count}} artistas",
"folder_one": "Pasta", "folder_one": "Pasta",
"folder_many": "pastas", "folder_many": "Pastas",
"folder_other": "Pastas", "folder_other": "Pastas",
"genre_one": "Gênero", "genre_one": "Gênero",
"genre_many": "gêneros", "genre_many": "Gêneros",
"genre_other": "Gêneros", "genre_other": "Gêneros",
"playlistWithCount_one": "{{count}} playlist", "playlistWithCount_one": "{{count}} playlist",
"playlistWithCount_many": "{{count}} playlists", "playlistWithCount_many": "{{count}} playlists",
@@ -806,11 +806,11 @@
"trackWithCount_many": "{{count}} faixas", "trackWithCount_many": "{{count}} faixas",
"trackWithCount_other": "{{count}} faixas", "trackWithCount_other": "{{count}} faixas",
"track_one": "Faixa", "track_one": "Faixa",
"track_many": "faixas", "track_many": "Faixas",
"track_other": "Faixas", "track_other": "Faixas",
"smartPlaylist": "$t(entity.playlist, {\"count\": 1}) inteligente", "smartPlaylist": "$t(entity.playlist, {\"count\": 1}) Inteligente",
"song_one": "Música", "song_one": "Música",
"song_many": "músicas", "song_many": "Músicas",
"song_other": "Músicas", "song_other": "Músicas",
"play_one": "{{count}} reprodução", "play_one": "{{count}} reprodução",
"play_many": "{{count}} reproduções", "play_many": "{{count}} reproduções",
+10 -10
View File
@@ -126,10 +126,10 @@
}, },
"entity": { "entity": {
"album_one": "Álbum", "album_one": "Álbum",
"album_many": "álbuns", "album_many": "Álbuns",
"album_other": "Álbuns", "album_other": "Álbuns",
"albumArtist_one": "Artista do álbum", "albumArtist_one": "Artista do álbum",
"albumArtist_many": "artistas do álbum", "albumArtist_many": "Artistas do álbum",
"albumArtist_other": "Artistas do álbum", "albumArtist_other": "Artistas do álbum",
"albumArtistCount_one": "{{count}} artista do álbum", "albumArtistCount_one": "{{count}} artista do álbum",
"albumArtistCount_many": "{{count}} artistas do álbum", "albumArtistCount_many": "{{count}} artistas do álbum",
@@ -138,28 +138,28 @@
"albumWithCount_many": "{{count}} álbuns", "albumWithCount_many": "{{count}} álbuns",
"albumWithCount_other": "{{count}} álbuns", "albumWithCount_other": "{{count}} álbuns",
"artist_one": "Artista", "artist_one": "Artista",
"artist_many": "artistas", "artist_many": "Artistas",
"artist_other": "Artistas", "artist_other": "Artistas",
"artistWithCount_one": "{{count}} artista", "artistWithCount_one": "{{count}} artista",
"artistWithCount_many": "{{count}} artistas", "artistWithCount_many": "{{count}} artistas",
"artistWithCount_other": "{{count}} artistas", "artistWithCount_other": "{{count}} artistas",
"favorite_one": "Favorito", "favorite_one": "Favorito",
"favorite_many": "favoritos", "favorite_many": "Favoritos",
"favorite_other": "Favoritos", "favorite_other": "Favoritos",
"folder_one": "Pasta", "folder_one": "Pasta",
"folder_many": "pastas", "folder_many": "Pastas",
"folder_other": "Pastas", "folder_other": "Pastas",
"folderWithCount_one": "{{count}} pasta", "folderWithCount_one": "{{count}} pasta",
"folderWithCount_many": "{{count}} pastas", "folderWithCount_many": "{{count}} pastas",
"folderWithCount_other": "{{count}} pastas", "folderWithCount_other": "{{count}} pastas",
"genre_one": "Gênero", "genre_one": "Gênero",
"genre_many": "gêneros", "genre_many": "Gêneros",
"genre_other": "Gêneros", "genre_other": "Gêneros",
"genreWithCount_one": "{{count}} gênero", "genreWithCount_one": "{{count}} gênero",
"genreWithCount_many": "{{count}} gêneros", "genreWithCount_many": "{{count}} gêneros",
"genreWithCount_other": "{{count}} gêneros", "genreWithCount_other": "{{count}} gêneros",
"playlist_one": "Playlist", "playlist_one": "Playlist",
"playlist_many": "playlists", "playlist_many": "Playlists",
"playlist_other": "Playlists", "playlist_other": "Playlists",
"play_one": "{{count}} reprodução", "play_one": "{{count}} reprodução",
"play_many": "{{count}} reproduções", "play_many": "{{count}} reproduções",
@@ -167,12 +167,12 @@
"playlistWithCount_one": "{{count}} playlist", "playlistWithCount_one": "{{count}} playlist",
"playlistWithCount_many": "{{count}} playlists", "playlistWithCount_many": "{{count}} playlists",
"playlistWithCount_other": "{{count}} playlists", "playlistWithCount_other": "{{count}} playlists",
"smartPlaylist": "$t(entity.playlist, {\"count\": 1}) inteligente", "smartPlaylist": "$t(entity.playlist, {\"count\": 1}) Inteligente",
"track_one": "Faixa", "track_one": "Faixa",
"track_many": "faixas", "track_many": "Faixas",
"track_other": "Faixas", "track_other": "Faixas",
"song_one": "Música", "song_one": "Música",
"song_many": "músicas", "song_many": "Músicas",
"song_other": "Músicas", "song_other": "Músicas",
"trackWithCount_one": "{{count}} faixa", "trackWithCount_one": "{{count}} faixa",
"trackWithCount_many": "{{count}} faixas", "trackWithCount_many": "{{count}} faixas",
+8 -8
View File
@@ -580,25 +580,25 @@
}, },
"entity": { "entity": {
"genre_one": "Žanr", "genre_one": "Žanr",
"genre_few": "žanrova", "genre_few": "Žanrova",
"genre_other": "Žanrova", "genre_other": "Žanrova",
"playlistWithCount_one": "{{count}} plejlista", "playlistWithCount_one": "{{count}} plejlista",
"playlistWithCount_few": "{{count}} plejlista", "playlistWithCount_few": "{{count}} plejlista",
"playlistWithCount_other": "{{count}} plejlista", "playlistWithCount_other": "{{count}} plejlista",
"playlist_one": "Plejlista", "playlist_one": "Plejlista",
"playlist_few": "plejlista", "playlist_few": "Plejlista",
"playlist_other": "Plejlista", "playlist_other": "Plejlista",
"artist_one": "Umetnik", "artist_one": "Umetnik",
"artist_few": "umetnika", "artist_few": "Umetnika",
"artist_other": "Umetnika", "artist_other": "Umetnika",
"folderWithCount_one": "{{count}} folder", "folderWithCount_one": "{{count}} folder",
"folderWithCount_few": "{{count}} foldera", "folderWithCount_few": "{{count}} foldera",
"folderWithCount_other": "{{count}} foldera", "folderWithCount_other": "{{count}} foldera",
"albumArtist_one": "Album umetnika", "albumArtist_one": "Album umetnika",
"albumArtist_few": "albuma umetnika", "albumArtist_few": "Albuma umetnika",
"albumArtist_other": "Albuma umetnika", "albumArtist_other": "Albuma umetnika",
"track_one": "Pesma", "track_one": "Pesma",
"track_few": "pesama", "track_few": "Pesama",
"track_other": "Pesama", "track_other": "Pesama",
"albumArtistCount_one": "{{count}} album umetnika", "albumArtistCount_one": "{{count}} album umetnika",
"albumArtistCount_few": "{{count}} albuma umetnika", "albumArtistCount_few": "{{count}} albuma umetnika",
@@ -607,17 +607,17 @@
"albumWithCount_few": "{{count}} albuma", "albumWithCount_few": "{{count}} albuma",
"albumWithCount_other": "{{count}} albuma", "albumWithCount_other": "{{count}} albuma",
"favorite_one": "Favorit", "favorite_one": "Favorit",
"favorite_few": "favorita", "favorite_few": "Favorita",
"favorite_other": "Favorita", "favorite_other": "Favorita",
"artistWithCount_one": "{{count}} umetnik", "artistWithCount_one": "{{count}} umetnik",
"artistWithCount_few": "{{count}} umetnika", "artistWithCount_few": "{{count}} umetnika",
"artistWithCount_other": "{{count}} umetnika", "artistWithCount_other": "{{count}} umetnika",
"folder_one": "Folder", "folder_one": "Folder",
"folder_few": "foldera", "folder_few": "Foldera",
"folder_other": "Foldera", "folder_other": "Foldera",
"smartPlaylist": "Pametna $t(entity.playlist, {\"count\": 1})", "smartPlaylist": "Pametna $t(entity.playlist, {\"count\": 1})",
"album_one": "Album", "album_one": "Album",
"album_few": "albumi", "album_few": "Albumi",
"album_other": "Albuma", "album_other": "Albuma",
"genreWithCount_one": "{{count}} žanr", "genreWithCount_one": "{{count}} žanr",
"genreWithCount_few": "{{count}} žanrova", "genreWithCount_few": "{{count}} žanrova",
+21 -21
View File
@@ -90,7 +90,7 @@
"share": "分享", "share": "分享",
"tags": "標籤", "tags": "標籤",
"trackGain": "曲目增益", "trackGain": "曲目增益",
"trackPeak": "曲峰值", "trackPeak": "曲峰值",
"translation": "翻譯", "translation": "翻譯",
"doNotShowAgain": "不再顯示", "doNotShowAgain": "不再顯示",
"externalLinks": "外部連結", "externalLinks": "外部連結",
@@ -103,8 +103,8 @@
"sort": "排序", "sort": "排序",
"tableColumns": "表格欄位", "tableColumns": "表格欄位",
"clean": "清除", "clean": "清除",
"explicitStatus": "Explicit狀態", "explicitStatus": "露骨狀態",
"explicit": "Explicit", "explicit": "露骨",
"gridRows": "網格行", "gridRows": "網格行",
"noFilters": "未設定任何過濾器", "noFilters": "未設定任何過濾器",
"countSelected": "{{count}}個已選取", "countSelected": "{{count}}個已選取",
@@ -128,7 +128,7 @@
"credentialsRequired": "需要憑證", "credentialsRequired": "需要憑證",
"genericError": "發生了錯誤", "genericError": "發生了錯誤",
"invalidServer": "無效的伺服器", "invalidServer": "無效的伺服器",
"localFontAccessDenied": "無法取得本地字", "localFontAccessDenied": "無法取得本地字",
"loginRateError": "登入請求嘗試次數過多,請稍後再試", "loginRateError": "登入請求嘗試次數過多,請稍後再試",
"remoteDisableError": "$t(common.disable)遠端伺服器時出現錯誤", "remoteDisableError": "$t(common.disable)遠端伺服器時出現錯誤",
"remoteEnableError": "$t(common.enable)遠端伺服器時出現錯誤", "remoteEnableError": "$t(common.enable)遠端伺服器時出現錯誤",
@@ -136,7 +136,7 @@
"remotePortWarning": "重啟伺服器使新連接埠生效", "remotePortWarning": "重啟伺服器使新連接埠生效",
"serverRequired": "需要伺服器", "serverRequired": "需要伺服器",
"sessionExpiredError": "工作階段已過期", "sessionExpiredError": "工作階段已過期",
"systemFontError": "嘗試取得系統字時出現錯誤", "systemFontError": "嘗試取得系統字時出現錯誤",
"serverNotSelectedError": "未選擇伺服器", "serverNotSelectedError": "未選擇伺服器",
"mpvRequired": "需要 MPV", "mpvRequired": "需要 MPV",
"playbackError": "無法播放媒體", "playbackError": "無法播放媒體",
@@ -221,7 +221,7 @@
"fullscreenPlayer": { "fullscreenPlayer": {
"config": { "config": {
"showLyricProvider": "顯示歌詞提供者", "showLyricProvider": "顯示歌詞提供者",
"useImageAspectRatio": "使用圖片縱橫比", "useImageAspectRatio": "使用圖片長寬比",
"dynamicBackground": "動態背景", "dynamicBackground": "動態背景",
"followCurrentLyric": "跟隨目前歌詞", "followCurrentLyric": "跟隨目前歌詞",
"lyricAlignment": "歌詞對齊", "lyricAlignment": "歌詞對齊",
@@ -440,8 +440,8 @@
"crossfadeDuration": "淡入淡出持續時間", "crossfadeDuration": "淡入淡出持續時間",
"crossfadeDuration_description": "設定淡入淡出持續時間", "crossfadeDuration_description": "設定淡入淡出持續時間",
"crossfadeStyle_description": "選擇用於音訊播放器的淡入淡出風格", "crossfadeStyle_description": "選擇用於音訊播放器的淡入淡出風格",
"customFontPath": "自定字體路徑", "customFontPath": "自訂字型路徑",
"customFontPath_description": "設定應用程式使用的自定字體路徑", "customFontPath_description": "設定應用程式使用的自訂字型路徑",
"disableLibraryUpdateOnStartup": "禁用啟動時檢查新版本", "disableLibraryUpdateOnStartup": "禁用啟動時檢查新版本",
"discordApplicationId": "{{discord}} 應用程式 ID", "discordApplicationId": "{{discord}} 應用程式 ID",
"discordApplicationId_description": "{{discord}} Rich Presence 應用程式 ID(預設為 {{defaultId}}", "discordApplicationId_description": "{{discord}} Rich Presence 應用程式 ID(預設為 {{defaultId}}",
@@ -454,12 +454,12 @@
"enableRemote_description": "啟用遠端控制伺服器,以允許其他設備控制此應用程式", "enableRemote_description": "啟用遠端控制伺服器,以允許其他設備控制此應用程式",
"exitToTray": "關閉時到將視窗最小化", "exitToTray": "關閉時到將視窗最小化",
"followLyric": "跟隨目前歌詞", "followLyric": "跟隨目前歌詞",
"font_description": "設定應用程式使用的字", "font_description": "設定應用程式使用的字",
"fontType": "字類型", "fontType": "字類型",
"fontType_description": "內建字可以選擇 Feishin 提供的字之一。系統字允許您選擇作業系統提供的任何字。自選項允許您使用自己的字", "fontType_description": "內建字可以選擇 Feishin 提供的字之一。系統字允許您選擇作業系統提供的任何字。自選項允許您使用自己的字",
"fontType_optionBuiltIn": "內建字", "fontType_optionBuiltIn": "內建字",
"fontType_optionCustom": "自定字體", "fontType_optionCustom": "自訂字型",
"fontType_optionSystem": "系統字", "fontType_optionSystem": "系統字",
"gaplessAudio": "無間隔音訊", "gaplessAudio": "無間隔音訊",
"gaplessAudio_description": "調整 MPV 無間隔音訊設定", "gaplessAudio_description": "調整 MPV 無間隔音訊設定",
"gaplessAudio_optionWeak": "弱(建議)", "gaplessAudio_optionWeak": "弱(建議)",
@@ -557,7 +557,7 @@
"playbackStyle": "播放風格", "playbackStyle": "播放風格",
"exitToTray_description": "退出應用程式時最小化到系統匣而非關閉", "exitToTray_description": "退出應用程式時最小化到系統匣而非關閉",
"followLyric_description": "滾動歌詞到目前播放位置", "followLyric_description": "滾動歌詞到目前播放位置",
"font": "字", "font": "字",
"globalMediaHotkeys_description": "啟用或禁用系統媒體快捷鍵以控制播放", "globalMediaHotkeys_description": "啟用或禁用系統媒體快捷鍵以控制播放",
"hotkey_browserBack": "瀏覽器返回", "hotkey_browserBack": "瀏覽器返回",
"hotkey_favoriteCurrentSong": "收藏 $t(common.currentSong)", "hotkey_favoriteCurrentSong": "收藏 $t(common.currentSong)",
@@ -764,7 +764,7 @@
"sidebarPlaylistListFilterRegex": "播放清單過濾器正規表達式", "sidebarPlaylistListFilterRegex": "播放清單過濾器正規表達式",
"blurExplicitImages": "模糊露骨圖片", "blurExplicitImages": "模糊露骨圖片",
"blurExplicitImages_description": "標記為露骨的專輯和歌曲封面將被模糊", "blurExplicitImages_description": "標記為露骨的專輯和歌曲封面將被模糊",
"releaseChannel_optionAlpha": "alpha (每日建構版)", "releaseChannel_optionAlpha": "Alpha (每日建構版)",
"analyticsEnable": "傳送基於使用情況的分析報告", "analyticsEnable": "傳送基於使用情況的分析報告",
"analyticsEnable_description": "匿名化的使用情況資料會傳送給開發者,以協助改進應用程式", "analyticsEnable_description": "匿名化的使用情況資料會傳送給開發者,以協助改進應用程式",
"automaticUpdates": "自動更新", "automaticUpdates": "自動更新",
@@ -985,7 +985,7 @@
"comment": "評論", "comment": "評論",
"communityRating": "社群評分", "communityRating": "社群評分",
"criticRating": "評論家評分", "criticRating": "評論家評分",
"dateAdded": "新增日期", "dateAdded": "新增日期",
"disc": "光碟", "disc": "光碟",
"duration": "時長", "duration": "時長",
"id": "ID", "id": "ID",
@@ -1034,8 +1034,8 @@
"success": "伺服器新增成功", "success": "伺服器新增成功",
"title": "新增伺服器", "title": "新增伺服器",
"error_savePassword": "儲存密碼時出現錯誤", "error_savePassword": "儲存密碼時出現錯誤",
"ignoreCors": "忽略 cors $t(common.restartRequired)", "ignoreCors": "忽略 CORS $t(common.restartRequired)",
"ignoreSsl": "忽略 ssl $t(common.restartRequired)", "ignoreSsl": "忽略 SSL $t(common.restartRequired)",
"input_preferInstantMix": "偏好即時混音", "input_preferInstantMix": "偏好即時混音",
"input_preferInstantMixDescription": "僅使用即時混音功能來取得相似歌曲。若您擁有能修改此行為的外掛,此功能將相當實用", "input_preferInstantMixDescription": "僅使用即時混音功能來取得相似歌曲。若您擁有能修改此行為的外掛,此功能將相當實用",
"input_preferRemoteUrl": "優先使用公開網址", "input_preferRemoteUrl": "優先使用公開網址",
@@ -1098,8 +1098,8 @@
"successMustClick": "分享建立成功,點擊此處開啟" "successMustClick": "分享建立成功,點擊此處開啟"
}, },
"privateMode": { "privateMode": {
"enabled": "已啟用私人模式,播放狀態將對外部整合隱藏", "enabled": "已啟用私人模式,播放狀態現在將對外部整合功能隱藏",
"disabled": "已停用私人模式,播放狀態現對已啟用的外部整合可見", "disabled": "已停用私人模式,啟用的外部整合功能現在可查看播放狀態",
"title": "私人模式" "title": "私人模式"
}, },
"largeFetchConfirmation": { "largeFetchConfirmation": {
@@ -59,9 +59,24 @@
.slider-value-wrapper { .slider-value-wrapper {
display: flex; display: flex;
flex: 1; flex: 1;
align-self: center; align-items: center;
justify-content: center; justify-content: center;
max-width: 50px; max-width: 50px;
min-height: 0;
@media (width < 768px) {
display: none;
}
}
.slider-value-wrapper-elapsed {
display: flex;
flex: 1;
align-items: center;
justify-content: center;
min-width: 0;
max-width: 4.75rem;
min-height: 0;
@media (width < 768px) { @media (width < 768px) {
display: none; display: none;
@@ -4,6 +4,7 @@ import { lazy, Suspense } from 'react';
import { PlayerbarSeekSlider } from './playerbar-seek-slider'; import { PlayerbarSeekSlider } from './playerbar-seek-slider';
import styles from './playerbar-slider.module.css'; import styles from './playerbar-slider.module.css';
import { ScrobbleStatus } from '/@/renderer/features/player/components/scrobble-status';
import { import {
useAppStore, useAppStore,
useAppStoreActions, useAppStoreActions,
@@ -41,17 +42,8 @@ export const PlayerbarSlider = () => {
return ( return (
<> <>
<div className={styles.sliderContainer}> <div className={styles.sliderContainer}>
<div className={styles.sliderValueWrapper}> <div className={styles.sliderValueWrapperElapsed}>
<Text <ScrobbleStatus formattedTime={formattedTime} />
className={PlaybackSelectors.elapsedTime}
fw={600}
isMuted
isNoSelect
size="xs"
style={{ userSelect: 'none' }}
>
{formattedTime}
</Text>
</div> </div>
<div className={styles.sliderWrapper}> <div className={styles.sliderWrapper}>
{isWaveform ? ( {isWaveform ? (
@@ -0,0 +1,124 @@
import { useTranslation } from 'react-i18next';
import {
invokeScrobbleForceSubmit,
invokeScrobbleResetListenedState,
} from '/@/renderer/features/player/hooks/use-scrobble';
import { useAppStore, useScrobbleDebugStore, useSettingsStore } from '/@/renderer/store';
import { Button } from '/@/shared/components/button/button';
import { Group } from '/@/shared/components/group/group';
import { HoverCard } from '/@/shared/components/hover-card/hover-card';
import { Icon } from '/@/shared/components/icon/icon';
import { Progress } from '/@/shared/components/progress/progress';
import { Stack } from '/@/shared/components/stack/stack';
import { Text } from '/@/shared/components/text/text';
import { PlaybackSelectors } from '/@/shared/constants/playback-selectors';
const scrobbleProgressProps = {
'aria-hidden': true,
color: 'var(--theme-colors-primary)',
size: 'xs' as const,
};
const clampPct = (n: number) => Math.min(100, Math.max(0, n));
const ScrobbleConditionProgress = ({ value }: { value: number }) => (
<Progress {...scrobbleProgressProps} value={value} w="100%" />
);
export const ScrobbleStatus = ({ formattedTime }: { formattedTime: string }) => {
const { t } = useTranslation();
const scrobbleEnabled = useSettingsStore((state) => state.playback.scrobble.enabled);
const privateMode = useAppStore((state) => state.privateMode);
const snapshot = useScrobbleDebugStore((state) => state.snapshot);
const hookInactive = !scrobbleEnabled || privateMode;
const listenedSec = (snapshot.listenedMs / 1000).toFixed(1);
const listenPercentOfTrack =
snapshot.trackDurationMs > 0 ? (snapshot.listenedMs / snapshot.trackDurationMs) * 100 : 0;
const durationConditionPct =
snapshot.targetDurationSec > 0
? clampPct((snapshot.listenedMs / 1000 / snapshot.targetDurationSec) * 100)
: 0;
const percentConditionPct =
snapshot.targetPercentage > 0 && snapshot.trackDurationMs > 0
? clampPct((listenPercentOfTrack / snapshot.targetPercentage) * 100)
: 0;
return (
<HoverCard position="top" width={280}>
<HoverCard.Target>
<Group
align="center"
aria-label={`${t('player.scrobble')}, ${formattedTime}`}
fz="xs"
gap="sm"
justify="center"
onClick={(e) => e.stopPropagation()}
style={{ userSelect: 'none' }}
wrap="nowrap"
>
<Icon
aria-hidden
color={snapshot.submitted ? 'primary' : 'transparent'}
fill={snapshot.submitted ? 'primary' : 'transparent'}
icon="circle"
size="0.375rem"
/>
<Text
className={PlaybackSelectors.elapsedTime}
fw={600}
fz="inherit"
isMuted
isNoSelect
style={{ userSelect: 'none' }}
>
{formattedTime}
</Text>
</Group>
</HoverCard.Target>
<HoverCard.Dropdown onClick={(e) => e.stopPropagation()}>
<Stack gap="md" p="sm">
{hookInactive ? (
<Text size="sm">{t('form.privateMode.enabled')}</Text>
) : (
<>
<Stack gap="xs">
<Text size="xs">
{`${listenedSec}s / ${snapshot.targetDurationSec}s`}
</Text>
<ScrobbleConditionProgress value={durationConditionPct} />
</Stack>
<Stack gap="xs">
<Text size="xs">
{`${listenPercentOfTrack.toFixed(1)}% / ${snapshot.targetPercentage}%`}
</Text>
<ScrobbleConditionProgress value={percentConditionPct} />
</Stack>
<Group gap="xs" grow wrap="nowrap">
<Button
disabled={!snapshot.songId}
onClick={() => invokeScrobbleResetListenedState()}
size="xs"
variant="outline"
>
{t('common.reset')}
</Button>
<Button
disabled={!snapshot.songId || snapshot.submitted}
onClick={() => invokeScrobbleForceSubmit()}
size="xs"
variant="filled"
>
{t('player.scrobbleForceSubmit')}
</Button>
</Group>
</>
)}
</Stack>
</HoverCard.Dropdown>
</HoverCard>
);
};
@@ -1,9 +1,10 @@
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef } from 'react';
import { useItemImageUrl } from '/@/renderer/components/item-image/item-image'; import { useItemImageUrl } from '/@/renderer/components/item-image/item-image';
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events'; import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
import { useSendScrobble } from '/@/renderer/features/player/mutations/scrobble-mutation'; import { useSendScrobble } from '/@/renderer/features/player/mutations/scrobble-mutation';
import { import {
publishScrobbleDebug,
useAppStore, useAppStore,
usePlaybackSettings, usePlaybackSettings,
usePlayerSong, usePlayerSong,
@@ -16,34 +17,64 @@ import { logMsg } from '/@/renderer/utils/logger-message';
import { LibraryItem, QueueSong, ServerType } from '/@/shared/types/domain-types'; import { LibraryItem, QueueSong, ServerType } from '/@/shared/types/domain-types';
import { PlayerStatus } from '/@/shared/types/types'; import { PlayerStatus } from '/@/shared/types/types';
type ScrobbleManualHandlers = {
forceSubmitScrobble: () => void;
resetListenedState: () => void;
};
let scrobbleManualHandlers: null | ScrobbleManualHandlers = null;
export const registerScrobbleManualHandlers = (next: null | ScrobbleManualHandlers) => {
scrobbleManualHandlers = next;
};
export const invokeScrobbleForceSubmit = () => {
scrobbleManualHandlers?.forceSubmitScrobble();
};
export const invokeScrobbleResetListenedState = () => {
scrobbleManualHandlers?.resetListenedState();
};
/* /*
Scrobble Conditions (match any): Submission (Last.fm / etc.) eligibility uses accumulated listen time:
- If the song has been played for the required percentage - If listened time meets the required percentage of track duration
- If the song has been played for the required duration - If listened time meets the required duration (seconds)
Scrobble Events: Listen time advances only while PLAYING, from consecutive timestamp deltas.
- On song timestamp update: Seeks and other timeline jumps re-baseline the next sample without counting
- If the song has been played for the required percentage the jump as listen time; accumulated listen time is kept across seeks.
- If the song has been played for the required duration
- When the song changes (or is completed): Listen time and submission state reset when the playhead returns to the start
- Current song: Sends the 'playing' scrobble event of the track (position before SCROBBLE_TRACK_BEGIN_SEC), e.g. seek-to-start or
- Resets the 'isCurrentSongScrobbled' state to false restart-from-near-zero. Song change and repeat still reset for a new play-through.
- When the song is restarted: Jellyfin progress APIs still use playback position (ticks), not listen time:
- Sends the 'submission' scrobble event if conditions are met AND the 'isCurrentSongScrobbled' state is false - Periodic timeupdate while playing
- Resets the 'isCurrentSongScrobbled' state to false - timeupdate on seek
- pause / unpause
- When the song is seeked: Other events:
- Sends the 'timeupdate' scrobble event (Jellyfin only) - When the song changes: sends 'start' when the new track is playing;
clears submission flag and listen accumulator for the new track.
- When the song is restarted (near 0 after 10s+): clears submission flag
and listen accumulator.
Progress Events: - When the song is seeked: Jellyfin sends timeupdate (throttled). Seeking from
- When the song is playing (Jellyfin only): at/after the intro into the start of the track clears listen accumulator and
- Sends the 'progress' scrobble event on an interval submission flag; other seeks keep accumulated listen time.
*/ */
// Positions before this time (seconds) count as the start of the track for listen/scrobble resets.
const SCROBBLE_TRACK_BEGIN_SEC = 5;
// Min previous position (seconds) to treat a jump to the start as a full restart.
const SCROBBLE_RESTART_PREVIOUS_MIN_SEC = 10;
// Max seconds between timestamp samples to count as continuous play (above poll interval, below a teleport).
const MAX_LISTEN_DELTA_SEC = 5;
const checkScrobbleConditions = (args: { const checkScrobbleConditions = (args: {
scrobbleAtDurationMs: number; scrobbleAtDurationMs: number;
scrobbleAtPercentage: number; scrobbleAtPercentage: number;
@@ -56,10 +87,10 @@ const checkScrobbleConditions = (args: {
? (songCompletedDurationMs / songDurationMs) * 100 ? (songCompletedDurationMs / songDurationMs) * 100
: 0; : 0;
const shouldScrobbleBasedOnPercetange = percentageOfSongCompleted >= scrobbleAtPercentage; const shouldScrobbleBasedOnPercentage = percentageOfSongCompleted >= scrobbleAtPercentage;
const shouldScrobbleBasedOnDuration = songCompletedDurationMs >= scrobbleAtDurationMs; const shouldScrobbleBasedOnDuration = songCompletedDurationMs >= scrobbleAtDurationMs;
return shouldScrobbleBasedOnPercetange || shouldScrobbleBasedOnDuration; return shouldScrobbleBasedOnPercentage || shouldScrobbleBasedOnDuration;
}; };
export const useScrobble = () => { export const useScrobble = () => {
@@ -77,7 +108,12 @@ export const useScrobble = () => {
}); });
const imageUrlRef = useRef<null | string | undefined>(imageUrl); const imageUrlRef = useRef<null | string | undefined>(imageUrl);
const [isCurrentSongScrobbled, setIsCurrentSongScrobbled] = useState(false); const isCurrentSongScrobbledRef = useRef(false);
const listenedMsRef = useRef(0);
const lastListenSampleTimeRef = useRef<null | number>(null);
const scrobbleAtDurationMsRef = useRef(0);
const scrobbleAtPercentageRef = useRef(75);
const previousSongRef = useRef<QueueSong | undefined>(undefined); const previousSongRef = useRef<QueueSong | undefined>(undefined);
const previousTimestampRef = useRef<number>(0); const previousTimestampRef = useRef<number>(0);
const lastProgressEventRef = useRef<number>(0); const lastProgressEventRef = useRef<number>(0);
@@ -89,30 +125,101 @@ export const useScrobble = () => {
imageUrlRef.current = imageUrl; imageUrlRef.current = imageUrl;
}, [imageUrl]); }, [imageUrl]);
useEffect(() => {
scrobbleAtDurationMsRef.current = (scrobbleSettings?.scrobbleAtDuration ?? 0) * 1000;
scrobbleAtPercentageRef.current = scrobbleSettings?.scrobbleAtPercentage ?? 75;
}, [scrobbleSettings?.scrobbleAtDuration, scrobbleSettings?.scrobbleAtPercentage]);
const flushScrobbleDebug = useCallback(() => {
const song = usePlayerStore.getState().getCurrentSong();
const status = usePlayerStore.getState().player.status;
const positionSec = useTimestampStoreBase.getState().timestamp;
const trackDurationMs = song?.duration ?? 0;
const eligibilityMet = Boolean(
song?.id &&
checkScrobbleConditions({
scrobbleAtDurationMs: scrobbleAtDurationMsRef.current,
scrobbleAtPercentage: scrobbleAtPercentageRef.current,
songCompletedDurationMs: listenedMsRef.current,
songDurationMs: trackDurationMs,
}),
);
publishScrobbleDebug({
eligibilityMet,
lastListenSampleTimeSec: lastListenSampleTimeRef.current,
listenedMs: listenedMsRef.current,
playerStatus: status,
positionSec,
songId: song?.id,
songName: song?.name,
submitted: isCurrentSongScrobbledRef.current,
targetDurationSec: scrobbleAtDurationMsRef.current / 1000,
targetPercentage: scrobbleAtPercentageRef.current,
trackDurationMs,
});
}, []);
const handleScrobbleFromProgress = useCallback( const handleScrobbleFromProgress = useCallback(
(properties: { timestamp: number }, prev: { timestamp: number }) => { (properties: { timestamp: number }, prev: { timestamp: number }) => {
if (!isScrobbleEnabled || isPrivateModeEnabled) return; if (!isScrobbleEnabled || isPrivateModeEnabled) return;
const currentSong = usePlayerStore.getState().getCurrentSong(); const currentSong = usePlayerStore.getState().getCurrentSong();
const currentStatus = usePlayerStore.getState().player.status; const currentStatus = usePlayerStore.getState().player.status;
if (!currentSong?.id || currentStatus !== PlayerStatus.PLAYING) return;
const currentTime = properties.timestamp; const currentTime = properties.timestamp;
const previousTime = prev.timestamp; const previousTime = prev.timestamp;
// Detect song restart: when timestamp resets to near 0 and was playing for at least 10 seconds if (!currentSong?.id) {
return;
}
if (currentStatus !== PlayerStatus.PLAYING) {
lastListenSampleTimeRef.current = currentTime;
return;
}
// Detect song restart: when timestamp resets to near 0 and was playing past the intro
if ( if (
currentTime < previousTime && currentTime < previousTime &&
currentTime < 5 && // Reset to near 0 currentTime < SCROBBLE_TRACK_BEGIN_SEC &&
previousTime >= 10 // Was playing for at least 10 seconds previousTime >= SCROBBLE_RESTART_PREVIOUS_MIN_SEC
) { ) {
setIsCurrentSongScrobbled(false); isCurrentSongScrobbledRef.current = false;
lastProgressEventRef.current = 0; lastProgressEventRef.current = 0;
previousTimestampRef.current = 0; previousTimestampRef.current = 0;
listenedMsRef.current = 0;
lastListenSampleTimeRef.current = null;
return; return;
} }
const lastSample = lastListenSampleTimeRef.current;
if (lastSample === null) {
const prevSec = prev.timestamp;
if (currentTime > prevSec && currentTime - prevSec <= MAX_LISTEN_DELTA_SEC) {
listenedMsRef.current += (currentTime - prevSec) * 1000;
}
lastListenSampleTimeRef.current = currentTime;
} else {
const deltaSec = currentTime - lastSample;
const jumpedBackToTrackStart =
currentTime < lastSample &&
currentTime < SCROBBLE_TRACK_BEGIN_SEC &&
lastSample >= SCROBBLE_TRACK_BEGIN_SEC;
if (jumpedBackToTrackStart) {
listenedMsRef.current = 0;
isCurrentSongScrobbledRef.current = false;
lastProgressEventRef.current = 0;
lastListenSampleTimeRef.current = currentTime;
} else if (currentTime < lastSample || deltaSec > MAX_LISTEN_DELTA_SEC) {
lastListenSampleTimeRef.current = currentTime;
} else if (deltaSec > 0) {
listenedMsRef.current += deltaSec * 1000;
lastListenSampleTimeRef.current = currentTime;
}
}
// Send Jellyfin progress events every 10 seconds // Send Jellyfin progress events every 10 seconds
if (currentSong._serverType === ServerType.JELLYFIN) { if (currentSong._serverType === ServerType.JELLYFIN) {
const timeSinceLastProgress = currentTime - lastProgressEventRef.current; const timeSinceLastProgress = currentTime - lastProgressEventRef.current;
@@ -144,12 +251,12 @@ export const useScrobble = () => {
} }
} }
// Check if we should submit scrobble based on conditions // Check if we should submit scrobble based on listened time
if (!isCurrentSongScrobbled) { if (!isCurrentSongScrobbledRef.current) {
const shouldSubmitScrobble = checkScrobbleConditions({ const shouldSubmitScrobble = checkScrobbleConditions({
scrobbleAtDurationMs: (scrobbleSettings?.scrobbleAtDuration ?? 0) * 1000, scrobbleAtDurationMs: scrobbleAtDurationMsRef.current,
scrobbleAtPercentage: scrobbleSettings?.scrobbleAtPercentage, scrobbleAtPercentage: scrobbleAtPercentageRef.current,
songCompletedDurationMs: currentTime * 1000, songCompletedDurationMs: listenedMsRef.current,
songDurationMs: currentSong.duration, songDurationMs: currentSong.duration,
}); });
@@ -177,25 +284,18 @@ export const useScrobble = () => {
category: LogCategory.SCROBBLE, category: LogCategory.SCROBBLE,
meta: { meta: {
id: currentSong.id, id: currentSong.id,
reason: 'from song progress', reason: 'from listened time',
}, },
}); });
}, },
}, },
); );
setIsCurrentSongScrobbled(true); isCurrentSongScrobbledRef.current = true;
} }
} }
}, },
[ [isScrobbleEnabled, isPrivateModeEnabled, sendScrobble],
isScrobbleEnabled,
isPrivateModeEnabled,
scrobbleSettings?.scrobbleAtDuration,
scrobbleSettings?.scrobbleAtPercentage,
isCurrentSongScrobbled,
sendScrobble,
],
); );
const handleScrobbleFromSongChange = useCallback( const handleScrobbleFromSongChange = useCallback(
@@ -240,11 +340,16 @@ export const useScrobble = () => {
if (!isScrobbleEnabled || isPrivateModeEnabled) { if (!isScrobbleEnabled || isPrivateModeEnabled) {
previousSongRef.current = currentSong; previousSongRef.current = currentSong;
previousTimestampRef.current = 0; previousTimestampRef.current = 0;
listenedMsRef.current = 0;
lastListenSampleTimeRef.current = null;
flushScrobbleDebug();
return; return;
} }
setIsCurrentSongScrobbled(false); isCurrentSongScrobbledRef.current = false;
lastProgressEventRef.current = 0; lastProgressEventRef.current = 0;
listenedMsRef.current = 0;
lastListenSampleTimeRef.current = null;
// Use a timeout to prevent spamming the server when switching songs quickly // Use a timeout to prevent spamming the server when switching songs quickly
clearTimeout(songChangeTimeoutRef.current); clearTimeout(songChangeTimeoutRef.current);
@@ -280,8 +385,15 @@ export const useScrobble = () => {
previousSongRef.current = currentSong; previousSongRef.current = currentSong;
previousTimestampRef.current = 0; previousTimestampRef.current = 0;
flushScrobbleDebug();
}, },
[scrobbleSettings?.notify, isScrobbleEnabled, isPrivateModeEnabled, sendScrobble], [
flushScrobbleDebug,
scrobbleSettings?.notify,
isScrobbleEnabled,
isPrivateModeEnabled,
sendScrobble,
],
); );
const handleScrobbleFromSeek = useCallback( const handleScrobbleFromSeek = useCallback(
@@ -297,8 +409,21 @@ export const useScrobble = () => {
return; return;
} }
const sampleBeforeSeek = lastListenSampleTimeRef.current;
lastListenSampleTimeRef.current = properties.timestamp;
if (
properties.timestamp < SCROBBLE_TRACK_BEGIN_SEC &&
(sampleBeforeSeek === null || sampleBeforeSeek >= SCROBBLE_TRACK_BEGIN_SEC)
) {
listenedMsRef.current = 0;
isCurrentSongScrobbledRef.current = false;
lastProgressEventRef.current = 0;
}
// Position scrobbles are only relevant for Jellyfin // Position scrobbles are only relevant for Jellyfin
if (currentSong._serverType !== ServerType.JELLYFIN) { if (currentSong._serverType !== ServerType.JELLYFIN) {
flushScrobbleDebug();
return; return;
} }
@@ -307,6 +432,7 @@ export const useScrobble = () => {
// Only allow seek scrobble once per second // Only allow seek scrobble once per second
if (timeSinceLastSeek < 1000) { if (timeSinceLastSeek < 1000) {
flushScrobbleDebug();
return; return;
} }
@@ -337,8 +463,9 @@ export const useScrobble = () => {
}, },
}, },
); );
flushScrobbleDebug();
}, },
[isScrobbleEnabled, isPrivateModeEnabled, sendScrobble], [flushScrobbleDebug, isScrobbleEnabled, isPrivateModeEnabled, sendScrobble],
); );
const handleScrobbleFromStatus = useCallback( const handleScrobbleFromStatus = useCallback(
@@ -412,8 +539,10 @@ export const useScrobble = () => {
}, },
); );
} }
flushScrobbleDebug();
}, },
[isScrobbleEnabled, isPrivateModeEnabled, sendScrobble], [flushScrobbleDebug, isScrobbleEnabled, isPrivateModeEnabled, sendScrobble],
); );
const handleScrobbleFromRepeat = useCallback(() => { const handleScrobbleFromRepeat = useCallback(() => {
@@ -428,9 +557,11 @@ export const useScrobble = () => {
return; return;
} }
setIsCurrentSongScrobbled(false); isCurrentSongScrobbledRef.current = false;
lastProgressEventRef.current = 0; lastProgressEventRef.current = 0;
previousTimestampRef.current = 0; previousTimestampRef.current = 0;
listenedMsRef.current = 0;
lastListenSampleTimeRef.current = null;
sendScrobble.mutate( sendScrobble.mutate(
{ {
@@ -455,17 +586,81 @@ export const useScrobble = () => {
}, },
}, },
); );
}, [isScrobbleEnabled, isPrivateModeEnabled, sendScrobble]); flushScrobbleDebug();
}, [flushScrobbleDebug, isScrobbleEnabled, isPrivateModeEnabled, sendScrobble]);
// Update previous timestamp on progress for use in status change handler // Update previous timestamp on progress for use in status change handler
const handleProgressUpdate = useCallback( const handleProgressUpdate = useCallback(
(properties: { timestamp: number }, prev: { timestamp: number }) => { (properties: { timestamp: number }, prev: { timestamp: number }) => {
previousTimestampRef.current = properties.timestamp; previousTimestampRef.current = properties.timestamp;
handleScrobbleFromProgress(properties, prev); handleScrobbleFromProgress(properties, prev);
flushScrobbleDebug();
}, },
[handleScrobbleFromProgress], [flushScrobbleDebug, handleScrobbleFromProgress],
); );
useEffect(() => {
registerScrobbleManualHandlers({
forceSubmitScrobble: () => {
if (!isScrobbleEnabled || isPrivateModeEnabled) {
return;
}
const song = usePlayerStore.getState().getCurrentSong();
if (!song?.id) {
return;
}
const position =
song._serverType === ServerType.JELLYFIN ? song.duration * 1e7 : undefined;
sendScrobble.mutate(
{
apiClientProps: { serverId: song._serverId || '' },
query: {
albumId: song.albumId,
id: song.id,
position,
submission: true,
},
},
{
onSuccess: () => {
logFn.debug(logMsg[LogCategory.SCROBBLE].scrobbledSubmission, {
category: LogCategory.SCROBBLE,
meta: {
id: song.id,
reason: 'forced from UI',
},
});
},
},
);
isCurrentSongScrobbledRef.current = true;
flushScrobbleDebug();
},
resetListenedState: () => {
if (!isScrobbleEnabled || isPrivateModeEnabled) {
return;
}
const song = usePlayerStore.getState().getCurrentSong();
if (!song?.id) {
return;
}
listenedMsRef.current = 0;
isCurrentSongScrobbledRef.current = false;
lastProgressEventRef.current = 0;
lastListenSampleTimeRef.current = useTimestampStoreBase.getState().timestamp;
flushScrobbleDebug();
},
});
return () => registerScrobbleManualHandlers(null);
}, [flushScrobbleDebug, isPrivateModeEnabled, isScrobbleEnabled, sendScrobble]);
usePlayerEvents( usePlayerEvents(
{ {
onCurrentSongChange: handleScrobbleFromSongChange, onCurrentSongChange: handleScrobbleFromSongChange,
+3
View File
@@ -65,13 +65,16 @@ declare global {
FS_LYRICS_TRANSLATION_API_KEY?: string; FS_LYRICS_TRANSLATION_API_KEY?: string;
FS_LYRICS_TRANSLATION_TARGET_LANGUAGE?: string; FS_LYRICS_TRANSLATION_TARGET_LANGUAGE?: string;
FS_PLAYBACK_AUDIO_FADE_ON_STATUS_CHANGE?: string; FS_PLAYBACK_AUDIO_FADE_ON_STATUS_CHANGE?: string;
FS_PLAYBACK_FILTERS?: string;
FS_PLAYBACK_MEDIA_SESSION?: string; FS_PLAYBACK_MEDIA_SESSION?: string;
FS_PLAYBACK_PRESERVE_PITCH?: string; FS_PLAYBACK_PRESERVE_PITCH?: string;
FS_PLAYBACK_SCROBBLE_AT_DURATION?: string; FS_PLAYBACK_SCROBBLE_AT_DURATION?: string;
FS_PLAYBACK_SCROBBLE_AT_PERCENTAGE?: string; FS_PLAYBACK_SCROBBLE_AT_PERCENTAGE?: string;
FS_PLAYBACK_SCROBBLE_ENABLED?: string; FS_PLAYBACK_SCROBBLE_ENABLED?: string;
FS_PLAYBACK_SCROBBLE_NOTIFY?: string; FS_PLAYBACK_SCROBBLE_NOTIFY?: string;
FS_PLAYBACK_TRANSCODE_BITRATE?: string;
FS_PLAYBACK_TRANSCODE_ENABLED?: string; FS_PLAYBACK_TRANSCODE_ENABLED?: string;
FS_PLAYBACK_TRANSCODE_FORMAT?: string;
FS_PLAYBACK_WEB_AUDIO?: string; FS_PLAYBACK_WEB_AUDIO?: string;
LEGACY_AUTHENTICATION?: boolean | string; LEGACY_AUTHENTICATION?: boolean | string;
REMOTE_URL?: string; REMOTE_URL?: string;
+101 -17
View File
@@ -1,7 +1,73 @@
import type { SettingsState } from './settings.store'; import type { PlayerFilter, SettingsState } from './settings.store';
import { sanitizeCss } from '/@/renderer/utils/sanitize'; import { sanitizeCss } from '/@/renderer/utils/sanitize';
const PLAYER_FILTER_FIELDS = new Set([
'albumArtist',
'artist',
'duration',
'favorite',
'genre',
'name',
'note',
'path',
'playCount',
'rating',
'year',
]);
const PLAYER_FILTER_OPERATORS = new Set([
'after',
'afterDate',
'before',
'beforeDate',
'contains',
'endsWith',
'gt',
'inTheLast',
'inTheRange',
'inTheRangeDate',
'is',
'isNot',
'lt',
'notContains',
'notInTheLast',
'regex',
'startsWith',
]);
function isValidPlayerFilter(item: unknown): item is PlayerFilter {
if (!item || typeof item !== 'object' || Array.isArray(item)) return false;
const o = item as Record<string, unknown>;
if (typeof o.id !== 'string') return false;
if (typeof o.field !== 'string' || !PLAYER_FILTER_FIELDS.has(o.field)) return false;
if (typeof o.operator !== 'string' || !PLAYER_FILTER_OPERATORS.has(o.operator)) return false;
if (!isValidPlayerFilterValue(o.value)) return false;
if (o.isEnabled !== undefined && typeof o.isEnabled !== 'boolean') return false;
return true;
}
function isValidPlayerFilterValue(value: unknown): boolean {
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
return true;
}
if (!Array.isArray(value)) return false;
return value.every((v) => typeof v === 'string' || typeof v === 'number');
}
function parsePlaybackFiltersJson(raw: string): unknown {
const t = raw.trim();
if (t === '') return undefined;
try {
const v = JSON.parse(t) as unknown;
if (!Array.isArray(v)) return undefined;
if (!v.every(isValidPlayerFilter)) return undefined;
return v;
} catch {
return undefined;
}
}
const APP_THEMES = new Set([ const APP_THEMES = new Set([
'ayuDark', 'ayuDark',
'ayuLight', 'ayuLight',
@@ -55,28 +121,29 @@ type DeepPartial<T> = {
interface EnvSettingSpec { interface EnvSettingSpec {
enumSet?: Set<string>; enumSet?: Set<string>;
key: string; key: string;
path: [string, string, string] | [string, string]; path: readonly string[];
skipIfEmpty?: boolean; skipIfEmpty?: boolean;
transform?: (raw: string) => unknown; transform?: (raw: string) => unknown;
type: 'bool' | 'enum' | 'num' | 'string'; type: 'bool' | 'enum' | 'num' | 'string';
} }
function setAtPath( function setAtPath(obj: EnvSettingsOverrides, path: readonly string[], value: unknown): void {
obj: EnvSettingsOverrides, if (path.length < 2) return;
path: [string, string, string] | [string, string], let cur: Record<string, unknown> = obj as Record<string, unknown>;
value: unknown, for (let i = 0; i < path.length - 1; i++) {
): void { const key = path[i]!;
const [a, b, c] = path; const existing = cur[key];
const root = (obj as Record<string, unknown>)[a] ?? {}; const next: Record<string, unknown> =
(obj as Record<string, unknown>)[a] = root; existing !== null &&
const branch = root as Record<string, unknown>; existing !== undefined &&
if (c === undefined) { typeof existing === 'object' &&
branch[b] = value; !Array.isArray(existing)
} else { ? { ...(existing as Record<string, unknown>) }
const nested = branch[b] ?? {}; : {};
branch[b] = nested; cur[key] = next;
(nested as Record<string, unknown>)[c] = value; cur = next;
} }
cur[path[path.length - 1]!] = value;
} }
const RGB_ACCENT_REGEX = /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/; const RGB_ACCENT_REGEX = /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/;
@@ -252,6 +319,23 @@ const ENV_SETTING_SPECS: EnvSettingSpec[] = [
path: ['playback', 'transcode', 'enabled'], path: ['playback', 'transcode', 'enabled'],
type: 'bool', type: 'bool',
}, },
{
key: 'FS_PLAYBACK_TRANSCODE_FORMAT',
path: ['playback', 'transcode', 'format'],
skipIfEmpty: true,
type: 'string',
},
{
key: 'FS_PLAYBACK_TRANSCODE_BITRATE',
path: ['playback', 'transcode', 'bitrate'],
type: 'num',
},
{
key: 'FS_PLAYBACK_FILTERS',
path: ['playback', 'filters'],
transform: parsePlaybackFiltersJson,
type: 'string',
},
{ key: 'FS_DISCORD_ENABLED', path: ['discord', 'enabled'], type: 'bool' }, { key: 'FS_DISCORD_ENABLED', path: ['discord', 'enabled'], type: 'bool' },
{ {
key: 'FS_DISCORD_CLIENT_ID', key: 'FS_DISCORD_CLIENT_ID',
+1
View File
@@ -2,6 +2,7 @@ export * from './app.store';
export * from './auth.store'; export * from './auth.store';
export * from './full-screen-player.store'; export * from './full-screen-player.store';
export * from './player.store'; export * from './player.store';
export * from './scrobble-debug.store';
export * from './scroll.store'; export * from './scroll.store';
export * from './settings.store'; export * from './settings.store';
export * from './timestamp.store'; export * from './timestamp.store';
@@ -0,0 +1,45 @@
import { createWithEqualityFn } from 'zustand/traditional';
import { PlayerStatus } from '/@/shared/types/types';
export type ScrobbleDebugSnapshot = {
eligibilityMet: boolean;
lastListenSampleTimeSec: null | number;
listenedMs: number;
playerStatus: PlayerStatus;
positionSec: number;
songId?: string;
songName?: string;
submitted: boolean;
targetDurationSec: number;
targetPercentage: number;
trackDurationMs: number;
};
const initialSnapshot: ScrobbleDebugSnapshot = {
eligibilityMet: false,
lastListenSampleTimeSec: null,
listenedMs: 0,
playerStatus: PlayerStatus.PAUSED,
positionSec: 0,
songId: undefined,
songName: undefined,
submitted: false,
targetDurationSec: 240,
targetPercentage: 75,
trackDurationMs: 0,
};
type ScrobbleDebugStore = {
snapshot: ScrobbleDebugSnapshot;
};
export const useScrobbleDebugStore = createWithEqualityFn<ScrobbleDebugStore>()(() => ({
snapshot: initialSnapshot,
}));
export const publishScrobbleDebug = (partial: Partial<ScrobbleDebugSnapshot>) => {
useScrobbleDebugStore.setState((state) => ({
snapshot: { ...state.snapshot, ...partial },
}));
};
+2 -1
View File
@@ -518,8 +518,9 @@ export const sortAlbumList = (albums: Album[], sortBy: AlbumListSort, sortOrder:
} }
return 0; return 0;
}, },
(v) => v.name.toLowerCase(),
], ],
[order, order], [order, order, 'asc'],
); );
break; break;
case AlbumListSort.SONG_COUNT: case AlbumListSort.SONG_COUNT:
@@ -79,6 +79,10 @@ img.size-5xl {
color: rgb(255 49 49); color: rgb(255 49 49);
} }
.color-transparent {
color: transparent;
}
.fill { .fill {
fill: transparent; fill: transparent;
} }
@@ -123,6 +127,10 @@ img.size-5xl {
fill: rgb(255 49 49); fill: rgb(255 49 49);
} }
.fill-transparent {
fill: transparent;
}
.spin { .spin {
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
} }
+7 -2
View File
@@ -35,6 +35,7 @@ import {
LuChevronLeft, LuChevronLeft,
LuChevronRight, LuChevronRight,
LuChevronUp, LuChevronUp,
LuCircle,
LuCircleCheck, LuCircleCheck,
LuCircleX, LuCircleX,
LuClipboardCopy, LuClipboardCopy,
@@ -247,6 +248,7 @@ export const AppIcon = {
brandSpotify: SpotifyLogoIcon, brandSpotify: SpotifyLogoIcon,
cache: LuCloudDownload, cache: LuCloudDownload,
check: LuCheck, check: LuCheck,
circle: LuCircle,
clipboardCopy: LuClipboardCopy, clipboardCopy: LuClipboardCopy,
collection: LuPackage2, collection: LuPackage2,
delete: LuTrash, delete: LuTrash,
@@ -384,6 +386,7 @@ type IconColor =
| 'muted' | 'muted'
| 'primary' | 'primary'
| 'success' | 'success'
| 'transparent'
| 'warn'; | 'warn';
const _Icon = forwardRef<HTMLDivElement, IconProps>((props, ref) => { const _Icon = forwardRef<HTMLDivElement, IconProps>((props, ref) => {
@@ -391,17 +394,19 @@ const _Icon = forwardRef<HTMLDivElement, IconProps>((props, ref) => {
const IconComponent: ComponentType<any> = AppIcon[icon]; const IconComponent: ComponentType<any> = AppIcon[icon];
const colorClassToken = color ?? (fill && fill !== 'transparent' ? fill : undefined);
const classNames = useMemo( const classNames = useMemo(
() => () =>
clsx(className, { clsx(className, {
[styles.fill]: true, [styles.fill]: true,
[styles.pulse]: animate === 'pulse', [styles.pulse]: animate === 'pulse',
[styles.spin]: animate === 'spin', [styles.spin]: animate === 'spin',
[styles[`color-${color || fill}`]]: color || fill, [styles[`color-${colorClassToken}`]]: colorClassToken,
[styles[`fill-${fill}`]]: fill, [styles[`fill-${fill}`]]: fill,
[styles[`size-${size}`]]: true, [styles[`size-${size}`]]: true,
}), }),
[animate, className, color, fill, size], [animate, className, colorClassToken, fill, size],
); );
return ( return (
@@ -0,0 +1,7 @@
.root {
width: 100%;
}
.section {
transition: width 0.15s ease-out;
}
@@ -0,0 +1,29 @@
import type { ProgressProps as MantineProgressProps } from '@mantine/core';
import { Progress as MantineProgress } from '@mantine/core';
import { forwardRef } from 'react';
import styles from './progress.module.css';
export interface ProgressProps extends MantineProgressProps {}
export const Progress = forwardRef<HTMLDivElement, ProgressProps>(
({ classNames, style, ...props }, ref) => {
return (
<MantineProgress
classNames={{
root: styles.root,
section: styles.section,
...classNames,
}}
ref={ref}
style={{
...style,
}}
{...props}
/>
);
},
);
Progress.displayName = 'Progress';