Compare commits

..

5 Commits

Author SHA1 Message Date
jeffvli c03f611547 convert remaining locales to sentence case 2026-05-11 19:26:15 -07:00
jeffvli bfbba60873 refactor some strings to sentence case 2026-05-11 19:25:51 -07:00
jeffvli 808b9f2e9d remove sentence case override configuration 2026-05-11 19:25:10 -07:00
jeffvli 36d9f513ac convert locales to new casing 2026-05-11 19:25:10 -07:00
jeffvli 737f1afd76 convert EN localization to use proper casing, remove postprocessing from renderer 2026-05-11 19:19:01 -07:00
70 changed files with 593 additions and 3836 deletions
-9
View File
@@ -43,13 +43,7 @@ These variables override app settings **on first run** when no persisted setting
| `general.showVisualizerInSidebar` | `true` | `FS_GENERAL_SHOW_VISUALIZER_IN_SIDEBAR` | `true` / `false` — Show visualizer in sidebar. | | `general.showVisualizerInSidebar` | `true` | `FS_GENERAL_SHOW_VISUALIZER_IN_SIDEBAR` | `true` / `false` — Show visualizer in sidebar. |
| `general.sidebarCollapsedNavigation` | `true` | `FS_GENERAL_SIDEBAR_COLLAPSED_NAVIGATION` | `true` / `false` — Start with collapsed sidebar nav. | | `general.sidebarCollapsedNavigation` | `true` | `FS_GENERAL_SIDEBAR_COLLAPSED_NAVIGATION` | `true` / `false` — Start with collapsed sidebar nav. |
| `general.sidebarCollapseShared` | `false` | `FS_GENERAL_SIDEBAR_COLLAPSE_SHARED` | `true` / `false` — Share sidebar collapse state. | | `general.sidebarCollapseShared` | `false` | `FS_GENERAL_SIDEBAR_COLLAPSE_SHARED` | `true` / `false` — Share sidebar collapse state. |
| `general.sidebarPlaylistFolders` | `true` | `FS_GENERAL_SIDEBAR_PLAYLIST_FOLDERS` | `true` / `false` — Group playlists into folders by name separator. |
| `general.sidebarPlaylistFolderSeparator` | `/` | `FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_SEPARATOR` | Character or string that separates folder levels in a playlist name. Empty = use default. |
| `general.sidebarPlaylistFolderTreeIndent` | `16` | `FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_TREE_INDENT` | Pixels each tree level is indented (064). |
| `general.sidebarPlaylistFolderTreeLineColor` | *(empty)* | `FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_TREE_LINE_COLOR` | CSS color for tree connecting lines. Empty = theme default. |
| `general.sidebarPlaylistFolderView` | `tree` | `FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_VIEW` | `single` / `tree` / `navigation` — How folders are displayed in the sidebar. |
| `general.sidebarPlaylistList` | `true` | `FS_GENERAL_SIDEBAR_PLAYLIST_LIST` | `true` / `false` — Show playlist list in sidebar. | | `general.sidebarPlaylistList` | `true` | `FS_GENERAL_SIDEBAR_PLAYLIST_LIST` | `true` / `false` — Show playlist list in sidebar. |
| `general.sidebarPlaylistMode` | `expanded` | `FS_GENERAL_SIDEBAR_PLAYLIST_MODE` | `compact` / `expanded` — Sidebar playlist row layout. |
| `general.sidebarPlaylistSorting` | `false` | `FS_GENERAL_SIDEBAR_PLAYLIST_SORTING` | `true` / `false` — Enable playlist sorting in sidebar. | | `general.sidebarPlaylistSorting` | `false` | `FS_GENERAL_SIDEBAR_PLAYLIST_SORTING` | `true` / `false` — Enable playlist sorting in sidebar. |
| `general.sideQueueType` | `sideQueue` | `FS_GENERAL_SIDE_QUEUE_TYPE` | `sideDrawerQueue` / `sideQueue` — Side play queue style. | | `general.sideQueueType` | `sideQueue` | `FS_GENERAL_SIDE_QUEUE_TYPE` | `sideDrawerQueue` / `sideQueue` — Side play queue style. |
| `general.sideQueueLayout` | `horizontal` | `FS_GENERAL_SIDE_QUEUE_LAYOUT` | `horizontal` / `vertical` — Attached side queue layout orientation. | | `general.sideQueueLayout` | `horizontal` | `FS_GENERAL_SIDE_QUEUE_LAYOUT` | `horizontal` / `vertical` — Attached side queue layout orientation. |
@@ -72,9 +66,6 @@ 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. |
--- ---
-9
View File
@@ -38,13 +38,7 @@ window.FS_GENERAL_SHOW_RATINGS = "${FS_GENERAL_SHOW_RATINGS}";
window.FS_GENERAL_SHOW_VISUALIZER_IN_SIDEBAR = "${FS_GENERAL_SHOW_VISUALIZER_IN_SIDEBAR}"; window.FS_GENERAL_SHOW_VISUALIZER_IN_SIDEBAR = "${FS_GENERAL_SHOW_VISUALIZER_IN_SIDEBAR}";
window.FS_GENERAL_SIDEBAR_COLLAPSED_NAVIGATION = "${FS_GENERAL_SIDEBAR_COLLAPSED_NAVIGATION}"; window.FS_GENERAL_SIDEBAR_COLLAPSED_NAVIGATION = "${FS_GENERAL_SIDEBAR_COLLAPSED_NAVIGATION}";
window.FS_GENERAL_SIDEBAR_COLLAPSE_SHARED = "${FS_GENERAL_SIDEBAR_COLLAPSE_SHARED}"; window.FS_GENERAL_SIDEBAR_COLLAPSE_SHARED = "${FS_GENERAL_SIDEBAR_COLLAPSE_SHARED}";
window.FS_GENERAL_SIDEBAR_PLAYLIST_FOLDERS = "${FS_GENERAL_SIDEBAR_PLAYLIST_FOLDERS}";
window.FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_SEPARATOR = "${FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_SEPARATOR}";
window.FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_TREE_INDENT = "${FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_TREE_INDENT}";
window.FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_TREE_LINE_COLOR = "${FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_TREE_LINE_COLOR}";
window.FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_VIEW = "${FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_VIEW}";
window.FS_GENERAL_SIDEBAR_PLAYLIST_LIST = "${FS_GENERAL_SIDEBAR_PLAYLIST_LIST}"; window.FS_GENERAL_SIDEBAR_PLAYLIST_LIST = "${FS_GENERAL_SIDEBAR_PLAYLIST_LIST}";
window.FS_GENERAL_SIDEBAR_PLAYLIST_MODE = "${FS_GENERAL_SIDEBAR_PLAYLIST_MODE}";
window.FS_GENERAL_SIDEBAR_PLAYLIST_SORTING = "${FS_GENERAL_SIDEBAR_PLAYLIST_SORTING}"; window.FS_GENERAL_SIDEBAR_PLAYLIST_SORTING = "${FS_GENERAL_SIDEBAR_PLAYLIST_SORTING}";
window.FS_GENERAL_SIDE_QUEUE_TYPE = "${FS_GENERAL_SIDE_QUEUE_TYPE}"; window.FS_GENERAL_SIDE_QUEUE_TYPE = "${FS_GENERAL_SIDE_QUEUE_TYPE}";
window.FS_GENERAL_SIDE_QUEUE_LAYOUT = "${FS_GENERAL_SIDE_QUEUE_LAYOUT}"; window.FS_GENERAL_SIDE_QUEUE_LAYOUT = "${FS_GENERAL_SIDE_QUEUE_LAYOUT}";
@@ -64,9 +58,6 @@ 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",
+22 -44
View File
@@ -48,8 +48,7 @@
"sleepTimer_timeRemaining": "Zbývá {{time}}", "sleepTimer_timeRemaining": "Zbývá {{time}}",
"sleepTimer_setCustom": "Nastavit časovač", "sleepTimer_setCustom": "Nastavit časovač",
"sleepTimer_cancel": "Zrušit časovač", "sleepTimer_cancel": "Zrušit časovač",
"albumRadio": "Rádio alba", "albumRadio": "Rádio alba"
"scrobbleForceSubmit": "Vynutit scrobble"
}, },
"setting": { "setting": {
"crossfadeStyle_description": "Vyberte způsob prolnutí u přehrávače zvuku", "crossfadeStyle_description": "Vyberte způsob prolnutí u přehrávače zvuku",
@@ -247,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í",
@@ -430,25 +429,7 @@
"playerbarWaveformStretch": "Natáhnutí vlnové křivky", "playerbarWaveformStretch": "Natáhnutí vlnové křivky",
"playerbarWaveformStretch_description": "Natáhně vlnovou křivku tak, aby vyplnila dostupný prostor", "playerbarWaveformStretch_description": "Natáhně vlnovou křivku tak, aby vyplnila dostupný prostor",
"preventSuspendOnPlayback_description": "Zabránit aplikaci v uspání během přehrávání hudby", "preventSuspendOnPlayback_description": "Zabránit aplikaci v uspání během přehrávání hudby",
"preventSuspendOnPlayback": "Zabránit uspání při přehrávání", "preventSuspendOnPlayback": "Zabránit uspání při přehrávání"
"hotkey_listShowPlayingSong": "Zobrazit přehrávanou skladbu v seznamu",
"sidebarPlaylistFolders_description": "Vytvořit zobrazení složky pro seznamy skadeb, které zahrnují nastavený oddělovač v názvu",
"sidebarPlaylistFolders": "Povolit složky",
"sidebarPlaylistFolderSeparator_description": "Znak (nebo řetězec) oddělující úrovně složek v názvu seznamu skladeb",
"sidebarPlaylistFolderSeparator": "Oddělovač složek",
"sidebarPlaylistFolderView_description": "Jak jsou složky zobrazeny v postranní liště",
"sidebarPlaylistFolderView": "Zobrazení složky",
"sidebarPlaylistFolderView_optionSingle": "Jedna složka",
"sidebarPlaylistFolderView_optionTree": "Stromové zobrazení",
"sidebarPlaylistFolderView_optionNavigation": "Zobrazení navigace",
"sidebarPlaylistFolderTreeIndent_description": "Počet pixelů, o který je odsazená každá větev",
"sidebarPlaylistFolderTreeIndent": "Odsazení větví",
"sidebarPlaylistFolderTreeLineColor_description": "Barva spojujících linek větví (ponechte prázdné pro výchozí)",
"sidebarPlaylistFolderTreeLineColor": "Barva linky stromu",
"sidebarPlaylistMode_description": "Jak je každý seznam skladeb zobrazen v seznamu v postranní liště",
"sidebarPlaylistMode": "Režim seznamů skladeb v postranní liště",
"sidebarPlaylistMode_optionCompact": "Kompaktní",
"sidebarPlaylistMode_optionExpanded": "Rozšířený"
}, },
"action": { "action": {
"editPlaylist": "Upravit $t(entity.playlist, {\"count\": 1})", "editPlaylist": "Upravit $t(entity.playlist, {\"count\": 1})",
@@ -492,9 +473,7 @@
"addOrRemoveFromSelection": "Přidat nebo odebrat z výběru", "addOrRemoveFromSelection": "Přidat nebo odebrat z výběru",
"selectRangeOfItems": "Vyberte rozsah položek", "selectRangeOfItems": "Vyberte rozsah položek",
"selectAll": "Vybrat vše", "selectAll": "Vybrat vše",
"goToCurrent": "Přejít na aktuální položku", "goToCurrent": "Přejít na aktuální položku"
"collapseAllFolders": "Sbalit všechny složky",
"expandAllFolders": "Rozbalit všechny složky"
}, },
"common": { "common": {
"backward": "Zpátky", "backward": "Zpátky",
@@ -623,8 +602,7 @@
"rename": "Přejmenovat", "rename": "Přejmenovat",
"newVersionAvailable": "Je dostupná nová verze", "newVersionAvailable": "Je dostupná nová verze",
"numberOfResults": "{{numberOfResults}} výsledků", "numberOfResults": "{{numberOfResults}} výsledků",
"grouping": "Seskupování", "grouping": "Seskupování"
"back": "Zpět"
}, },
"table": { "table": {
"config": { "config": {
@@ -711,7 +689,7 @@
"rating": "Hodnocení", "rating": "Hodnocení",
"favorite": "Oblíbené", "favorite": "Oblíbené",
"playCount": "Přehrání", "playCount": "Přehrání",
"albumCount": "Alba", "albumCount": "$t(entity.album, {\"count\": 2})",
"releaseYear": "Rok", "releaseYear": "Rok",
"lastPlayed": "Naposledy přehráno", "lastPlayed": "Naposledy přehráno",
"biography": "Biografie", "biography": "Biografie",
@@ -719,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": "Umělec", "artist": "$t(entity.artist, {\"count\": 1})",
"songCount": "Skladby", "songCount": "$t(entity.track, {\"count\": 2})",
"trackNumber": "Skladba", "trackNumber": "Skladba",
"genre": "Žánr", "genre": "$t(entity.genre, {\"count\": 1})",
"albumArtist": "Umělec alba", "albumArtist": "Umělec alba",
"path": "Cesta", "path": "Cesta",
"discNumber": "Disk", "discNumber": "Disk",
"channels": "Kanály", "channels": "$t(common.channel, {\"count\": 2})",
"size": "Velikost", "size": "$t(common.size)",
"codec": "Kodek", "codec": "$t(common.codec)",
"owner": "Majitel", "owner": "Majitel",
"bitDepth": "Bitová hloubka", "bitDepth": "$t(common.bitDepth)",
"sampleRate": "Vzorkovací frekvence" "sampleRate": "$t(common.sampleRate)"
} }
}, },
"error": { "error": {
@@ -784,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í",
@@ -909,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",
@@ -1052,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)",
@@ -1064,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": "$t(entity.trackWithCount, {\"count\": {{message}} }) přidáno do $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })", "success": "PřIDáno $t(entity.trackWithCount, {\"count\": {{message}} }) 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})",
@@ -1161,7 +1139,7 @@
"folderWithCount_few": "{{count}} složky", "folderWithCount_few": "{{count}} složky",
"folderWithCount_other": "{{count}} složek", "folderWithCount_other": "{{count}} složek",
"albumArtist_one": "Umělec alba", "albumArtist_one": "Umělec alba",
"albumArtist_few": "Umělci alb", "albumArtist_few": "umělci alb",
"albumArtist_other": "Umělci alb", "albumArtist_other": "Umělci alb",
"track_one": "Skladba", "track_one": "Skladba",
"track_few": "skladby", "track_few": "skladby",
@@ -1198,7 +1176,7 @@
"song_few": "písničky", "song_few": "písničky",
"song_other": "Písní", "song_other": "Písní",
"radioStation_one": "Stanice rádia", "radioStation_one": "Stanice rádia",
"radioStation_few": "Stanice rádia", "radioStation_few": "stanice rádia",
"radioStation_other": "Stanice rádia", "radioStation_other": "Stanice rádia",
"radioStationWithCount_one": "{{count}} stanice rádia", "radioStationWithCount_one": "{{count}} stanice rádia",
"radioStationWithCount_few": "{{count}} stanice rádia", "radioStationWithCount_few": "{{count}} stanice rádia",
+5 -27
View File
@@ -6,8 +6,6 @@
"selectRangeOfItems": "Select a range of items", "selectRangeOfItems": "Select a range of items",
"clearQueue": "Clear queue", "clearQueue": "Clear queue",
"goToCurrent": "Go to current item", "goToCurrent": "Go to current item",
"collapseAllFolders": "Collapse all folders",
"expandAllFolders": "Expand all folders",
"createPlaylist": "Create $t(entity.playlist, {\"count\": 1})", "createPlaylist": "Create $t(entity.playlist, {\"count\": 1})",
"createRadioStation": "Create $t(entity.radioStation, {\"count\": 1})", "createRadioStation": "Create $t(entity.radioStation, {\"count\": 1})",
"deletePlaylist": "Delete $t(entity.playlist, {\"count\": 1})", "deletePlaylist": "Delete $t(entity.playlist, {\"count\": 1})",
@@ -58,7 +56,6 @@
"albumPeak": "Album peak", "albumPeak": "Album peak",
"areYouSure": "Are you sure?", "areYouSure": "Are you sure?",
"ascending": "Ascending", "ascending": "Ascending",
"back": "Back",
"backward": "Backward", "backward": "Backward",
"biography": "Biography", "biography": "Biography",
"bitDepth": "Bit depth", "bitDepth": "Bit depth",
@@ -174,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",
@@ -699,8 +696,7 @@
"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",
@@ -877,7 +873,6 @@
"hotkey_listPlayLast": "List play last", "hotkey_listPlayLast": "List play last",
"hotkey_listPlayNext": "List play next", "hotkey_listPlayNext": "List play next",
"hotkey_listPlayNow": "List play now", "hotkey_listPlayNow": "List play now",
"hotkey_listShowPlayingSong": "Show playing song in list",
"hotkey_navigateHome": "Navigate to home", "hotkey_navigateHome": "Navigate to home",
"hotkey_playbackNext": "Next track", "hotkey_playbackNext": "Next track",
"hotkey_playbackPause": "Pause", "hotkey_playbackPause": "Pause",
@@ -1048,25 +1043,8 @@
"sidebarConfiguration": "Sidebar configuration", "sidebarConfiguration": "Sidebar configuration",
"playerItemConfiguration_description": "Configure what items are shown, and in what order, on the fullscreen player", "playerItemConfiguration_description": "Configure what items are shown, and in what order, on the fullscreen player",
"playerItemConfiguration": "Player item configuration", "playerItemConfiguration": "Player item configuration",
"sidebarPlaylistFolders_description": "Create a folder view for playlists that include the configured separator in the name",
"sidebarPlaylistFolders": "Enable folders",
"sidebarPlaylistFolderSeparator_description": "Character (or string) that separates folder levels in a playlist name",
"sidebarPlaylistFolderSeparator": "Folder separator",
"sidebarPlaylistFolderView_description": "How folders are displayed in the sidebar",
"sidebarPlaylistFolderView": "Folder view",
"sidebarPlaylistFolderView_optionSingle": "Single folder",
"sidebarPlaylistFolderView_optionTree": "Tree view",
"sidebarPlaylistFolderView_optionNavigation": "Navigation view",
"sidebarPlaylistFolderTreeIndent_description": "Pixels each tree level is indented",
"sidebarPlaylistFolderTreeIndent": "Tree indent",
"sidebarPlaylistFolderTreeLineColor_description": "Color of the connecting tree lines (leave empty for theme default)",
"sidebarPlaylistFolderTreeLineColor": "Tree line color",
"sidebarPlaylistList_description": "Show or hide the playlist list in the sidebar", "sidebarPlaylistList_description": "Show or hide the playlist list in the sidebar",
"sidebarPlaylistList": "Sidebar playlist list", "sidebarPlaylistList": "Sidebar playlist list",
"sidebarPlaylistMode_description": "How each playlist is displayed in the sidebar list",
"sidebarPlaylistMode": "Sidebar playlist mode",
"sidebarPlaylistMode_optionCompact": "Compact",
"sidebarPlaylistMode_optionExpanded": "Expanded",
"sidebarPlaylistSorting_description": "Allows manual playlist sorting in the sidebar using drag and drop instead of the default server order", "sidebarPlaylistSorting_description": "Allows manual playlist sorting in the sidebar using drag and drop instead of the default server order",
"sidebarPlaylistSorting": "Sidebar playlist sorting", "sidebarPlaylistSorting": "Sidebar playlist sorting",
"sidebarPlaylistListFilterRegex_description": "Hide playlists in the sidebar that match this regular expression", "sidebarPlaylistListFilterRegex_description": "Hide playlists in the sidebar that match this regular expression",
+41 -63
View File
@@ -48,8 +48,7 @@
"sleepTimer_off": "Apagado", "sleepTimer_off": "Apagado",
"sleepTimer_endOfSong": "Fin de la canción actual", "sleepTimer_endOfSong": "Fin de la canción actual",
"sleepTimer": "Temporizador de apagado", "sleepTimer": "Temporizador de apagado",
"albumRadio": "Radio del álbum", "albumRadio": "Radio del álbum"
"scrobbleForceSubmit": "Forzar scrobble"
}, },
"setting": { "setting": {
"crossfadeStyle_description": "Selecciona el estilo de crossfade a usar por el reproductor de audio", "crossfadeStyle_description": "Selecciona el estilo de crossfade a usar por el reproductor de audio",
@@ -264,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",
@@ -343,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",
@@ -430,25 +429,7 @@
"playerbarWaveformStretch": "Estiramiento de la forma de onda", "playerbarWaveformStretch": "Estiramiento de la forma de onda",
"playerbarWaveformStretch_description": "Estira la forma de onda para rellenar el espacio disponible", "playerbarWaveformStretch_description": "Estira la forma de onda para rellenar el espacio disponible",
"preventSuspendOnPlayback": "Evitar la suspensión durante la reproducción", "preventSuspendOnPlayback": "Evitar la suspensión durante la reproducción",
"preventSuspendOnPlayback_description": "Evita que la aplicación se suspenda mientras se reproduce música", "preventSuspendOnPlayback_description": "Evita que la aplicación se suspenda mientras se reproduce música"
"hotkey_listShowPlayingSong": "Mostrar canción en reproducción en la lista",
"sidebarPlaylistFolders": "Habilitar carpetas",
"sidebarPlaylistFolders_description": "Crea una vista de carpeta para las listas de reproducción que incluye el separador configurado en el nombre",
"sidebarPlaylistFolderSeparator_description": "Carácter (o cadena) que separa los niveles de la carpeta en un nombre de lista de reproducción",
"sidebarPlaylistFolderSeparator": "Separador de carpeta",
"sidebarPlaylistFolderView": "Vista de carpeta",
"sidebarPlaylistFolderView_description": "Cómo se muestran las carpetas en la barra lateral",
"sidebarPlaylistFolderView_optionSingle": "Carpeta simple",
"sidebarPlaylistFolderView_optionTree": "Vista en árbol",
"sidebarPlaylistFolderView_optionNavigation": "Vista de navegación",
"sidebarPlaylistFolderTreeIndent": "Sangría del árbol",
"sidebarPlaylistFolderTreeLineColor": "Color de línea del árbol",
"sidebarPlaylistFolderTreeLineColor_description": "Color de las líneas interconectadas del árbol (dejar en blanco para usar el tema predeterminado)",
"sidebarPlaylistMode": "Modo de lista de reproducción de la barra lateral",
"sidebarPlaylistMode_optionCompact": "Compacto",
"sidebarPlaylistMode_optionExpanded": "Expandido",
"sidebarPlaylistMode_description": "Cómo se muestra cada lista de reproducción en la lista de la barra lateral",
"sidebarPlaylistFolderTreeIndent_description": "Píxeles que está sangrado cada nivel del árbol"
}, },
"action": { "action": {
"editPlaylist": "Editar $t(entity.playlist, {\"count\": 1})", "editPlaylist": "Editar $t(entity.playlist, {\"count\": 1})",
@@ -492,9 +473,7 @@
"addOrRemoveFromSelection": "Añadir o quitar de la selección", "addOrRemoveFromSelection": "Añadir o quitar de la selección",
"selectRangeOfItems": "Seleccionar un intervalo de elementos", "selectRangeOfItems": "Seleccionar un intervalo de elementos",
"selectAll": "Seleccionar todo", "selectAll": "Seleccionar todo",
"goToCurrent": "Ir al elemento actual", "goToCurrent": "Ir al elemento actual"
"collapseAllFolders": "Contraer todas las carpetas",
"expandAllFolders": "Expandir todas las carpetas"
}, },
"common": { "common": {
"backward": "Hacia atrás", "backward": "Hacia atrás",
@@ -554,7 +533,7 @@
"comingSoon": "Próximamente…", "comingSoon": "Próximamente…",
"reset": "Restablecer", "reset": "Restablecer",
"disable": "Desactivar", "disable": "Desactivar",
"sortOrder": "Orden", "sortOrder": "Ordenar",
"none": "Ninguno", "none": "Ninguno",
"menu": "Menú", "menu": "Menú",
"restartRequired": "Reinicio requerido", "restartRequired": "Reinicio requerido",
@@ -623,8 +602,7 @@
"rename": "Renombrar", "rename": "Renombrar",
"newVersionAvailable": "Una nueva versión está disponible", "newVersionAvailable": "Una nueva versión está disponible",
"numberOfResults": "{{numberOfResults}} resultados", "numberOfResults": "{{numberOfResults}} resultados",
"grouping": "Agrupar", "grouping": "Agrupar"
"back": "Atrás"
}, },
"error": { "error": {
"remotePortWarning": "Reiniciar el servidor para aplicar el nuevo puerto", "remotePortWarning": "Reiniciar el servidor para aplicar el nuevo puerto",
@@ -722,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"
@@ -783,7 +761,7 @@
"genres": "$t(entity.genre, {\"count\": 2})" "genres": "$t(entity.genre, {\"count\": 2})"
}, },
"fullscreenPlayer": { "fullscreenPlayer": {
"upNext": "A continuación", "upNext": "Siguiente",
"config": { "config": {
"dynamicBackground": "Fondo dinámico", "dynamicBackground": "Fondo dinámico",
"synchronized": "Sincronizado", "synchronized": "Sincronizado",
@@ -822,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",
@@ -833,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})"
@@ -948,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": {
@@ -1042,7 +1020,7 @@
"album": "Álbum", "album": "Álbum",
"favorite": "Favorito", "favorite": "Favorito",
"playCount": "Reproducciones", "playCount": "Reproducciones",
"albumCount": "Álbumes", "albumCount": "$t(entity.album, {\"count\": 2})",
"releaseYear": "Año", "releaseYear": "Año",
"lastPlayed": "Última reproducción", "lastPlayed": "Última reproducción",
"biography": "Biografía", "biography": "Biografía",
@@ -1051,19 +1029,19 @@
"title": "Título", "title": "Título",
"bpm": "BPM", "bpm": "BPM",
"dateAdded": "Fecha de adición", "dateAdded": "Fecha de adición",
"artist": "Artista", "artist": "$t(entity.artist, {\"count\": 1})",
"songCount": "Pistas", "songCount": "$t(entity.track, {\"count\": 2})",
"trackNumber": "Pista", "trackNumber": "Pista",
"genre": "Género", "genre": "$t(entity.genre, {\"count\": 1})",
"albumArtist": "Artista del álbum", "albumArtist": "Artista del álbum",
"path": "Ruta", "path": "Ruta",
"discNumber": "Disco", "discNumber": "Disco",
"channels": "Canales", "channels": "$t(common.channel, {\"count\": 2})",
"size": "Tamaño", "size": "$t(common.size)",
"codec": "decs", "codec": "$t(common.codec)",
"owner": "Propietario", "owner": "Propietario",
"bitDepth": "Profundidad de Bit", "bitDepth": "$t(common.bitDepth)",
"sampleRate": "Frecuencia de Muestreo" "sampleRate": "$t(common.sampleRate)"
}, },
"config": { "config": {
"label": { "label": {
@@ -1102,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)",
@@ -1145,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",
@@ -1174,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",
@@ -1195,11 +1173,11 @@
"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",
"radioStation_other": "Estaciones de Radio", "radioStation_other": "Estaciones de radio",
"radioStationWithCount_one": "{{count}} estación de radio", "radioStationWithCount_one": "{{count}} estación de radio",
"radioStationWithCount_many": "{{count}} estaciones de radio", "radioStationWithCount_many": "{{count}} estaciones de radio",
"radioStationWithCount_other": "{{count}} estaciones de radio" "radioStationWithCount_other": "{{count}} estaciones de radio"
@@ -1219,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"
} }
}, },
+18 -40
View File
@@ -48,8 +48,7 @@
"sleepTimer_timeRemaining": "{{time}} restante(s)", "sleepTimer_timeRemaining": "{{time}} restante(s)",
"sleepTimer_setCustom": "Définir le minuteur", "sleepTimer_setCustom": "Définir le minuteur",
"sleepTimer_cancel": "Annuler le minuteur", "sleepTimer_cancel": "Annuler le minuteur",
"albumRadio": "Radio d'album", "albumRadio": "Radio d'album"
"scrobbleForceSubmit": "Forcer le scrobble"
}, },
"action": { "action": {
"editPlaylist": "Éditer $t(entity.playlist, {\"count\": 1})", "editPlaylist": "Éditer $t(entity.playlist, {\"count\": 1})",
@@ -93,9 +92,7 @@
"selectRangeOfItems": "Sélectionner une plage d'entrées", "selectRangeOfItems": "Sélectionner une plage d'entrées",
"selectAll": "Tout sélectionner", "selectAll": "Tout sélectionner",
"openApplicationDirectory": "Ouvrir le répertoire de l'application", "openApplicationDirectory": "Ouvrir le répertoire de l'application",
"goToCurrent": "Aller à la piste en cours", "goToCurrent": "Aller à la piste en cours"
"collapseAllFolders": "Réduire tous les dossiers",
"expandAllFolders": "Développer tous les dossiers"
}, },
"common": { "common": {
"backward": "En arrière", "backward": "En arrière",
@@ -225,8 +222,7 @@
"filter_multiple": "Multiple", "filter_multiple": "Multiple",
"rename": "Renommer", "rename": "Renommer",
"newVersionAvailable": "Une nouvelle version est disponible", "newVersionAvailable": "Une nouvelle version est disponible",
"numberOfResults": "{{numberOfResults}} résultats", "numberOfResults": "{{numberOfResults}} résultats"
"back": "Retour"
}, },
"error": { "error": {
"remotePortWarning": "Redémarrer le serveur pour appliquer le nouveau port", "remotePortWarning": "Redémarrer le serveur pour appliquer le nouveau port",
@@ -703,7 +699,7 @@
"transcodeFormat_description": "Sélectionne le format du transcodage. laisser vide pour laisser le serveur décider", "transcodeFormat_description": "Sélectionne le format du transcodage. laisser vide pour laisser le serveur décider",
"volumeWidth": "Largeur de la barre de volume", "volumeWidth": "Largeur de la barre de volume",
"volumeWidth_description": "La largeur de la barre de volume", "volumeWidth_description": "La largeur de la barre de volume",
"customCssEnable": "Activer le CSS personnalisé", "customCssEnable": "Active le CSS personnalisé",
"customCssEnable_description": "Permet l'écriture de CSS personnalisé", "customCssEnable_description": "Permet l'écriture de CSS personnalisé",
"customCssNotice": "Attention : bien qu'il y ait un certain assainissement (blocage de URL() et de content :), l'utilisation de CSS personnalisé peut toujours présenter des risques en modifiant l'interface", "customCssNotice": "Attention : bien qu'il y ait un certain assainissement (blocage de URL() et de content :), l'utilisation de CSS personnalisé peut toujours présenter des risques en modifiant l'interface",
"customCss": "Css personnalisé", "customCss": "Css personnalisé",
@@ -899,24 +895,7 @@
"sidePlayQueueLayout_optionHorizontal": "Horizontal", "sidePlayQueueLayout_optionHorizontal": "Horizontal",
"sidePlayQueueLayout_optionVertical": "Vertical", "sidePlayQueueLayout_optionVertical": "Vertical",
"waveformLoadingDelay": "Délai de chargement de la forme d'onde", "waveformLoadingDelay": "Délai de chargement de la forme d'onde",
"waveformLoadingDelay_description": "Délai en secondes avant le chargement de l'onde. augmentez cette valeur si vous rencontrez des saccades lors de l'utilisation du lecteur web.", "waveformLoadingDelay_description": "Délai en secondes avant le chargement de l'onde. augmentez cette valeur si vous rencontrez des saccades lors de l'utilisation du lecteur web."
"sidebarPlaylistMode_optionExpanded": "Étendu",
"hotkey_listShowPlayingSong": "Affiche le titre en cours dans la liste",
"preventSuspendOnPlayback_description": "Empêcher la suspension de l'application lors de la lecture de musique",
"preventSuspendOnPlayback": "Empêcher la suspension lors de la lecture",
"sidebarPlaylistFolders_description": "Crée une vue de dossier pour les listes de lecture dont le nom inclut le séparateur configuré",
"sidebarPlaylistFolders": "Activer les dossiers",
"sidebarPlaylistFolderSeparator_description": "Caractère (ou chaîne de caractères) servant à séparer les niveaux de dossiers dans le nom d'une liste de lecture",
"sidebarPlaylistFolderSeparator": "Séparateur de dossiers",
"sidebarPlaylistFolderView_description": "Comment les dossiers sont affichés dans la barre latérale",
"sidebarPlaylistFolderView": "Affichage des dossiers",
"sidebarPlaylistFolderView_optionSingle": "Dossier unique",
"sidebarPlaylistFolderView_optionTree": "Vue arborescente",
"sidebarPlaylistFolderView_optionNavigation": "Vue de navigation",
"sidebarPlaylistFolderTreeIndent": "Indentation de l'arbre",
"sidebarPlaylistMode_description": "Comment chaque liste de lecture est affichée dans la barre latérale",
"sidebarPlaylistMode": "Mode de liste de lecture de la barre latérale",
"sidebarPlaylistMode_optionCompact": "Compacte"
}, },
"form": { "form": {
"deletePlaylist": { "deletePlaylist": {
@@ -943,13 +922,12 @@
"input_remoteUrlPlaceholder": "Optionnel : URL publique pour les fonctionnalités externes" "input_remoteUrlPlaceholder": "Optionnel : URL publique pour les fonctionnalités externes"
}, },
"addToPlaylist": { "addToPlaylist": {
"success": "$t(entity.trackWithCount, {\"count\": {{message}} }) ajouté à $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })", "success": "$t(entity.trackWithCount, {\"count\" : {{message}} }) ajouté à $t(entity.playlistWithCount, {\"count\" : {{numOfPlaylists}} })",
"title": "Ajouter à $t(entity.playlist, {\"count\": 1})", "title": "Ajouter à $t(entity.playlist, {\"count\": 1})",
"input_skipDuplicates": "Sauter les doublons", "input_skipDuplicates": "Sauter les doublons",
"input_playlists": "$t(entity.playlist, {\"count\": 2})", "input_playlists": "$t(entity.playlist, {\"count\": 2})",
"create": "Créer $t(entity.playlist, {\"count\": 1}) {{playlist}}", "create": "Créer $t(entity.playlist, {\"count\": 1}) {{playlist}}",
"searchOrCreate": "Rechercher $t(entity.playlist, {\"count\": 2}) ou tapez pour en créer une nouvelle", "searchOrCreate": "Rechercher $t(entity.playlist, {\"count\": 2}) ou tapez pour en créer une nouvelle"
"noneAdded": "Aucune piste n'a été ajoutée à $t(entity.playlist, {\"count\": 1}) {{playlist}}"
}, },
"createPlaylist": { "createPlaylist": {
"title": "Créer une $t(entity.playlist, {\"count\": 1})", "title": "Créer une $t(entity.playlist, {\"count\": 1})",
@@ -1033,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",
@@ -1060,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",
@@ -1082,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",
+23 -45
View File
@@ -1,7 +1,7 @@
{ {
"action": { "action": {
"editPlaylist": "Edytuj $t(entity.playlist, {\"count\": 1})", "editPlaylist": "Edytuj $t(entity.playlist, {\"count\": 1})",
"goToPage": "Idź do strony", "goToPage": "IDź do strony",
"clearQueue": "Wyczyść kolejkę", "clearQueue": "Wyczyść kolejkę",
"addToFavorites": "Dodaj do $t(entity.favorite, {\"count\": 2})", "addToFavorites": "Dodaj do $t(entity.favorite, {\"count\": 2})",
"removeFromPlaylist": "Usuń z $t(entity.playlist, {\"count\": 1})", "removeFromPlaylist": "Usuń z $t(entity.playlist, {\"count\": 1})",
@@ -41,9 +41,7 @@
"selectRangeOfItems": "Wybierz zakres elementów", "selectRangeOfItems": "Wybierz zakres elementów",
"selectAll": "Wybierz wszystkie", "selectAll": "Wybierz wszystkie",
"openApplicationDirectory": "Otwórz katalog aplikacji", "openApplicationDirectory": "Otwórz katalog aplikacji",
"goToCurrent": "Przejdź do aktualnego elementu", "goToCurrent": "Przejdź do aktualnego elementu"
"collapseAllFolders": "Zwiń wszystkie foldery",
"expandAllFolders": "Rozwiń wszystkie foldery"
}, },
"common": { "common": {
"increase": "Zwiększ", "increase": "Zwiększ",
@@ -172,8 +170,7 @@
"rename": "Zmień nazwę", "rename": "Zmień nazwę",
"newVersionAvailable": "Nowa wersja jest dostępna", "newVersionAvailable": "Nowa wersja jest dostępna",
"numberOfResults": "{{numberOfResults}} wyników", "numberOfResults": "{{numberOfResults}} wyników",
"grouping": "Grupowanie", "grouping": "Grupowanie"
"back": "Wstecz"
}, },
"entity": { "entity": {
"genre_one": "Gatunek", "genre_one": "Gatunek",
@@ -192,8 +189,8 @@
"folderWithCount_few": "{{count}} katalogi", "folderWithCount_few": "{{count}} katalogi",
"folderWithCount_many": "{{count}} katalogów", "folderWithCount_many": "{{count}} katalogów",
"albumArtist_one": "Wykonawca albumu", "albumArtist_one": "Wykonawca albumu",
"albumArtist_few": "Wykonawców albumów", "albumArtist_few": "wykonawcy albumu",
"albumArtist_many": "Wykonawców albumów", "albumArtist_many": "wykonawcy albumów",
"track_one": "Utwór", "track_one": "Utwór",
"track_few": "utwory", "track_few": "utwory",
"track_many": "utworów", "track_many": "utworów",
@@ -229,8 +226,8 @@
"song_few": "piosenki", "song_few": "piosenki",
"song_many": "­piosenek", "song_many": "­piosenek",
"radioStation_one": "Stacja radiowa", "radioStation_one": "Stacja radiowa",
"radioStation_few": "Stacje radiowe", "radioStation_few": "stacje radiowe",
"radioStation_many": "Stacji radiowych", "radioStation_many": "stacji radiowych",
"radioStationWithCount_one": "{{count}} stacja radiowa", "radioStationWithCount_one": "{{count}} stacja radiowa",
"radioStationWithCount_few": "{{count}} stacje radiowych", "radioStationWithCount_few": "{{count}} stacje radiowych",
"radioStationWithCount_many": "{{count}} stacji radiowych" "radioStationWithCount_many": "{{count}} stacji radiowych"
@@ -693,8 +690,7 @@
"sleepTimer_timeRemaining": "Pozostało {{time}}", "sleepTimer_timeRemaining": "Pozostało {{time}}",
"sleepTimer_setCustom": "Ustaw wyłącznik", "sleepTimer_setCustom": "Ustaw wyłącznik",
"sleepTimer_cancel": "Anuluj wyłączanie", "sleepTimer_cancel": "Anuluj wyłączanie",
"albumRadio": "Radio albumu", "albumRadio": "Radio albumu"
"scrobbleForceSubmit": "Wymuś scrobble"
}, },
"setting": { "setting": {
"crossfadeStyle_description": "Wybierz styl przenikania, który ma być używany do odtwarzania dźwięku", "crossfadeStyle_description": "Wybierz styl przenikania, który ma być używany do odtwarzania dźwięku",
@@ -891,7 +887,7 @@
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)", "playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
"playerbarOpenDrawer_description": "Pozwala przełączyć na odtwarzacz pełnoekranowy po kliknięciu paska odtwarzania", "playerbarOpenDrawer_description": "Pozwala przełączyć na odtwarzacz pełnoekranowy po kliknięciu paska odtwarzania",
"playerbarOpenDrawer": "Przełącznik pełnego ekranu na pasku odtwarzania", "playerbarOpenDrawer": "Przełącznik pełnego ekranu na pasku odtwarzania",
"imageAspectRatio": "Używaj natywnych proporcji okładki", "imageAspectRatio": "Używaj natywnych proporcji OKładki",
"volumeWidth": "Szerokość paska głośności", "volumeWidth": "Szerokość paska głośności",
"discordListening": "Pokazuj status jako słucha", "discordListening": "Pokazuj status jako słucha",
"imageAspectRatio_description": "Jeżeli włączone, okładka będzie pokazywana z użyciem jej natywnych proporcji. dla okładek które nie mają proporcji 1:1, pozostałe miejsce będzie puste", "imageAspectRatio_description": "Jeżeli włączone, okładka będzie pokazywana z użyciem jej natywnych proporcji. dla okładek które nie mają proporcji 1:1, pozostałe miejsce będzie puste",
@@ -916,7 +912,7 @@
"musicbrainz_description": "Pokazuj linki do MusicBrainz na stronach wykonawców/albumów, gdzie istnieje MusicBrainz ID", "musicbrainz_description": "Pokazuj linki do MusicBrainz na stronach wykonawców/albumów, gdzie istnieje MusicBrainz ID",
"discordPausedStatus": "Pokaż status podczas pauzy", "discordPausedStatus": "Pokaż status podczas pauzy",
"discordServeImage": "Wysyłaj obrazy dla {{discord}} z serwera", "discordServeImage": "Wysyłaj obrazy dla {{discord}} z serwera",
"discordServeImage_description": "Pokazuj okładki w statusie {{discord}} prosto z serwera, dostępne tylko dla Jellyfin i Navidrome. {{discord}} używa bota do pobierania obrazów, więc twój serwer musi być dostępny publicznie w internecie", "discordServeImage_description": "Pokazuj OKładki w statusie {{discord}} prosto z serwera, dostępne tylko dla Jellyfin i Navidrome. {{discord}} używa bota do pobierania obrazów, więc twój serwer musi być dostępny publicznie w internecie",
"analyticsDisable": "Zrezygnuj z analityki bazowanej na użytkowaniu", "analyticsDisable": "Zrezygnuj z analityki bazowanej na użytkowaniu",
"analyticsDisable_description": "Zanonymizowane dane użytkowania są wysyłane do dewelopera w celu poprawienia aplikacji", "analyticsDisable_description": "Zanonymizowane dane użytkowania są wysyłane do dewelopera w celu poprawienia aplikacji",
"artistBackground": "Obraz tła wykonawcy", "artistBackground": "Obraz tła wykonawcy",
@@ -982,7 +978,7 @@
"queryBuilderCustomFields": "Niestandardowe pola", "queryBuilderCustomFields": "Niestandardowe pola",
"queryBuilderCustomFields_description": "Dodaj niestandardowe pola do użycia w kreatorach zapytań", "queryBuilderCustomFields_description": "Dodaj niestandardowe pola do użycia w kreatorach zapytań",
"followCurrentSong_description": "Automatycznie przewija kolejkę odtwarzania do aktualnie odtwarzanej piosenki", "followCurrentSong_description": "Automatycznie przewija kolejkę odtwarzania do aktualnie odtwarzanej piosenki",
"followCurrentSong": ledź aktualną piosenkę", "followCurrentSong": LEDź aktualną piosenkę",
"playerFilters": "Filtruj piosenki z kolejki", "playerFilters": "Filtruj piosenki z kolejki",
"playerFilters_description": "Nie dodawaj piosenek do kolejki na podstawie poniższych kryteriów", "playerFilters_description": "Nie dodawaj piosenek do kolejki na podstawie poniższych kryteriów",
"playerbarSlider_description": "Krzywe nie są zalecane w przypadku wolnego lub ograniczonego połączenia internetowego", "playerbarSlider_description": "Krzywe nie są zalecane w przypadku wolnego lub ograniczonego połączenia internetowego",
@@ -1008,7 +1004,7 @@
"imageResolution_description": "Rozdzielczość dla obrazów używanych w programie. użycie wartości 0 ustawi rozdzielczość na natywną", "imageResolution_description": "Rozdzielczość dla obrazów używanych w programie. użycie wartości 0 ustawi rozdzielczość na natywną",
"imageResolution_optionTable": "Tabela", "imageResolution_optionTable": "Tabela",
"imageResolution_optionItemCard": "Karta elementu", "imageResolution_optionItemCard": "Karta elementu",
"imageResolution_optionSidebar": "Pasek boczny", "imageResolution_optionSidebar": "­pasek boczny",
"imageResolution_optionHeader": "Nagłówek", "imageResolution_optionHeader": "Nagłówek",
"imageResolution_optionFullScreenPlayer": "Odtwarzacz pełnoekranowy", "imageResolution_optionFullScreenPlayer": "Odtwarzacz pełnoekranowy",
"combinedLyricsAndVisualizer_description": "Połącz tekst i wizualizacje w tym samym panelu", "combinedLyricsAndVisualizer_description": "Połącz tekst i wizualizacje w tym samym panelu",
@@ -1075,25 +1071,7 @@
"playerbarWaveformStretch": "Rozciąganie przebiegu", "playerbarWaveformStretch": "Rozciąganie przebiegu",
"playerbarWaveformStretch_description": "Rozciąga przebieg aby wypełnić dostępną przestrzeń", "playerbarWaveformStretch_description": "Rozciąga przebieg aby wypełnić dostępną przestrzeń",
"preventSuspendOnPlayback_description": "Powstrzymuj wstrzymanie podczas odtwarzania muzyki", "preventSuspendOnPlayback_description": "Powstrzymuj wstrzymanie podczas odtwarzania muzyki",
"preventSuspendOnPlayback": "Powstrzymuje wstrzymanie przy odtwarzaniu", "preventSuspendOnPlayback": "Powstrzymuje wstrzymanie przy odtwarzaniu"
"hotkey_listShowPlayingSong": "Pokaż odtwarzaną piosenkę w liście",
"sidebarPlaylistFolders_description": "Utwórz widok folderu dla playlist który zawiera skonfigurowany separator w nazwie",
"sidebarPlaylistFolders": "Włącz foldery",
"sidebarPlaylistFolderSeparator_description": "Znak (lub ciąg) który oddziela poziomy folderów w nazwie playlisty",
"sidebarPlaylistFolderSeparator": "Separator folderów",
"sidebarPlaylistFolderView_description": "Jak foldery są wyświetlane w pasku bocznym",
"sidebarPlaylistFolderView": "Widok folderów",
"sidebarPlaylistFolderView_optionSingle": "Pojedynczy folder",
"sidebarPlaylistFolderView_optionTree": "Widok drzewa",
"sidebarPlaylistFolderView_optionNavigation": "Widok nawigacji",
"sidebarPlaylistFolderTreeIndent_description": "Piksele na każdym poziomie drzewa mają wcięcie",
"sidebarPlaylistFolderTreeIndent": "Wcięcie drzewa",
"sidebarPlaylistFolderTreeLineColor_description": "Kolor linii łączących drzewo (pozostaw pusty dla domyślnego dla motywu)",
"sidebarPlaylistFolderTreeLineColor": "Kolor linii drzewa",
"sidebarPlaylistMode_description": "Jak każda z playlist jest wyświetlana w liście w pasku bocznym",
"sidebarPlaylistMode": "Tryb playlist bocznego paska",
"sidebarPlaylistMode_optionCompact": "Kompaktowy",
"sidebarPlaylistMode_optionExpanded": "Rozszerzony"
}, },
"table": { "table": {
"config": { "config": {
@@ -1111,7 +1089,7 @@
"size": "$t(common.size)", "size": "$t(common.size)",
"itemSize": "Rozmiar elementu (px)", "itemSize": "Rozmiar elementu (px)",
"itemGap": "Odstęp między elementami (px)", "itemGap": "Odstęp między elementami (px)",
"followCurrentSong": ledź aktualną piosenkę", "followCurrentSong": LEDź aktualną piosenkę",
"advancedSettings": "Zaawansowane ustawienia", "advancedSettings": "Zaawansowane ustawienia",
"autosize": "Rozmiar automatyczny", "autosize": "Rozmiar automatyczny",
"moveUp": "Przesuń w górę", "moveUp": "Przesuń w górę",
@@ -1180,7 +1158,7 @@
"rating": "Ocena", "rating": "Ocena",
"favorite": "Ulubione", "favorite": "Ulubione",
"playCount": "Odtwarzane", "playCount": "Odtwarzane",
"albumCount": "Albumy", "albumCount": "$t(entity.album, {\"count\": 2})",
"releaseYear": "Rok", "releaseYear": "Rok",
"lastPlayed": "Ostatnio odtwarzane", "lastPlayed": "Ostatnio odtwarzane",
"biography": "Biografia", "biography": "Biografia",
@@ -1189,19 +1167,19 @@
"title": "Tytuł", "title": "Tytuł",
"bpm": "BPM", "bpm": "BPM",
"dateAdded": "Data dodania", "dateAdded": "Data dodania",
"artist": "Wykonawca", "artist": "$t(entity.artist, {\"count\": 1})",
"songCount": "Utwory", "songCount": "$t(entity.track, {\"count\": 2})",
"trackNumber": "Utwór", "trackNumber": "Utwór",
"genre": "Gatunek", "genre": "$t(entity.genre, {\"count\": 1})",
"albumArtist": "Wykonawca albumu", "albumArtist": "Wykonawca albumu",
"path": "Ścieżka", "path": "Ścieżka",
"discNumber": "Płyta", "discNumber": "Płyta",
"channels": "Kanały", "channels": "$t(common.channel, {\"count\": 2})",
"size": "Rozmiar", "size": "$t(common.size)",
"codec": "Kodek", "codec": "$t(common.codec)",
"owner": "Właściciel", "owner": "Właściciel",
"bitDepth": "Głębia bitowa", "bitDepth": "$t(common.bitDepth)",
"sampleRate": "Częstotliwość próbkowania" "sampleRate": "$t(common.sampleRate)"
} }
}, },
"queryBuilder": { "queryBuilder": {
+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",
+53 -75
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": "露骨狀態", "explicitStatus": "Explicit狀態",
"explicit": "露骨", "explicit": "Explicit",
"gridRows": "網格行", "gridRows": "網格行",
"noFilters": "未設定任何過濾器", "noFilters": "未設定任何過濾器",
"countSelected": "{{count}}個已選取", "countSelected": "{{count}}個已選取",
@@ -118,8 +118,7 @@
"filter_multiple": "複選", "filter_multiple": "複選",
"newVersionAvailable": "有新的版本可供使用", "newVersionAvailable": "有新的版本可供使用",
"numberOfResults": "{{numberOfResults}} 項結果", "numberOfResults": "{{numberOfResults}} 項結果",
"grouping": "分組", "grouping": "分組"
"back": "返回"
}, },
"error": { "error": {
"endpointNotImplementedError": "{{serverType}} 尚未實現端點 {{endpoint}}", "endpointNotImplementedError": "{{serverType}} 尚未實現端點 {{endpoint}}",
@@ -129,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)遠端伺服器時出現錯誤",
@@ -137,7 +136,7 @@
"remotePortWarning": "重啟伺服器使新連接埠生效", "remotePortWarning": "重啟伺服器使新連接埠生效",
"serverRequired": "需要伺服器", "serverRequired": "需要伺服器",
"sessionExpiredError": "工作階段已過期", "sessionExpiredError": "工作階段已過期",
"systemFontError": "嘗試取得系統字時出現錯誤", "systemFontError": "嘗試取得系統字時出現錯誤",
"serverNotSelectedError": "未選擇伺服器", "serverNotSelectedError": "未選擇伺服器",
"mpvRequired": "需要 MPV", "mpvRequired": "需要 MPV",
"playbackError": "無法播放媒體", "playbackError": "無法播放媒體",
@@ -198,7 +197,7 @@
"title": "$t(common.home)", "title": "$t(common.home)",
"mostPlayed": "最多播放", "mostPlayed": "最多播放",
"newlyAdded": "最近新增的發行", "newlyAdded": "最近新增的發行",
"recentlyReleased": "最近發", "recentlyReleased": "最近發",
"genres": "$t(entity.genre, {\"count\": 2})" "genres": "$t(entity.genre, {\"count\": 2})"
}, },
"appMenu": { "appMenu": {
@@ -222,7 +221,7 @@
"fullscreenPlayer": { "fullscreenPlayer": {
"config": { "config": {
"showLyricProvider": "顯示歌詞提供者", "showLyricProvider": "顯示歌詞提供者",
"useImageAspectRatio": "使用圖片長寬比", "useImageAspectRatio": "使用圖片縱橫比",
"dynamicBackground": "動態背景", "dynamicBackground": "動態背景",
"followCurrentLyric": "跟隨目前歌詞", "followCurrentLyric": "跟隨目前歌詞",
"lyricAlignment": "歌詞對齊", "lyricAlignment": "歌詞對齊",
@@ -323,8 +322,8 @@
"topSongsFrom": "{{title}} 的熱門歌曲", "topSongsFrom": "{{title}} 的熱門歌曲",
"viewAll": "檢視所有", "viewAll": "檢視所有",
"viewAllTracks": "檢視所有$t(entity.track, {\"count\": 2})", "viewAllTracks": "檢視所有$t(entity.track, {\"count\": 2})",
"groupingTypeAll": "所有發類型", "groupingTypeAll": "所有發類型",
"groupingTypePrimary": "主要發類型", "groupingTypePrimary": "主要發類型",
"favoriteSongs": "最愛歌曲", "favoriteSongs": "最愛歌曲",
"favoriteSongsFrom": "{{title}} 的最愛歌曲", "favoriteSongsFrom": "{{title}} 的最愛歌曲",
"topSongsCommunity": "社群", "topSongsCommunity": "社群",
@@ -418,8 +417,7 @@
"sleepTimer_timeRemaining": "剩餘 {{time}}", "sleepTimer_timeRemaining": "剩餘 {{time}}",
"sleepTimer_setCustom": "設定定時器", "sleepTimer_setCustom": "設定定時器",
"sleepTimer_cancel": "取消定時器", "sleepTimer_cancel": "取消定時器",
"albumRadio": "專輯電台", "albumRadio": "專輯電台"
"scrobbleForceSubmit": "強制紀錄"
}, },
"setting": { "setting": {
"audioPlayer_description": "選擇用於播放的音訊播放器", "audioPlayer_description": "選擇用於播放的音訊播放器",
@@ -442,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}}",
@@ -456,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": "弱(建議)",
@@ -501,7 +499,7 @@
"lyricFetchProvider_description": "選擇歌詞來源", "lyricFetchProvider_description": "選擇歌詞來源",
"minimizeToTray": "最小化到系統匣", "minimizeToTray": "最小化到系統匣",
"minimizeToTray_description": "將應用程式最小化到系統匣", "minimizeToTray_description": "將應用程式最小化到系統匣",
"minimumScrobbleSeconds": "最小紀錄時(秒)", "minimumScrobbleSeconds": "最小紀錄時(秒)",
"minimumScrobbleSeconds_description": "歌曲被記錄為已播放(Scrobble)所需的最小播放時間", "minimumScrobbleSeconds_description": "歌曲被記錄為已播放(Scrobble)所需的最小播放時間",
"mpvExecutablePath": "MPV 執行檔路徑", "mpvExecutablePath": "MPV 執行檔路徑",
"playbackStyle_optionCrossFade": "淡入淡出", "playbackStyle_optionCrossFade": "淡入淡出",
@@ -559,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)",
@@ -617,7 +615,7 @@
"lastfmApiKey_description": "{{lastfm}}的API金鑰。用於封面照", "lastfmApiKey_description": "{{lastfm}}的API金鑰。用於封面照",
"mpvExtraParameters_help": "一行一個", "mpvExtraParameters_help": "一行一個",
"musicbrainz": "顯示 MusicBrainz 連結", "musicbrainz": "顯示 MusicBrainz 連結",
"musicbrainz_description": "在擁有 MusicBrainz ID 的藝人/專輯頁面上顯示 MusicBrainz 的連結", "musicbrainz_description": "在存在 MusicBrainz ID 的藝人/專輯頁面上顯示 MusicBrainz 的連結",
"neteaseTranslation": "啟用網易翻譯", "neteaseTranslation": "啟用網易翻譯",
"neteaseTranslation_description": "啟用後,將從網易取得並顯示翻譯的歌詞(如果有)", "neteaseTranslation_description": "啟用後,將從網易取得並顯示翻譯的歌詞(如果有)",
"passwordStore": "密碼/secret儲存", "passwordStore": "密碼/secret儲存",
@@ -628,7 +626,7 @@
"startMinimized": "啟動時最小化", "startMinimized": "啟動時最小化",
"startMinimized_description": "在系統匣中啟動應用程式", "startMinimized_description": "在系統匣中啟動應用程式",
"transcode_description": "啟用轉碼到不同格式", "transcode_description": "啟用轉碼到不同格式",
"transcodeBitrate": "轉碼的位元率", "transcodeBitrate": "轉碼的位元率",
"transcodeBitrate_description": "選擇要轉碼的位元率。 0 表示讓伺服器選擇", "transcodeBitrate_description": "選擇要轉碼的位元率。 0 表示讓伺服器選擇",
"transcodeFormat": "轉碼的格式", "transcodeFormat": "轉碼的格式",
"transcodeFormat_description": "選擇要轉碼的格式。留空來讓伺服器決定", "transcodeFormat_description": "選擇要轉碼的格式。留空來讓伺服器決定",
@@ -710,7 +708,7 @@
"queryBuilderCustomFields_description": "在查詢建構器中新增自訂欄位", "queryBuilderCustomFields_description": "在查詢建構器中新增自訂欄位",
"followCurrentSong_description": "自動將播放佇列捲動至當前播放的歌曲", "followCurrentSong_description": "自動將播放佇列捲動至當前播放的歌曲",
"followCurrentSong": "跟隨當前歌曲", "followCurrentSong": "跟隨當前歌曲",
"playerbarSlider_description": "不建議在速度緩慢或計費的網路下使用波形", "playerbarSlider_description": "不建議在網路速度緩慢或計費的網路下使用波形",
"playerFilters": "從佇列中過濾歌曲", "playerFilters": "從佇列中過濾歌曲",
"playerFilters_description": "根據以下條件,排除要新增至佇列中的歌曲", "playerFilters_description": "根據以下條件,排除要新增至佇列中的歌曲",
"autoDJ": "Auto DJ", "autoDJ": "Auto DJ",
@@ -719,7 +717,7 @@
"autoDJ_itemCount_description": "在啟用Auto DJ時嘗試加入佇列的歌曲數量", "autoDJ_itemCount_description": "在啟用Auto DJ時嘗試加入佇列的歌曲數量",
"autoDJ_timing_description": "佇列中剩餘多少歌曲時啟動 Auto DJ", "autoDJ_timing_description": "佇列中剩餘多少歌曲時啟動 Auto DJ",
"autoDJ_timing": "觸發時機", "autoDJ_timing": "觸發時機",
"logLevel": "Log等級", "logLevel": "log等級",
"logLevel_description": "設定要顯示的最低日誌等級。Debug 會顯示所有日誌,Error 僅會顯示錯誤訊息", "logLevel_description": "設定要顯示的最低日誌等級。Debug 會顯示所有日誌,Error 僅會顯示錯誤訊息",
"logLevel_optionDebug": "Debug", "logLevel_optionDebug": "Debug",
"logLevel_optionError": "Error", "logLevel_optionError": "Error",
@@ -727,7 +725,7 @@
"logLevel_optionWarn": "Warn", "logLevel_optionWarn": "Warn",
"useThemeAccentColor": "使用主題強調色", "useThemeAccentColor": "使用主題強調色",
"useThemeAccentColor_description": "使用所選主題中定義的主要顏色,而非自訂的強調色", "useThemeAccentColor_description": "使用所選主題中定義的主要顏色,而非自訂的強調色",
"artistRadioCount_description": "設定為藝人電台與曲目電台擷取的歌曲數量", "artistRadioCount_description": "設定為藝人電台與曲目電台擷取的歌曲數量",
"imageResolution": "圖片解析度", "imageResolution": "圖片解析度",
"imageResolution_description": "應用程式中所使用圖片的解析度。設定為 0 時,將使用圖片的原始解析度", "imageResolution_description": "應用程式中所使用圖片的解析度。設定為 0 時,將使用圖片的原始解析度",
"imageResolution_optionTable": "表格", "imageResolution_optionTable": "表格",
@@ -762,11 +760,11 @@
"enableGridMultiSelect_description": "啟用時,允許在網格檢視中選擇多項。停用時,單擊網格項目圖片將導航到項目頁面", "enableGridMultiSelect_description": "啟用時,允許在網格檢視中選擇多項。停用時,單擊網格項目圖片將導航到項目頁面",
"sidebarPlaylistSorting_description": "允許在側邊欄中使用拖放手動對播放清單進行排序,而不是預設的伺服器排序", "sidebarPlaylistSorting_description": "允許在側邊欄中使用拖放手動對播放清單進行排序,而不是預設的伺服器排序",
"sidebarPlaylistListFilterRegex_description": "在側邊欄中隱藏與此正規表達式匹配的播放清單", "sidebarPlaylistListFilterRegex_description": "在側邊欄中隱藏與此正規表達式匹配的播放清單",
"sidebarPlaylistListFilterRegex_placeholder": "範例: ^daily mix.*", "sidebarPlaylistListFilterRegex_placeholder": "範例: ^Daily Mix.*",
"sidebarPlaylistListFilterRegex": "播放清單過濾器正規表達式", "sidebarPlaylistListFilterRegex": "播放清單過濾器正規表達式",
"blurExplicitImages": "模糊露骨圖片", "blurExplicitImages": "模糊露骨圖片",
"blurExplicitImages_description": "標記為露骨的專輯和歌曲封面將被模糊", "blurExplicitImages_description": "標記為露骨的專輯和歌曲封面將被模糊",
"releaseChannel_optionAlpha": "Alpha (每日建構版)", "releaseChannel_optionAlpha": "alpha (每日建構版)",
"analyticsEnable": "傳送基於使用情況的分析報告", "analyticsEnable": "傳送基於使用情況的分析報告",
"analyticsEnable_description": "匿名化的使用情況資料會傳送給開發者,以協助改進應用程式", "analyticsEnable_description": "匿名化的使用情況資料會傳送給開發者,以協助改進應用程式",
"automaticUpdates": "自動更新", "automaticUpdates": "自動更新",
@@ -785,40 +783,22 @@
"autosaveCount_description": "在儲存佇列之前,有多少曲目更改。1(最小)表示每次歌曲更改", "autosaveCount_description": "在儲存佇列之前,有多少曲目更改。1(最小)表示每次歌曲更改",
"spotify_description": "在藝人與專輯頁面顯示 Spotify 的連結", "spotify_description": "在藝人與專輯頁面顯示 Spotify 的連結",
"spotify": "顯示 Spotify 的連結", "spotify": "顯示 Spotify 的連結",
"nativeSpotify_description": "在 Spotify 應用程式而非瀏覽器中開啟", "nativeSpotify_description": "在 Spotify 應用程式中開啟,而非瀏覽器中開啟",
"nativeSpotify": "使用 Spotify 應用程式", "nativeSpotify": "使用 Spotify 應用程式",
"sidePlayQueueLayout": "側邊播放佇列佈局", "sidePlayQueueLayout": "側邊播放佇列佈局",
"sidePlayQueueLayout_description": "設定吸附側邊播放佇列的佈局", "sidePlayQueueLayout_description": "設定吸附側邊播放佇列的佈局",
"sidePlayQueueLayout_optionHorizontal": "水平", "sidePlayQueueLayout_optionHorizontal": "水平",
"sidePlayQueueLayout_optionVertical": "垂直", "sidePlayQueueLayout_optionVertical": "垂直",
"listenbrainz_description": "在藝/專輯頁面上顯示 ListenBrainz 的連結", "listenbrainz_description": "在藝術家/專輯頁面上顯示 ListenBrainz 的連結",
"listenbrainz": "顯示 ListenBrainz 連結", "listenbrainz": "顯示 ListenBrainz 連結",
"qobuz_description": "在藝/專輯頁面上顯示 Qobuz 的連結", "qobuz_description": "在藝術家/專輯頁面上顯示 Qobuz 的連結",
"qobuz": "顯示 Qobuz 連結", "qobuz": "顯示 Qobuz 連結",
"waveformLoadingDelay": "波形載入延遲", "waveformLoadingDelay": "波形載入延遲",
"waveformLoadingDelay_description": "載入波形前的延遲(以秒為單位)。如果您在使用網頁播放器時遇到卡頓,請增加此值。", "waveformLoadingDelay_description": "載入波形前的延遲(以秒為單位)。如果您在使用網頁播放器時遇到卡頓,請增加此值。",
"playerbarWaveformStretch": "波形拉伸", "playerbarWaveformStretch": "波形拉伸",
"playerbarWaveformStretch_description": "拉伸波形來填補可用空間", "playerbarWaveformStretch_description": "拉伸波形來填補可用空間",
"preventSuspendOnPlayback_description": "音樂播放時防止應用程式進入休眠", "preventSuspendOnPlayback_description": "音樂播放時防止應用程式進入休眠",
"preventSuspendOnPlayback": "在播放時防止應用程式暫停", "preventSuspendOnPlayback": "在播放時防止應用程式暫停"
"hotkey_listShowPlayingSong": "在清單中顯示正在播放的歌曲",
"sidebarPlaylistFolders_description": "為名稱中包含配置分隔符的播放清單建立資料夾檢視",
"sidebarPlaylistFolders": "啟用資料夾",
"sidebarPlaylistFolderSeparator_description": "在播放清單名稱中分隔資料夾級別的字元(或字串)",
"sidebarPlaylistFolderSeparator": "資料夾分隔符",
"sidebarPlaylistFolderView_description": "資料夾在側邊欄中顯示的方式",
"sidebarPlaylistFolderView": "資料夾檢視",
"sidebarPlaylistFolderView_optionSingle": "單一資料夾",
"sidebarPlaylistFolderView_optionTree": "樹狀顯示",
"sidebarPlaylistFolderView_optionNavigation": "導覽視圖",
"sidebarPlaylistFolderTreeIndent_description": "每個樹狀層級縮排的像素",
"sidebarPlaylistFolderTreeIndent": "樹狀縮排",
"sidebarPlaylistFolderTreeLineColor_description": "樹狀連接線的顏色(留空則使用佈景主題預設值)",
"sidebarPlaylistFolderTreeLineColor": "樹狀線條顏色",
"sidebarPlaylistMode_description": "各播放清單在側邊欄列表中的顯示方式",
"sidebarPlaylistMode": "側邊欄播放清單模式",
"sidebarPlaylistMode_optionCompact": "緊湊",
"sidebarPlaylistMode_optionExpanded": "展開"
}, },
"table": { "table": {
"config": { "config": {
@@ -859,7 +839,7 @@
"album": "$t(entity.album, {\"count\": 1})", "album": "$t(entity.album, {\"count\": 1})",
"albumArtist": "$t(entity.albumArtist, {\"count\": 1})", "albumArtist": "$t(entity.albumArtist, {\"count\": 1})",
"artist": "$t(entity.artist, {\"count\": 1})", "artist": "$t(entity.artist, {\"count\": 1})",
"bpm": "$t(common.bpm)", "bpm": "$t(common.BPM)",
"biography": "$t(common.biography)", "biography": "$t(common.biography)",
"bitrate": "$t(common.bitrate)", "bitrate": "$t(common.bitrate)",
"channels": "$t(common.channel, {\"count\": 2})", "channels": "$t(common.channel, {\"count\": 2})",
@@ -902,11 +882,11 @@
"column": { "column": {
"album": "專輯", "album": "專輯",
"albumArtist": "專輯藝人", "albumArtist": "專輯藝人",
"albumCount": "專輯", "albumCount": "$t(entity.album, {\"count\": 2})",
"artist": "藝人", "artist": "$t(entity.artist, {\"count\": 1})",
"biography": "簡介", "biography": "簡介",
"bitrate": "位元率", "bitrate": "位元率",
"channels": "聲道", "channels": "$t(common.channel, {\"count\": 2})",
"comment": "評論", "comment": "評論",
"dateAdded": "新增日期", "dateAdded": "新增日期",
"discNumber": "光碟", "discNumber": "光碟",
@@ -914,19 +894,19 @@
"lastPlayed": "最後播放", "lastPlayed": "最後播放",
"path": "路徑", "path": "路徑",
"playCount": "播放次數", "playCount": "播放次數",
"rating": "評", "rating": "評",
"releaseDate": "發日期", "releaseDate": "發日期",
"releaseYear": "年份", "releaseYear": "年份",
"genre": "曲風", "genre": "$t(entity.genre, {\"count\": 1})",
"bpm": "BPM", "bpm": "BPM",
"songCount": "曲目", "songCount": "$t(entity.track, {\"count\": 2})",
"title": "標題", "title": "標題",
"trackNumber": "曲目編號", "trackNumber": "曲目編號",
"size": "大小", "size": "$t(common.size)",
"codec": "編碼", "codec": "$t(common.codec)",
"owner": "擁有者", "owner": "擁有者",
"bitDepth": "位元深度", "bitDepth": "$t(common.bitDepth)",
"sampleRate": "取樣率" "sampleRate": "$t(common.sampleRate)"
} }
}, },
"action": { "action": {
@@ -971,9 +951,7 @@
"addOrRemoveFromSelection": "新增或移除選取項目", "addOrRemoveFromSelection": "新增或移除選取項目",
"selectAll": "全選", "selectAll": "全選",
"selectRangeOfItems": "批量選取", "selectRangeOfItems": "批量選取",
"goToCurrent": "前往當前項目", "goToCurrent": "前往當前項目"
"expandAllFolders": "展開所有資料夾",
"collapseAllFolders": "摺疊所有資料夾"
}, },
"entity": { "entity": {
"album_other": "專輯", "album_other": "專輯",
@@ -1007,7 +985,7 @@
"comment": "評論", "comment": "評論",
"communityRating": "社群評分", "communityRating": "社群評分",
"criticRating": "評論家評分", "criticRating": "評論家評分",
"dateAdded": "新增日期", "dateAdded": "新增日期",
"disc": "光碟", "disc": "光碟",
"duration": "時長", "duration": "時長",
"id": "ID", "id": "ID",
@@ -1029,13 +1007,13 @@
"rating": "評分", "rating": "評分",
"recentlyPlayed": "最近播放", "recentlyPlayed": "最近播放",
"recentlyUpdated": "最近更新", "recentlyUpdated": "最近更新",
"releaseDate": "發日期", "releaseDate": "發日期",
"songCount": "曲目數", "songCount": "曲目數",
"album": "$t(entity.album, {\"count\": 1})", "album": "$t(entity.album, {\"count\": 1})",
"albumArtist": "$t(entity.albumArtist, {\"count\": 1})", "albumArtist": "$t(entity.albumArtist, {\"count\": 1})",
"favorited": "已收藏", "favorited": "已收藏",
"recentlyAdded": "最近新增", "recentlyAdded": "最近新增",
"releaseYear": "發年份", "releaseYear": "發年份",
"search": "搜尋", "search": "搜尋",
"title": "標題", "title": "標題",
"toYear": "從年份", "toYear": "從年份",
@@ -1056,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": "優先使用公開網址",
@@ -1120,8 +1098,8 @@
"successMustClick": "分享建立成功,點擊此處開啟" "successMustClick": "分享建立成功,點擊此處開啟"
}, },
"privateMode": { "privateMode": {
"enabled": "已啟用私人模式,播放狀態現在將對外部整合功能隱藏", "enabled": "已啟用私人模式,播放狀態將對外部整合隱藏",
"disabled": "已停用私人模式,啟用的外部整合功能現在可查看播放狀態", "disabled": "已停用私人模式,播放狀態現對已啟用的外部整合可見",
"title": "私人模式" "title": "私人模式"
}, },
"largeFetchConfirmation": { "largeFetchConfirmation": {
@@ -1155,7 +1133,7 @@
"input_offset": "$t(setting.lyricOffset)" "input_offset": "$t(setting.lyricOffset)"
}, },
"editRadioStation": { "editRadioStation": {
"success": "電更新成功" "success": "電更新成功"
} }
}, },
"releaseType": { "releaseType": {
@@ -100,6 +100,9 @@ const EXCLUDED_SONG_TAGS = new Set<string>(['disctotal', 'tracktotal']);
const numericSortCollator = new Intl.Collator(undefined, { numeric: true }); const numericSortCollator = new Intl.Collator(undefined, { numeric: true });
const collator = new Intl.Collator(); const collator = new Intl.Collator();
// Tags that use IDs as values as opposed to the tag value
const ID_TAGS = new Set<string>(['albumversion', 'mood']);
const excludeMissing = (server?: null | ServerListItemWithCredential) => { const excludeMissing = (server?: null | ServerListItemWithCredential) => {
if (!server) { if (!server) {
return undefined; return undefined;
@@ -962,13 +965,13 @@ export const NavidromeController: InternalControllerEndpoint = {
if (!EXCLUDED_TAGS.has(tag.tagName)) { if (!EXCLUDED_TAGS.has(tag.tagName)) {
if (tagsToValues.has(tag.tagName)) { if (tagsToValues.has(tag.tagName)) {
tagsToValues.get(tag.tagName)!.push({ tagsToValues.get(tag.tagName)!.push({
id: tag.id, id: ID_TAGS.has(tag.tagName) ? tag.id : tag.tagValue,
name: tag.tagValue, name: tag.tagValue,
}); });
} else { } else {
tagsToValues.set(tag.tagName, [ tagsToValues.set(tag.tagName, [
{ {
id: tag.id, id: ID_TAGS.has(tag.tagName) ? tag.id : tag.tagValue,
name: tag.tagValue, name: tag.tagValue,
}, },
]); ]);
@@ -6,7 +6,7 @@ import styles from './drag-preview.module.css';
import { useItemImageUrl } from '/@/renderer/components/item-image/item-image'; import { useItemImageUrl } from '/@/renderer/components/item-image/item-image';
import { Icon } from '/@/shared/components/icon/icon'; import { Icon } from '/@/shared/components/icon/icon';
import { LibraryItem } from '/@/shared/types/domain-types'; import { LibraryItem } from '/@/shared/types/domain-types';
import { DragData, DragTarget } from '/@/shared/types/drag-and-drop'; import { DragData } from '/@/shared/types/drag-and-drop';
interface DragPreviewProps { interface DragPreviewProps {
data: DragData; data: DragData;
@@ -29,8 +29,7 @@ export const DragPreview = memo(({ data }: DragPreviewProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const itemCount = items.length; const itemCount = items.length;
const firstItem = items[0]; const firstItem = items[0];
const folderName = data.type === DragTarget.SIDEBAR_PLAYLIST_FOLDER ? data.id[0] : undefined; const itemName = firstItem ? getItemName(firstItem) : 'Item';
const itemName = folderName || (firstItem ? getItemName(firstItem) : 'Item');
const itemImage = useItemImageUrl({ const itemImage = useItemImageUrl({
id: (firstItem as { imageId: string })?.imageId, id: (firstItem as { imageId: string })?.imageId,
@@ -51,9 +50,6 @@ export const DragPreview = memo(({ data }: DragPreviewProps) => {
</div> </div>
) : ( ) : (
<div className={styles['icon-container']}> <div className={styles['icon-container']}>
{data.type === DragTarget.SIDEBAR_PLAYLIST_FOLDER && (
<Icon icon="folder" size="xl" />
)}
{data.itemType === LibraryItem.ALBUM && <Icon icon="album" size="xl" />} {data.itemType === LibraryItem.ALBUM && <Icon icon="album" size="xl" />}
{data.itemType === LibraryItem.SONG && ( {data.itemType === LibraryItem.SONG && (
<Icon icon="itemSong" size="xl" /> <Icon icon="itemSong" size="xl" />
@@ -1,26 +0,0 @@
import { ItemTableListColumnConfig } from '/@/renderer/components/item-list/types';
import { TableColumn } from '/@/shared/types/types';
const ROW_PLAY_CONTROL_COLUMN_IDS = [TableColumn.TRACK_NUMBER, TableColumn.ROW_INDEX] as const;
type RowPlayControlColumnId = (typeof ROW_PLAY_CONTROL_COLUMN_IDS)[number];
const isRowPlayControlColumnId = (columnId: TableColumn): columnId is RowPlayControlColumnId =>
ROW_PLAY_CONTROL_COLUMN_IDS.includes(columnId as RowPlayControlColumnId);
export const getRowPlayControlColumnId = (
columns: Array<Pick<ItemTableListColumnConfig, 'id'>>,
): null | TableColumn => {
for (const column of columns) {
if (isRowPlayControlColumnId(column.id)) {
return column.id;
}
}
return null;
};
export const isRowPlayControlColumn = (
columnId: TableColumn,
columns: Array<Pick<ItemTableListColumnConfig, 'id'>>,
): boolean => getRowPlayControlColumnId(columns) === columnId;
@@ -1,74 +0,0 @@
import { ItemListStateActions } from '/@/renderer/components/item-list/helpers/item-list-state';
import { Album, AlbumArtist, Artist, LibraryItem, Song } from '/@/shared/types/domain-types';
import { Play } from '/@/shared/types/types';
type PlayableArtistItem = AlbumArtist | Artist;
interface PlayerQueueByDataActions {
addToQueueByData: (data: Song[], type: Play, playSongId?: string) => void;
}
interface PlayerQueueByFetchActions {
addToQueueByFetch: (
serverId: string,
ids: string[],
itemType: LibraryItem,
playType: Play,
) => void;
}
export const playSongFromItemListControl = ({
index,
internalState,
item,
meta,
player,
}: {
index?: number;
internalState?: ItemListStateActions;
item: Song;
meta?: Record<string, unknown>;
player: PlayerQueueByDataActions;
}) => {
const playType = (meta?.playType as Play) || Play.NOW;
const singleSongOnly = meta?.singleSongOnly === true;
if (singleSongOnly) {
player.addToQueueByData([item], playType, item.id);
return;
}
const items = internalState?.getData() as Song[];
if (index !== undefined && items) {
player.addToQueueByData(items, playType, item.id);
}
};
export const playAlbumFromItemListControl = ({
album,
meta,
player,
}: {
album: Album;
meta?: Record<string, unknown>;
player: PlayerQueueByFetchActions;
}) => {
const playType = (meta?.playType as Play) || Play.NOW;
player.addToQueueByFetch(album._serverId, [album.id], LibraryItem.ALBUM, playType);
};
export const playArtistFromItemListControl = ({
artist,
itemType,
meta,
player,
}: {
artist: PlayableArtistItem;
itemType: LibraryItem.ALBUM_ARTIST | LibraryItem.ARTIST;
meta?: Record<string, unknown>;
player: PlayerQueueByFetchActions;
}) => {
const playType = (meta?.playType as Play) || Play.NOW;
player.addToQueueByFetch(artist._serverId, [artist.id], itemType, playType);
};
@@ -16,13 +16,11 @@ export const useListHotkeys = ({
focused, focused,
internalState, internalState,
itemType, itemType,
onShowPlayingSong,
}: { }: {
controls: ItemControls; controls: ItemControls;
focused: boolean; focused: boolean;
internalState: ItemListStateActions; internalState: ItemListStateActions;
itemType: LibraryItem; itemType: LibraryItem;
onShowPlayingSong?: () => void;
}) => { }) => {
const { bindings } = useHotkeySettings(); const { bindings } = useHotkeySettings();
const playButtonBehavior = usePlayButtonBehavior(); const playButtonBehavior = usePlayButtonBehavior();
@@ -121,11 +119,5 @@ export const useListHotkeys = ({
} }
}, },
], ],
[
bindings.listShowPlayingSong.hotkey,
() => {
onShowPlayingSong?.();
},
],
]); ]);
}; };
@@ -1,45 +1,3 @@
.full-size-content {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
overflow: visible;
-webkit-line-clamp: unset;
line-clamp: unset;
}
.expansion-cell {
overflow: visible;
}
.expansion-inner {
position: relative;
width: 100%;
height: 100%;
overflow: visible;
}
.play-target {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
}
.expand { .expand {
position: absolute; position: absolute;
top: 50%;
left: 50%;
z-index: 4;
transform: translate(-50%, -50%);
}
.index-content {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
} }
@@ -2,38 +2,30 @@ import clsx from 'clsx';
import styles from './row-index-column.module.css'; import styles from './row-index-column.module.css';
import { isRowPlayControlColumn } from '/@/renderer/components/item-list/helpers/get-row-play-control-column';
import { RowPlayControlCell } from '/@/renderer/components/item-list/item-table-list/columns/row-play-control-cell';
import { useRowPlayControl } from '/@/renderer/components/item-list/item-table-list/columns/use-row-play-control';
import { import {
ItemTableListInnerColumn, ItemTableListInnerColumn,
TableColumnContainer, TableColumnContainer,
TableColumnTextContainer, TableColumnTextContainer,
} from '/@/renderer/components/item-list/item-table-list/item-table-list-column'; } from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
import { useIsActiveRow } from '/@/renderer/components/item-list/item-table-list/item-table-list-context';
import { ItemListItem } from '/@/renderer/components/item-list/types'; import { ItemListItem } from '/@/renderer/components/item-list/types';
import { usePlayerStatus } from '/@/renderer/store';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Flex } from '/@/shared/components/flex/flex'; import { Flex } from '/@/shared/components/flex/flex';
import { Icon } from '/@/shared/components/icon/icon'; import { Icon } from '/@/shared/components/icon/icon';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
import { LibraryItem } from '/@/shared/types/domain-types'; import { LibraryItem, QueueSong } from '/@/shared/types/domain-types';
import { TableColumn } from '/@/shared/types/types'; import { PlayerStatus } from '/@/shared/types/types';
const RowIndexColumnBase = (props: ItemTableListInnerColumn) => { const RowIndexColumnBase = (props: ItemTableListInnerColumn) => {
const { itemType } = props; const { itemType } = props;
if (!isRowPlayControlColumn(TableColumn.ROW_INDEX, props.columns)) {
return <DefaultRowIndexColumn {...props} />;
}
switch (itemType) { switch (itemType) {
case LibraryItem.ALBUM:
case LibraryItem.ALBUM_ARTIST:
case LibraryItem.ARTIST:
case LibraryItem.FOLDER: case LibraryItem.FOLDER:
case LibraryItem.PLAYLIST_SONG: case LibraryItem.PLAYLIST_SONG:
case LibraryItem.QUEUE_SONG: case LibraryItem.QUEUE_SONG:
case LibraryItem.SONG: case LibraryItem.SONG:
return <PlayableRowIndexColumn {...props} />; return <QueueSongRowIndexColumn {...props} />;
default: default:
return <DefaultRowIndexColumn {...props} />; return <DefaultRowIndexColumn {...props} />;
} }
@@ -96,8 +88,12 @@ const DefaultRowIndexColumn = (props: ItemTableListInnerColumn) => {
return <TableColumnTextContainer {...props}>{adjustedRowIndex}</TableColumnTextContainer>; return <TableColumnTextContainer {...props}>{adjustedRowIndex}</TableColumnTextContainer>;
}; };
const PlayableRowIndexColumn = (props: ItemTableListInnerColumn) => { const QueueSongRowIndexColumn = (props: ItemTableListInnerColumn) => {
const { handlePlay, isActive, isPlaying, showPlayControls } = useRowPlayControl(props); const status = usePlayerStatus();
const song = (props.getRowItem?.(props.rowIndex) ?? props.data[props.rowIndex]) as QueueSong;
const isActive = useIsActiveRow(song?.id, song?._uniqueId);
const isActiveAndPlaying = isActive && status === PlayerStatus.PLAYING;
let adjustedRowIndex = let adjustedRowIndex =
props.getAdjustedRowIndex?.(props.rowIndex) ?? props.getAdjustedRowIndex?.(props.rowIndex) ??
@@ -108,20 +104,38 @@ const PlayableRowIndexColumn = (props: ItemTableListInnerColumn) => {
adjustedRowIndex = props.startRowIndex + adjustedRowIndex; adjustedRowIndex = props.startRowIndex + adjustedRowIndex;
} }
const indexContent = isActive ? (
<Flex className={styles.indexContent}>
<Icon fill="primary" icon={isPlaying ? 'mediaPlay' : 'mediaPause'} />
</Flex>
) : (
adjustedRowIndex
);
return ( return (
<RowPlayControlCell <InnerQueueSongRowIndexColumn
{...props} {...props}
indexContent={indexContent} adjustedRowIndex={adjustedRowIndex}
onPlay={handlePlay} isActive={isActive}
showPlayControls={showPlayControls} isPlaying={isActiveAndPlaying}
/> />
); );
}; };
const InnerQueueSongRowIndexColumn = (
props: ItemTableListInnerColumn & {
adjustedRowIndex: number;
isActive: boolean;
isPlaying: boolean;
},
) => {
return (
<TableColumnTextContainer {...props}>
{props.isActive ? (
props.isPlaying ? (
<Flex>
<Icon fill="primary" icon="mediaPlay" />
</Flex>
) : (
<Flex>
<Icon fill="primary" icon="mediaPause" />
</Flex>
)
) : (
props.adjustedRowIndex
)}
</TableColumnTextContainer>
);
};
@@ -1,125 +0,0 @@
import clsx from 'clsx';
import { ReactNode, useCallback } from 'react';
import styles from './row-index-column.module.css';
import {
ItemTableListInnerColumn,
TableColumnContainer,
TableColumnTextContainer,
} from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
import { ItemListItem } from '/@/renderer/components/item-list/types';
import { ItemRowPlayControls } from '/@/renderer/features/shared/components/item-row-play-controls';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Flex } from '/@/shared/components/flex/flex';
import { HoverCard } from '/@/shared/components/hover-card/hover-card';
import { Text } from '/@/shared/components/text/text';
import { Play } from '/@/shared/types/types';
export const RowPlayControlCell = (
props: ItemTableListInnerColumn & {
indexContent: ReactNode;
onPlay: (playType: Play) => void;
showPlayControls: boolean;
},
) => {
const {
controls,
data,
enableExpansion,
getRowItem,
indexContent,
internalState,
itemType,
onPlay,
rowIndex,
showPlayControls,
} = props;
const handleExpand = useCallback(
(e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
const item = (getRowItem?.(rowIndex) ?? data[rowIndex]) as ItemListItem;
const rowId = internalState.extractRowId(item);
const index = rowId ? internalState.findItemIndex(rowId) : -1;
controls.onExpand?.({
event: e,
index,
internalState,
item,
itemType,
});
},
[controls, data, getRowItem, internalState, itemType, rowIndex],
);
const getIndexDisplay = (useMutedText: boolean) => {
const hideOnHoverClass = enableExpansion ? 'hide-on-hover' : undefined;
if (typeof indexContent === 'number') {
return useMutedText ? (
<Text className={hideOnHoverClass} isMuted isNoSelect>
{indexContent}
</Text>
) : (
indexContent
);
}
return <span className={hideOnHoverClass}>{indexContent}</span>;
};
const expansionTarget = (
<div className={styles.playTarget}>
{getIndexDisplay(true)}
<ActionIcon
className={clsx(styles.expand, 'hover-only')}
icon="arrowDownS"
iconProps={{ color: 'muted', size: 'md' }}
onClick={handleExpand}
size="xs"
variant="subtle"
/>
</div>
);
if (enableExpansion) {
return (
<TableColumnContainer {...props} className={styles.expansionCell}>
<div className={styles.expansionInner}>
{showPlayControls ? (
<HoverCard openDelay={300} position="top" withArrow>
<HoverCard.Target>{expansionTarget}</HoverCard.Target>
<HoverCard.Dropdown onClick={(e) => e.stopPropagation()}>
<ItemRowPlayControls onPlay={onPlay} />
</HoverCard.Dropdown>
</HoverCard>
) : (
expansionTarget
)}
</div>
</TableColumnContainer>
);
}
if (!showPlayControls) {
return (
<TableColumnTextContainer {...props}>{getIndexDisplay(false)}</TableColumnTextContainer>
);
}
return (
<TableColumnTextContainer {...props} className={styles.fullSizeContent}>
<HoverCard openDelay={300} position="top" withArrow>
<HoverCard.Target>
<Flex className={styles.indexContent} justify="center" w="100%">
{getIndexDisplay(false)}
</Flex>
</HoverCard.Target>
<HoverCard.Dropdown onClick={(e) => e.stopPropagation()}>
<ItemRowPlayControls onPlay={onPlay} />
</HoverCard.Dropdown>
</HoverCard>
</TableColumnTextContainer>
);
};
@@ -1,51 +0,0 @@
import styles from './row-index-column.module.css';
import { isRowPlayControlColumn } from '/@/renderer/components/item-list/helpers/get-row-play-control-column';
import { NumericColumn } from '/@/renderer/components/item-list/item-table-list/columns/numeric-column';
import { RowPlayControlCell } from '/@/renderer/components/item-list/item-table-list/columns/row-play-control-cell';
import {
supportsTrackNumberRowPlayControls,
useRowPlayControl,
} from '/@/renderer/components/item-list/item-table-list/columns/use-row-play-control';
import { ItemTableListInnerColumn } from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
import { Flex } from '/@/shared/components/flex/flex';
import { Icon } from '/@/shared/components/icon/icon';
import { TableColumn } from '/@/shared/types/types';
export const TrackNumberColumn = (props: ItemTableListInnerColumn) => {
if (
isRowPlayControlColumn(TableColumn.TRACK_NUMBER, props.columns) &&
supportsTrackNumberRowPlayControls(props.itemType)
) {
return <PlayableTrackNumberColumn {...props} />;
}
return <NumericColumn {...props} />;
};
const PlayableTrackNumberColumn = (props: ItemTableListInnerColumn) => {
const { handlePlay, isActive, isPlaying, showPlayControls } = useRowPlayControl(props);
const rowItem = props.getRowItem?.(props.rowIndex) ?? (props.data as unknown[])[props.rowIndex];
const trackNumber = (rowItem as undefined | { trackNumber?: number })?.trackNumber;
if (typeof trackNumber !== 'number') {
return <NumericColumn {...props} />;
}
const indexContent = isActive ? (
<Flex className={styles.indexContent}>
<Icon fill="primary" icon={isPlaying ? 'mediaPlay' : 'mediaPause'} />
</Flex>
) : (
trackNumber
);
return (
<RowPlayControlCell
{...props}
indexContent={indexContent}
onPlay={handlePlay}
showPlayControls={showPlayControls}
/>
);
};
@@ -1,140 +0,0 @@
import { useCallback } from 'react';
import {
playAlbumFromItemListControl,
playArtistFromItemListControl,
playSongFromItemListControl,
} from '/@/renderer/components/item-list/helpers/play-row-from-list';
import { ItemTableListInnerColumn } from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
import { useIsActiveRow } from '/@/renderer/components/item-list/item-table-list/item-table-list-context';
import { usePlayer } from '/@/renderer/features/player/context/player-context';
import { usePlayerSong, usePlayerStatus } from '/@/renderer/store';
import {
Album,
AlbumArtist,
Artist,
LibraryItem,
QueueSong,
Song,
} from '/@/shared/types/domain-types';
import { Play, PlayerStatus } from '/@/shared/types/types';
export const supportsRowPlayControls = (itemType: LibraryItem) =>
itemType === LibraryItem.ALBUM ||
itemType === LibraryItem.ALBUM_ARTIST ||
itemType === LibraryItem.ARTIST ||
itemType === LibraryItem.PLAYLIST_SONG ||
itemType === LibraryItem.SONG;
export const supportsTrackNumberRowPlayControls = (itemType: LibraryItem) =>
itemType === LibraryItem.PLAYLIST_SONG ||
itemType === LibraryItem.QUEUE_SONG ||
itemType === LibraryItem.SONG;
export const hasPlayableRowItem = (
itemType: LibraryItem,
items: { album: Album; artist: AlbumArtist | Artist; song: QueueSong },
) => {
switch (itemType) {
case LibraryItem.ALBUM:
return !!items.album?.id;
case LibraryItem.ALBUM_ARTIST:
case LibraryItem.ARTIST:
return !!items.artist?.id;
default:
return !!items.song;
}
};
export const useRowPlayControl = (props: ItemTableListInnerColumn) => {
const status = usePlayerStatus();
const currentSong = usePlayerSong();
const player = usePlayer();
const rowItem = props.getRowItem?.(props.rowIndex) ?? props.data[props.rowIndex];
const song = rowItem as QueueSong;
const album = rowItem as Album;
const artist = rowItem as AlbumArtist | Artist;
const isActiveFromRow = useIsActiveRow(song?.id, song?._uniqueId);
const isActive = (() => {
switch (props.itemType) {
case LibraryItem.ALBUM:
return !!album?.id && currentSong?.albumId === album.id;
case LibraryItem.ALBUM_ARTIST:
return (
!!artist?.id &&
!!currentSong?.albumArtists?.some(
(relatedArtist) => relatedArtist.id === artist.id,
)
);
case LibraryItem.ARTIST:
return (
!!artist?.id &&
!!currentSong?.artists?.some((relatedArtist) => relatedArtist.id === artist.id)
);
default:
return isActiveFromRow;
}
})();
const isPlaying = isActive && status === PlayerStatus.PLAYING;
const showPlayControls =
supportsRowPlayControls(props.itemType) &&
hasPlayableRowItem(props.itemType, { album, artist, song });
const handlePlay = useCallback(
(playType: Play) => {
if (props.itemType === LibraryItem.ALBUM) {
if (!album?.id) {
return;
}
playAlbumFromItemListControl({
album,
meta: { playType },
player,
});
return;
}
if (
props.itemType === LibraryItem.ALBUM_ARTIST ||
props.itemType === LibraryItem.ARTIST
) {
if (!artist?.id) {
return;
}
playArtistFromItemListControl({
artist,
itemType: props.itemType,
meta: { playType },
player,
});
return;
}
if (!song) {
return;
}
playSongFromItemListControl({
item: song as Song,
meta: { playType, singleSongOnly: true },
player,
});
},
[album, artist, player, props.itemType, song],
);
return {
album,
artist,
handlePlay,
isActive,
isPlaying,
showPlayControls,
song,
};
};
@@ -44,6 +44,7 @@ import { FavoriteColumn } from '/@/renderer/components/item-list/item-table-list
import { GenreBadgeColumn } from '/@/renderer/components/item-list/item-table-list/columns/genre-badge-column'; import { GenreBadgeColumn } from '/@/renderer/components/item-list/item-table-list/columns/genre-badge-column';
import { GenreColumn } from '/@/renderer/components/item-list/item-table-list/columns/genre-column'; import { GenreColumn } from '/@/renderer/components/item-list/item-table-list/columns/genre-column';
import { ImageColumn } from '/@/renderer/components/item-list/item-table-list/columns/image-column'; import { ImageColumn } from '/@/renderer/components/item-list/item-table-list/columns/image-column';
import { NumericColumn } from '/@/renderer/components/item-list/item-table-list/columns/numeric-column';
import { PathColumn } from '/@/renderer/components/item-list/item-table-list/columns/path-column'; import { PathColumn } from '/@/renderer/components/item-list/item-table-list/columns/path-column';
import { PlaylistReorderColumn } from '/@/renderer/components/item-list/item-table-list/columns/playlist-reorder-column'; import { PlaylistReorderColumn } from '/@/renderer/components/item-list/item-table-list/columns/playlist-reorder-column';
import { RatingColumn } from '/@/renderer/components/item-list/item-table-list/columns/rating-column'; import { RatingColumn } from '/@/renderer/components/item-list/item-table-list/columns/rating-column';
@@ -53,7 +54,6 @@ import { TextColumn } from '/@/renderer/components/item-list/item-table-list/col
import { TitleArtistColumn } from '/@/renderer/components/item-list/item-table-list/columns/title-artist-column'; import { TitleArtistColumn } from '/@/renderer/components/item-list/item-table-list/columns/title-artist-column';
import { TitleColumn } from '/@/renderer/components/item-list/item-table-list/columns/title-column'; import { TitleColumn } from '/@/renderer/components/item-list/item-table-list/columns/title-column';
import { TitleCombinedColumn } from '/@/renderer/components/item-list/item-table-list/columns/title-combined-column'; import { TitleCombinedColumn } from '/@/renderer/components/item-list/item-table-list/columns/title-combined-column';
import { TrackNumberColumn } from '/@/renderer/components/item-list/item-table-list/columns/track-number-column';
import { YearColumn } from '/@/renderer/components/item-list/item-table-list/columns/year-column'; import { YearColumn } from '/@/renderer/components/item-list/item-table-list/columns/year-column';
import { useItemDragDropState } from '/@/renderer/components/item-list/item-table-list/hooks/use-item-drag-drop-state'; import { useItemDragDropState } from '/@/renderer/components/item-list/item-table-list/hooks/use-item-drag-drop-state';
import { TableItemProps } from '/@/renderer/components/item-list/item-table-list/item-table-list'; import { TableItemProps } from '/@/renderer/components/item-list/item-table-list/item-table-list';
@@ -240,9 +240,7 @@ const ItemTableListColumnBase = (props: ItemTableListColumn) => {
case TableColumn.DISC_NUMBER: case TableColumn.DISC_NUMBER:
case TableColumn.SAMPLE_RATE: case TableColumn.SAMPLE_RATE:
case TableColumn.TRACK_NUMBER: case TableColumn.TRACK_NUMBER:
return ( return <NumericColumn {...props} {...dragProps} controls={controls} type={type} />;
<TrackNumberColumn {...props} {...dragProps} controls={controls} type={type} />
);
case TableColumn.COMPOSER: case TableColumn.COMPOSER:
return <ComposerColumn {...props} {...dragProps} controls={controls} type={type} />; return <ComposerColumn {...props} {...dragProps} controls={controls} type={type} />;
@@ -66,7 +66,6 @@ import {
ItemTableListColumnConfig, ItemTableListColumnConfig,
} from '/@/renderer/components/item-list/types'; } from '/@/renderer/components/item-list/types';
import { PlayerContext, usePlayer } from '/@/renderer/features/player/context/player-context'; import { PlayerContext, usePlayer } from '/@/renderer/features/player/context/player-context';
import { usePlayerStore } from '/@/renderer/store';
import { animationProps } from '/@/shared/components/animations/animation-props'; import { animationProps } from '/@/shared/components/animations/animation-props';
import { useFocusWithin } from '/@/shared/hooks/use-focus-within'; import { useFocusWithin } from '/@/shared/hooks/use-focus-within';
import { useMergedRef } from '/@/shared/hooks/use-merged-ref'; import { useMergedRef } from '/@/shared/hooks/use-merged-ref';
@@ -1597,22 +1596,11 @@ const BaseItemTableList = ({
], ],
); );
const onShowPlayingSong = useCallback(() => {
const targetId = usePlayerStore.getState().getCurrentSong()?.id;
if (!targetId) return;
const index =
getItemIndex?.(targetId) ??
data.findIndex((item) => (item as null | { id?: string })?.id === targetId);
if (index === undefined || index < 0) return;
handleRef.current?.scrollToIndex(index, { align: 'center', behavior: 'auto' });
}, [data, getItemIndex]);
useListHotkeys({ useListHotkeys({
controls, controls,
focused, focused,
internalState, internalState,
itemType, itemType,
onShowPlayingSong,
}); });
const tableConfigValue = useMemo<ItemTableListConfig>( const tableConfigValue = useMemo<ItemTableListConfig>(
@@ -12,7 +12,6 @@ import styles from './album-detail-content.module.css';
import { useGridCarouselContainerQuery } from '/@/renderer/components/grid-carousel/grid-carousel-v2'; import { useGridCarouselContainerQuery } from '/@/renderer/components/grid-carousel/grid-carousel-v2';
import { useItemListStateSubscription } from '/@/renderer/components/item-list/helpers/item-list-state'; import { useItemListStateSubscription } from '/@/renderer/components/item-list/helpers/item-list-state';
import { playSongFromItemListControl } from '/@/renderer/components/item-list/helpers/play-row-from-list';
import { useItemListColumnReorder } from '/@/renderer/components/item-list/helpers/use-item-list-column-reorder'; 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 { useItemListColumnResize } from '/@/renderer/components/item-list/helpers/use-item-list-column-resize';
import { SONG_TABLE_COLUMNS } from '/@/renderer/components/item-list/item-table-list/default-columns'; import { SONG_TABLE_COLUMNS } from '/@/renderer/components/item-list/item-table-list/default-columns';
@@ -61,7 +60,7 @@ import {
SongListSort, SongListSort,
SortOrder, SortOrder,
} from '/@/shared/types/domain-types'; } from '/@/shared/types/domain-types';
import { ItemListKey, ListDisplayType } from '/@/shared/types/types'; import { ItemListKey, ListDisplayType, Play } from '/@/shared/types/types';
const MetadataPillGroup = ({ const MetadataPillGroup = ({
items, items,
@@ -831,13 +830,13 @@ const AlbumDetailSongsTable = ({ songs }: AlbumDetailSongsTableProps) => {
return; return;
} }
playSongFromItemListControl({ const playType = (meta?.playType as Play) || Play.NOW;
index,
internalState, const items = internalState?.getData() as Song[];
item: item as Song,
meta, if (index !== undefined) {
player, player.addToQueueByData(items, playType, item.id);
}); }
}, },
}; };
}, [player]); }, [player]);
@@ -14,7 +14,6 @@ import styles from './album-artist-detail-content.module.css';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import { DataRow, MemoizedItemCard } from '/@/renderer/components/item-card/item-card'; import { DataRow, MemoizedItemCard } from '/@/renderer/components/item-card/item-card';
import { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls'; import { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls';
import { playSongFromItemListControl } from '/@/renderer/components/item-list/helpers/play-row-from-list';
import { useGridRows } from '/@/renderer/components/item-list/helpers/use-grid-rows'; import { useGridRows } from '/@/renderer/components/item-list/helpers/use-grid-rows';
import { useItemListColumnReorder } from '/@/renderer/components/item-list/helpers/use-item-list-column-reorder'; 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 { useItemListColumnResize } from '/@/renderer/components/item-list/helpers/use-item-list-column-resize';
@@ -366,13 +365,12 @@ const AlbumArtistMetadataTopSongsContent = ({
return; return;
} }
playSongFromItemListControl({ const playType = (meta?.playType as Play) || Play.NOW;
index, const items = internalState?.getData() as Song[];
internalState,
item: item as Song, if (index !== undefined) {
meta, player.addToQueueByData(items, playType, item.id);
player, }
});
}, },
}; };
}, [player]); }, [player]);
@@ -659,13 +657,12 @@ const AlbumArtistMetadataFavoriteSongs = ({
return; return;
} }
playSongFromItemListControl({ const playType = (meta?.playType as Play) || Play.NOW;
index, const items = internalState?.getData() as Song[];
internalState,
item: item as Song, if (index !== undefined) {
meta, player.addToQueueByData(items, playType, item.id);
player, }
});
}, },
}; };
}, [player]); }, [player]);
@@ -2,7 +2,6 @@ import { useSuspenseQueries } from '@tanstack/react-query';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
import { playSongFromItemListControl } from '/@/renderer/components/item-list/helpers/play-row-from-list';
import { useItemListColumnReorder } from '/@/renderer/components/item-list/helpers/use-item-list-column-reorder'; 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 { useItemListColumnResize } from '/@/renderer/components/item-list/helpers/use-item-list-column-resize';
import { ItemTableList } from '/@/renderer/components/item-list/item-table-list/item-table-list'; import { ItemTableList } from '/@/renderer/components/item-list/item-table-list/item-table-list';
@@ -25,7 +24,7 @@ import { useCurrentServer } from '/@/renderer/store/auth.store';
import { useSettingsStore } from '/@/renderer/store/settings.store'; import { useSettingsStore } from '/@/renderer/store/settings.store';
import { sortSongList } from '/@/shared/api/utils'; import { sortSongList } from '/@/shared/api/utils';
import { LibraryItem, Song } from '/@/shared/types/domain-types'; import { LibraryItem, Song } from '/@/shared/types/domain-types';
import { ItemListKey } from '/@/shared/types/types'; import { ItemListKey, Play } from '/@/shared/types/types';
const AlbumArtistDetailFavoriteSongsListRoute = () => { const AlbumArtistDetailFavoriteSongsListRoute = () => {
const { albumArtistId, artistId } = useParams() as { const { albumArtistId, artistId } = useParams() as {
@@ -97,13 +96,12 @@ const AlbumArtistDetailFavoriteSongsListRoute = () => {
return; return;
} }
playSongFromItemListControl({ const playType = (meta?.playType as Play) || Play.NOW;
index, const items = internalState?.getData() as Song[];
internalState,
item: item as Song, if (index !== undefined) {
meta, player.addToQueueByData(items, playType, item.id);
player, }
});
}, },
}; };
}, [player]); }, [player]);
@@ -2,7 +2,6 @@ import { useSuspenseQuery } from '@tanstack/react-query';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
import { playSongFromItemListControl } from '/@/renderer/components/item-list/helpers/play-row-from-list';
import { useItemListColumnReorder } from '/@/renderer/components/item-list/helpers/use-item-list-column-reorder'; 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 { useItemListColumnResize } from '/@/renderer/components/item-list/helpers/use-item-list-column-resize';
import { ItemTableList } from '/@/renderer/components/item-list/item-table-list/item-table-list'; import { ItemTableList } from '/@/renderer/components/item-list/item-table-list/item-table-list';
@@ -19,7 +18,7 @@ import { useCurrentServer } from '/@/renderer/store/auth.store';
import { useSettingsStore } from '/@/renderer/store/settings.store'; import { useSettingsStore } from '/@/renderer/store/settings.store';
import { useLocalStorage } from '/@/shared/hooks/use-local-storage'; import { useLocalStorage } from '/@/shared/hooks/use-local-storage';
import { LibraryItem, Song } from '/@/shared/types/domain-types'; import { LibraryItem, Song } from '/@/shared/types/domain-types';
import { ItemListKey } from '/@/shared/types/types'; import { ItemListKey, Play } from '/@/shared/types/types';
const AlbumArtistDetailTopSongsListRoute = () => { const AlbumArtistDetailTopSongsListRoute = () => {
const { albumArtistId, artistId } = useParams() as { const { albumArtistId, artistId } = useParams() as {
@@ -79,13 +78,12 @@ const AlbumArtistDetailTopSongsListRoute = () => {
return; return;
} }
playSongFromItemListControl({ const playType = (meta?.playType as Play) || Play.NOW;
index, const items = internalState?.getData() as Song[];
internalState,
item: item as Song, if (index !== undefined) {
meta, player.addToQueueByData(items, playType, item.id);
player, }
});
}, },
}; };
}, [player]); }, [player]);
@@ -59,24 +59,9 @@
.slider-value-wrapper { .slider-value-wrapper {
display: flex; display: flex;
flex: 1; flex: 1;
align-items: center; align-self: 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,7 +4,6 @@ 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,
@@ -42,8 +41,17 @@ export const PlayerbarSlider = () => {
return ( return (
<> <>
<div className={styles.sliderContainer}> <div className={styles.sliderContainer}>
<div className={styles.sliderValueWrapperElapsed}> <div className={styles.sliderValueWrapper}>
<ScrobbleStatus formattedTime={formattedTime} /> <Text
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 ? (
@@ -1,124 +0,0 @@
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,10 +1,9 @@
import React, { useCallback, useEffect, useRef } from 'react'; import React, { useCallback, useEffect, useRef, useState } 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,
@@ -17,64 +16,34 @@ 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();
};
/* /*
Submission (Last.fm / etc.) eligibility uses accumulated listen time: Scrobble Conditions (match any):
- If listened time meets the required percentage of track duration - If the song has been played for the required percentage
- If listened time meets the required duration (seconds) - If the song has been played for the required duration
Listen time advances only while PLAYING, from consecutive timestamp deltas. Scrobble Events:
Seeks and other timeline jumps re-baseline the next sample without counting - On song timestamp update:
the jump as listen time; accumulated listen time is kept across seeks. - If the song has been played for the required percentage
- If the song has been played for the required duration
Listen time and submission state reset when the playhead returns to the start - When the song changes (or is completed):
of the track (position before SCROBBLE_TRACK_BEGIN_SEC), e.g. seek-to-start or - Current song: Sends the 'playing' scrobble event
restart-from-near-zero. Song change and repeat still reset for a new play-through. - Resets the 'isCurrentSongScrobbled' state to false
Jellyfin progress APIs still use playback position (ticks), not listen time: - When the song is restarted:
- Periodic timeupdate while playing - Sends the 'submission' scrobble event if conditions are met AND the 'isCurrentSongScrobbled' state is false
- timeupdate on seek - Resets the 'isCurrentSongScrobbled' state to false
- pause / unpause
Other events: - When the song is seeked:
- When the song changes: sends 'start' when the new track is playing; - Sends the 'timeupdate' scrobble event (Jellyfin only)
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.
- When the song is seeked: Jellyfin sends timeupdate (throttled). Seeking from Progress Events:
at/after the intro into the start of the track clears listen accumulator and - When the song is playing (Jellyfin only):
submission flag; other seeks keep accumulated listen time. - Sends the 'progress' scrobble event on an interval
*/ */
// 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;
@@ -87,10 +56,10 @@ const checkScrobbleConditions = (args: {
? (songCompletedDurationMs / songDurationMs) * 100 ? (songCompletedDurationMs / songDurationMs) * 100
: 0; : 0;
const shouldScrobbleBasedOnPercentage = percentageOfSongCompleted >= scrobbleAtPercentage; const shouldScrobbleBasedOnPercetange = percentageOfSongCompleted >= scrobbleAtPercentage;
const shouldScrobbleBasedOnDuration = songCompletedDurationMs >= scrobbleAtDurationMs; const shouldScrobbleBasedOnDuration = songCompletedDurationMs >= scrobbleAtDurationMs;
return shouldScrobbleBasedOnPercentage || shouldScrobbleBasedOnDuration; return shouldScrobbleBasedOnPercetange || shouldScrobbleBasedOnDuration;
}; };
export const useScrobble = () => { export const useScrobble = () => {
@@ -108,12 +77,7 @@ export const useScrobble = () => {
}); });
const imageUrlRef = useRef<null | string | undefined>(imageUrl); const imageUrlRef = useRef<null | string | undefined>(imageUrl);
const isCurrentSongScrobbledRef = useRef(false); const [isCurrentSongScrobbled, setIsCurrentSongScrobbled] = useState(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);
@@ -125,101 +89,30 @@ 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;
if (!currentSong?.id) { // Detect song restart: when timestamp resets to near 0 and was playing for at least 10 seconds
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 < SCROBBLE_TRACK_BEGIN_SEC && currentTime < 5 && // Reset to near 0
previousTime >= SCROBBLE_RESTART_PREVIOUS_MIN_SEC previousTime >= 10 // Was playing for at least 10 seconds
) { ) {
isCurrentSongScrobbledRef.current = false; setIsCurrentSongScrobbled(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;
@@ -251,12 +144,12 @@ export const useScrobble = () => {
} }
} }
// Check if we should submit scrobble based on listened time // Check if we should submit scrobble based on conditions
if (!isCurrentSongScrobbledRef.current) { if (!isCurrentSongScrobbled) {
const shouldSubmitScrobble = checkScrobbleConditions({ const shouldSubmitScrobble = checkScrobbleConditions({
scrobbleAtDurationMs: scrobbleAtDurationMsRef.current, scrobbleAtDurationMs: (scrobbleSettings?.scrobbleAtDuration ?? 0) * 1000,
scrobbleAtPercentage: scrobbleAtPercentageRef.current, scrobbleAtPercentage: scrobbleSettings?.scrobbleAtPercentage,
songCompletedDurationMs: listenedMsRef.current, songCompletedDurationMs: currentTime * 1000,
songDurationMs: currentSong.duration, songDurationMs: currentSong.duration,
}); });
@@ -284,18 +177,25 @@ export const useScrobble = () => {
category: LogCategory.SCROBBLE, category: LogCategory.SCROBBLE,
meta: { meta: {
id: currentSong.id, id: currentSong.id,
reason: 'from listened time', reason: 'from song progress',
}, },
}); });
}, },
}, },
); );
isCurrentSongScrobbledRef.current = true; setIsCurrentSongScrobbled(true);
} }
} }
}, },
[isScrobbleEnabled, isPrivateModeEnabled, sendScrobble], [
isScrobbleEnabled,
isPrivateModeEnabled,
scrobbleSettings?.scrobbleAtDuration,
scrobbleSettings?.scrobbleAtPercentage,
isCurrentSongScrobbled,
sendScrobble,
],
); );
const handleScrobbleFromSongChange = useCallback( const handleScrobbleFromSongChange = useCallback(
@@ -340,16 +240,11 @@ 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;
} }
isCurrentSongScrobbledRef.current = false; setIsCurrentSongScrobbled(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);
@@ -385,15 +280,8 @@ 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(
@@ -409,21 +297,8 @@ 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;
} }
@@ -432,7 +307,6 @@ 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;
} }
@@ -463,9 +337,8 @@ export const useScrobble = () => {
}, },
}, },
); );
flushScrobbleDebug();
}, },
[flushScrobbleDebug, isScrobbleEnabled, isPrivateModeEnabled, sendScrobble], [isScrobbleEnabled, isPrivateModeEnabled, sendScrobble],
); );
const handleScrobbleFromStatus = useCallback( const handleScrobbleFromStatus = useCallback(
@@ -539,10 +412,8 @@ export const useScrobble = () => {
}, },
); );
} }
flushScrobbleDebug();
}, },
[flushScrobbleDebug, isScrobbleEnabled, isPrivateModeEnabled, sendScrobble], [isScrobbleEnabled, isPrivateModeEnabled, sendScrobble],
); );
const handleScrobbleFromRepeat = useCallback(() => { const handleScrobbleFromRepeat = useCallback(() => {
@@ -557,11 +428,9 @@ export const useScrobble = () => {
return; return;
} }
isCurrentSongScrobbledRef.current = false; setIsCurrentSongScrobbled(false);
lastProgressEventRef.current = 0; lastProgressEventRef.current = 0;
previousTimestampRef.current = 0; previousTimestampRef.current = 0;
listenedMsRef.current = 0;
lastListenSampleTimeRef.current = null;
sendScrobble.mutate( sendScrobble.mutate(
{ {
@@ -586,81 +455,17 @@ export const useScrobble = () => {
}, },
}, },
); );
flushScrobbleDebug(); }, [isScrobbleEnabled, isPrivateModeEnabled, sendScrobble]);
}, [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();
}, },
[flushScrobbleDebug, handleScrobbleFromProgress], [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,
@@ -1,7 +1,6 @@
import { forwardRef, useMemo } from 'react'; import { forwardRef, useMemo } from 'react';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { playSongFromItemListControl } from '/@/renderer/components/item-list/helpers/play-row-from-list';
import { useItemListColumnReorder } from '/@/renderer/components/item-list/helpers/use-item-list-column-reorder'; 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 { useItemListColumnResize } from '/@/renderer/components/item-list/helpers/use-item-list-column-resize';
import { useItemListScrollPersist } from '/@/renderer/components/item-list/helpers/use-item-list-scroll-persist'; import { useItemListScrollPersist } from '/@/renderer/components/item-list/helpers/use-item-list-scroll-persist';
@@ -22,7 +21,7 @@ import {
PlaylistSongListResponse, PlaylistSongListResponse,
Song, Song,
} from '/@/shared/types/domain-types'; } from '/@/shared/types/domain-types';
import { ItemListKey, TableColumn } from '/@/shared/types/types'; import { ItemListKey, Play, TableColumn } from '/@/shared/types/types';
interface PlaylistDetailSongListTableProps extends Omit< interface PlaylistDetailSongListTableProps extends Omit<
ItemListTableComponentProps<PlaylistSongListQuery>, ItemListTableComponentProps<PlaylistSongListQuery>,
@@ -104,13 +103,12 @@ export const PlaylistDetailSongListTable = forwardRef<any, PlaylistDetailSongLis
return; return;
} }
playSongFromItemListControl({ const playType = (meta?.playType as Play) || Play.NOW;
index, const items = internalState?.getData() as Song[];
internalState,
item: item as Song, if (index !== undefined) {
meta, player.addToQueueByData(items, playType, item.id);
player, }
});
}, },
}; };
}, [player]); }, [player]);
@@ -230,13 +228,12 @@ export const PlaylistDetailSongListEditTable = forwardRef<any, PlaylistDetailSon
return; return;
} }
playSongFromItemListControl({ const playType = (meta?.playType as Play) || Play.NOW;
index, const items = internalState?.getData() as Song[];
internalState,
item: item as Song, if (index !== undefined) {
meta, player.addToQueueByData(items, playType, item.id);
player, }
});
}, },
}; };
}, [player]); }, [player]);
@@ -1,62 +0,0 @@
import { useIsMutating, useMutation, useQueryClient } from '@tanstack/react-query';
import { AxiosError } from 'axios';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { Playlist } from '/@/shared/types/domain-types';
export const sidebarPlaylistFolderMoveMutationKey = ['sidebar-playlist-folder-move'];
export type SidebarPlaylistFolderMoveArgs = {
serverId: string;
updates: SidebarPlaylistFolderMoveUpdate[];
};
export type SidebarPlaylistFolderMoveUpdate = {
newName: string;
playlist: Playlist;
};
export const useSidebarPlaylistFolderMove = () => {
const queryClient = useQueryClient();
return useMutation<void, AxiosError, SidebarPlaylistFolderMoveArgs>({
mutationFn: async ({ serverId, updates }) => {
for (const { newName, playlist } of updates) {
if (newName === playlist.name) continue;
await api.controller.updatePlaylist({
apiClientProps: { serverId },
body: {
comment: playlist.description || '',
name: newName,
ownerId: playlist.ownerId || '',
public: playlist.public || false,
queryBuilderRules: playlist.rules ?? undefined,
sync: playlist.sync ?? undefined,
},
query: { id: playlist.id },
});
}
},
mutationKey: sidebarPlaylistFolderMoveMutationKey,
onSuccess: (_data, { serverId, updates }) => {
queryClient.invalidateQueries({
queryKey: queryKeys.playlists.list(serverId),
});
for (const { playlist } of updates) {
queryClient.invalidateQueries({
queryKey: queryKeys.playlists.detail(serverId, playlist.id),
});
queryClient.invalidateQueries({
queryKey: queryKeys.playlists.songList(serverId, playlist.id),
});
}
},
});
};
export const useIsMutatingSidebarPlaylistFolderMove = () => {
return useIsMutating({ mutationKey: sidebarPlaylistFolderMoveMutationKey }) > 0;
};
@@ -29,8 +29,9 @@ export const useRemote = () => {
// Initialize the remote // Initialize the remote
useEffect(() => { useEffect(() => {
// we must send this EVEN IF the remote is disabled, as this is what if (!isRemoteEnabled) {
// makes sure that the main process gets the port/username/password on startup return;
}
logFn.debug(logMsg[LogCategory.REMOTE].initializingRemoteSettings, { logFn.debug(logMsg[LogCategory.REMOTE].initializingRemoteSettings, {
category: LogCategory.REMOTE, category: LogCategory.REMOTE,
@@ -1,6 +1,4 @@
import { openContextModal } from '@mantine/modals';
import { CSSProperties, useCallback, useState } from 'react'; import { CSSProperties, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import styles from './library-command-item.module.css'; import styles from './library-command-item.module.css';
@@ -15,7 +13,6 @@ import { useCurrentServer } from '/@/renderer/store';
import { ActionIcon, ActionIconGroup } from '/@/shared/components/action-icon/action-icon'; import { ActionIcon, ActionIconGroup } from '/@/shared/components/action-icon/action-icon';
import { Flex } from '/@/shared/components/flex/flex'; import { Flex } from '/@/shared/components/flex/flex';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
import { Tooltip } from '/@/shared/components/tooltip/tooltip';
import { ExplicitStatus, LibraryItem, Song } from '/@/shared/types/domain-types'; import { ExplicitStatus, LibraryItem, Song } from '/@/shared/types/domain-types';
import { Play } from '/@/shared/types/types'; import { Play } from '/@/shared/types/types';
@@ -64,7 +61,6 @@ export const LibraryCommandItem = ({
}: LibraryCommandItemProps) => { }: LibraryCommandItemProps) => {
const { addToQueueByData, addToQueueByFetch } = usePlayer(); const { addToQueueByData, addToQueueByFetch } = usePlayer();
const server = useCurrentServer(); const server = useCurrentServer();
const { t } = useTranslation();
const handlePlay = useCallback( const handlePlay = useCallback(
(playType: Play) => { (playType: Play) => {
@@ -107,39 +103,6 @@ export const LibraryCommandItem = ({
}, },
}); });
const handleOpenPlaylistModal = useCallback(() => {
const modalProps: {
albumId?: string[];
artistId?: string[];
songId?: string[];
} = {};
switch (itemType) {
case LibraryItem.ALBUM:
modalProps.albumId = [id];
break;
case LibraryItem.ALBUM_ARTIST:
case LibraryItem.ARTIST:
modalProps.artistId = [id];
break;
case LibraryItem.QUEUE_SONG:
case LibraryItem.SONG:
modalProps.songId = [id];
break;
default:
return;
}
openContextModal({
innerProps: {
...modalProps,
},
modal: 'addToPlaylist',
size: 'lg',
title: t('page.contextMenu.addToPlaylist'),
});
}, [id, itemType, t]);
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
const showControls = isHighlighted || isHovered; const showControls = isHighlighted || isHovered;
@@ -217,29 +180,6 @@ export const LibraryCommandItem = ({
)} )}
/> />
</PlayTooltip> </PlayTooltip>
<Tooltip disabled={disabled} label={t('action.addToPlaylist')}>
<ActionIcon
icon="playlistAdd"
onClick={(event) => {
event.stopPropagation();
event.preventDefault();
handleOpenPlaylistModal();
}}
onKeyDown={(e) => {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
e.stopPropagation();
if (!disabled) {
handleOpenPlaylistModal();
}
} else if (e.key === 'Tab') {
e.stopPropagation();
}
}}
size="xs"
variant="default"
/>
</Tooltip>
</ActionIconGroup> </ActionIconGroup>
)} )}
</Flex> </Flex>
@@ -7,29 +7,15 @@ import {
SettingsSection, SettingsSection,
} from '/@/renderer/features/settings/components/settings-section'; } from '/@/renderer/features/settings/components/settings-section';
import { useGeneralSettings, useSettingsStoreActions } from '/@/renderer/store'; import { useGeneralSettings, useSettingsStoreActions } from '/@/renderer/store';
import { ColorInput } from '/@/shared/components/color-input/color-input';
import { NumberInput } from '/@/shared/components/number-input/number-input';
import { Select } from '/@/shared/components/select/select';
import { Switch } from '/@/shared/components/switch/switch'; import { Switch } from '/@/shared/components/switch/switch';
import { TextInput } from '/@/shared/components/text-input/text-input'; import { TextInput } from '/@/shared/components/text-input/text-input';
import { useDebouncedCallback } from '/@/shared/hooks/use-debounced-callback'; import { useDebouncedCallback } from '/@/shared/hooks/use-debounced-callback';
type FolderView = 'navigation' | 'single' | 'tree';
type PlaylistMode = 'compact' | 'expanded';
export const SidebarSettings = memo(() => { export const SidebarSettings = memo(() => {
const { t } = useTranslation(); const { t } = useTranslation();
const settings = useGeneralSettings(); const settings = useGeneralSettings();
const { setSettings } = useSettingsStoreActions(); const { setSettings } = useSettingsStoreActions();
const handleSetSidebarPlaylistFolders = (e: ChangeEvent<HTMLInputElement>) => {
setSettings({
general: {
sidebarPlaylistFolders: e.target.checked,
},
});
};
const handleSetSidebarPlaylistList = (e: ChangeEvent<HTMLInputElement>) => { const handleSetSidebarPlaylistList = (e: ChangeEvent<HTMLInputElement>) => {
setSettings({ setSettings({
general: { general: {
@@ -70,60 +56,6 @@ export const SidebarSettings = memo(() => {
}); });
}, 500); }, 500);
const [localSeparator, setLocalSeparator] = useState(settings.sidebarPlaylistFolderSeparator);
useEffect(() => {
setLocalSeparator(settings.sidebarPlaylistFolderSeparator);
}, [settings.sidebarPlaylistFolderSeparator]);
const debouncedSetSeparator = useDebouncedCallback((value: string) => {
if (value.length === 0) return;
setSettings({
general: {
sidebarPlaylistFolderSeparator: value,
},
});
}, 500);
const folderViewOptions: Array<{ label: string; value: FolderView }> = [
{
label: t('setting.sidebarPlaylistFolderView_optionSingle', {
postProcess: 'sentenceCase',
}),
value: 'single',
},
{
label: t('setting.sidebarPlaylistFolderView_optionTree', {
postProcess: 'sentenceCase',
}),
value: 'tree',
},
{
label: t('setting.sidebarPlaylistFolderView_optionNavigation', {
postProcess: 'sentenceCase',
}),
value: 'navigation',
},
];
const playlistModeOptions: Array<{ label: string; value: PlaylistMode }> = [
{
label: t('setting.sidebarPlaylistMode_optionCompact', {
postProcess: 'sentenceCase',
}),
value: 'compact',
},
{
label: t('setting.sidebarPlaylistMode_optionExpanded', {
postProcess: 'sentenceCase',
}),
value: 'expanded',
},
];
const foldersEnabled = settings.sidebarPlaylistFolders;
const isTreeView = settings.sidebarPlaylistFolderView === 'tree';
const options: SettingOption[] = [ const options: SettingOption[] = [
{ {
control: ( control: (
@@ -166,137 +98,6 @@ export const SidebarSettings = memo(() => {
}), }),
title: t('setting.sidebarPlaylistSorting'), title: t('setting.sidebarPlaylistSorting'),
}, },
{
control: (
<Select
data={playlistModeOptions}
onChange={(value) => {
if (!value) return;
setSettings({
general: {
sidebarPlaylistMode: value as PlaylistMode,
},
});
}}
value={settings.sidebarPlaylistMode}
width={200}
/>
),
description: t('setting.sidebarPlaylistMode', {
context: 'description',
postProcess: 'sentenceCase',
}),
title: t('setting.sidebarPlaylistMode', { postProcess: 'sentenceCase' }),
},
{
control: (
<Switch
checked={settings.sidebarPlaylistFolders}
onChange={handleSetSidebarPlaylistFolders}
/>
),
description: t('setting.sidebarPlaylistFolders', {
context: 'description',
postProcess: 'sentenceCase',
}),
title: t('setting.sidebarPlaylistFolders', { postProcess: 'sentenceCase' }),
},
{
control: (
<TextInput
onChange={(e) => {
const value = e.currentTarget.value;
setLocalSeparator(value);
debouncedSetSeparator(value);
}}
value={localSeparator}
width={120}
/>
),
description: t('setting.sidebarPlaylistFolderSeparator', {
context: 'description',
postProcess: 'sentenceCase',
}),
indent: true,
isHidden: !foldersEnabled,
title: t('setting.sidebarPlaylistFolderSeparator', { postProcess: 'sentenceCase' }),
},
{
control: (
<Select
data={folderViewOptions}
onChange={(value) => {
if (!value) return;
setSettings({
general: {
sidebarPlaylistFolderView: value as FolderView,
},
});
}}
value={settings.sidebarPlaylistFolderView}
width={200}
/>
),
description: t('setting.sidebarPlaylistFolderView', {
context: 'description',
postProcess: 'sentenceCase',
}),
indent: true,
isHidden: !foldersEnabled,
title: t('setting.sidebarPlaylistFolderView', { postProcess: 'sentenceCase' }),
},
{
control: (
<NumberInput
max={64}
min={0}
onBlur={(e) => {
const value = Number(e.currentTarget.value);
if (Number.isFinite(value)) {
setSettings({
general: {
sidebarPlaylistFolderTreeIndent: Math.max(
0,
Math.min(64, Math.round(value)),
),
},
});
}
}}
value={settings.sidebarPlaylistFolderTreeIndent}
width={100}
/>
),
description: t('setting.sidebarPlaylistFolderTreeIndent', {
context: 'description',
postProcess: 'sentenceCase',
}),
indent: true,
isHidden: !foldersEnabled || !isTreeView,
title: t('setting.sidebarPlaylistFolderTreeIndent', { postProcess: 'sentenceCase' }),
},
{
control: (
<ColorInput
format="rgba"
onChangeEnd={(value) => {
setSettings({
general: {
sidebarPlaylistFolderTreeLineColor: value,
},
});
}}
value={settings.sidebarPlaylistFolderTreeLineColor}
/>
),
description: t('setting.sidebarPlaylistFolderTreeLineColor', {
context: 'description',
postProcess: 'sentenceCase',
}),
indent: true,
isHidden: !foldersEnabled || !isTreeView,
title: t('setting.sidebarPlaylistFolderTreeLineColor', { postProcess: 'sentenceCase' }),
},
{ {
control: ( control: (
<Switch <Switch
@@ -60,7 +60,6 @@ const BINDINGS_MAP: Record<BindingActions, string> = {
context: 'listPlayNext', context: 'listPlayNext',
}), }),
listPlayNow: i18n.t('setting.hotkey', { context: 'listPlayNow' }), listPlayNow: i18n.t('setting.hotkey', { context: 'listPlayNow' }),
listShowPlayingSong: i18n.t('setting.hotkey', { context: 'listShowPlayingSong' }),
localSearch: i18n.t('setting.hotkey', { context: 'localSearch' }), localSearch: i18n.t('setting.hotkey', { context: 'localSearch' }),
navigateHome: i18n.t('setting.hotkey', { navigateHome: i18n.t('setting.hotkey', {
context: 'navigateHome', context: 'navigateHome',
@@ -1,9 +0,0 @@
.row {
align-items: center;
}
.row-indented {
padding-left: var(--theme-spacing-lg);
margin-left: var(--theme-spacing-md);
border-left: 2px solid var(--theme-colors-surface);
}
@@ -1,7 +1,5 @@
import React, { memo } from 'react'; import React, { memo } from 'react';
import styles from './settings-option.module.css';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
import { Icon } from '/@/shared/components/icon/icon'; import { Icon } from '/@/shared/components/icon/icon';
import { Stack } from '/@/shared/components/stack/stack'; import { Stack } from '/@/shared/components/stack/stack';
@@ -11,20 +9,15 @@ import { Tooltip } from '/@/shared/components/tooltip/tooltip';
interface SettingsOptionProps { interface SettingsOptionProps {
control: React.ReactNode; control: React.ReactNode;
description?: React.ReactNode | string; description?: React.ReactNode | string;
indent?: boolean;
note?: string; note?: string;
title: React.ReactNode | string; title: React.ReactNode | string;
} }
export const SettingsOptions = memo( export const SettingsOptions = memo(
({ control, description, indent, note, title }: SettingsOptionProps) => { ({ control, description, note, title }: SettingsOptionProps) => {
return ( return (
<> <>
<Group <Group justify="space-between" style={{ alignItems: 'center' }} wrap="nowrap">
className={indent ? styles.rowIndented : styles.row}
justify="space-between"
wrap="nowrap"
>
<Stack <Stack
gap="xs" gap="xs"
style={{ style={{
@@ -8,7 +8,6 @@ import { TextTitle } from '/@/shared/components/text-title/text-title';
export type SettingOption = { export type SettingOption = {
control: ReactNode; control: ReactNode;
description: ReactNode | string; description: ReactNode | string;
indent?: boolean;
isHidden?: boolean; isHidden?: boolean;
note?: string; note?: string;
title: string; title: string;
@@ -1,5 +0,0 @@
.controls {
display: flex;
flex-shrink: 0;
gap: 0;
}
@@ -1,90 +0,0 @@
import clsx from 'clsx';
import styles from './item-row-play-controls.module.css';
import {
LONG_PRESS_PLAY_BEHAVIOR,
PlayTooltip,
} from '/@/renderer/features/shared/components/play-button-group';
import { usePlayButtonClick } from '/@/renderer/features/shared/hooks/use-play-button-click';
import { ActionIcon, ActionIconGroup } from '/@/shared/components/action-icon/action-icon';
import { Play } from '/@/shared/types/types';
interface ItemRowPlayControlsProps {
className?: string;
disabled?: boolean;
onPlay: (playType: Play) => void;
}
export const ItemRowPlayControls = ({ className, disabled, onPlay }: ItemRowPlayControlsProps) => {
const handlePlayNext = usePlayButtonClick({
onClick: () => {
onPlay(Play.NEXT);
},
onLongPress: () => {
onPlay(LONG_PRESS_PLAY_BEHAVIOR[Play.NEXT]);
},
});
const handlePlayNow = usePlayButtonClick({
onClick: () => {
onPlay(Play.NOW);
},
onLongPress: () => {
onPlay(LONG_PRESS_PLAY_BEHAVIOR[Play.NOW]);
},
});
const handlePlayLast = usePlayButtonClick({
onClick: () => {
onPlay(Play.LAST);
},
onLongPress: () => {
onPlay(LONG_PRESS_PLAY_BEHAVIOR[Play.LAST]);
},
});
return (
<ActionIconGroup className={clsx(styles.controls, className)}>
<PlayTooltip disabled={disabled} type={Play.NOW}>
<ActionIcon
icon="mediaPlay"
iconProps={{
size: 'md',
}}
size="xs"
variant="subtle"
{...handlePlayNow.handlers}
{...handlePlayNow.props}
disabled={disabled}
/>
</PlayTooltip>
<PlayTooltip disabled={disabled} type={Play.NEXT}>
<ActionIcon
icon="mediaPlayNext"
iconProps={{
size: 'md',
}}
size="xs"
variant="subtle"
{...handlePlayNext.handlers}
{...handlePlayNext.props}
disabled={disabled}
/>
</PlayTooltip>
<PlayTooltip disabled={disabled} type={Play.LAST}>
<ActionIcon
icon="mediaPlayLast"
iconProps={{
size: 'md',
}}
size="xs"
variant="subtle"
{...handlePlayLast.handlers}
{...handlePlayLast.props}
disabled={disabled}
/>
</PlayTooltip>
</ActionIconGroup>
);
};
@@ -1,7 +1,6 @@
.container { .container {
display: flex; display: flex;
align-items: center; align-items: center;
width: 100%;
height: 65px; height: 65px;
-webkit-app-region: drag; -webkit-app-region: drag;
@@ -18,19 +18,7 @@ export const ActionBar = () => {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<Grid <Grid display="flex" gap="sm" style={{ padding: '0 var(--theme-spacing-md)' }} w="100%">
display="flex"
gap="sm"
styles={{
inner: {
width: '100%',
},
root: {
padding: '0 var(--theme-spacing-md',
width: '100%',
},
}}
>
<Grid.Col span={7}> <Grid.Col span={7}>
<TextInput <TextInput
leftSection={<Icon icon="search" />} leftSection={<Icon icon="search" />}
@@ -7,10 +7,8 @@ import { ActionBar } from '/@/renderer/features/sidebar/components/action-bar';
import { SidebarIcon } from '/@/renderer/features/sidebar/components/sidebar-icon'; import { SidebarIcon } from '/@/renderer/features/sidebar/components/sidebar-icon';
import { SidebarItem } from '/@/renderer/features/sidebar/components/sidebar-item'; import { SidebarItem } from '/@/renderer/features/sidebar/components/sidebar-item';
import { import {
SidebarPlaylistAddDragContext,
SidebarPlaylistList, SidebarPlaylistList,
SidebarSharedPlaylistList, SidebarSharedPlaylistList,
useSidebarPlaylistAddDragMonitor,
} from '/@/renderer/features/sidebar/components/sidebar-playlist-list'; } from '/@/renderer/features/sidebar/components/sidebar-playlist-list';
import { import {
SidebarItemType, SidebarItemType,
@@ -22,17 +20,6 @@ import { Group } from '/@/shared/components/group/group';
import { ScrollArea } from '/@/shared/components/scroll-area/scroll-area'; import { ScrollArea } from '/@/shared/components/scroll-area/scroll-area';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
const MobileSidebarPlaylistSection = () => {
const isAddDragActive = useSidebarPlaylistAddDragMonitor();
return (
<SidebarPlaylistAddDragContext.Provider value={isAddDragActive}>
<SidebarPlaylistList />
<SidebarSharedPlaylistList />
</SidebarPlaylistAddDragContext.Provider>
);
};
export const MobileSidebar = () => { export const MobileSidebar = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const sidebarPlaylistList = useSidebarPlaylistList(); const sidebarPlaylistList = useSidebarPlaylistList();
@@ -106,7 +93,12 @@ export const MobileSidebar = () => {
})} })}
</Accordion.Panel> </Accordion.Panel>
</Accordion.Item> </Accordion.Item>
{sidebarPlaylistList && <MobileSidebarPlaylistSection />} {sidebarPlaylistList && (
<>
<SidebarPlaylistList />
<SidebarSharedPlaylistList />
</>
)}
</Accordion> </Accordion>
</ScrollArea> </ScrollArea>
</div> </div>
@@ -1,146 +0,0 @@
.folder {
display: flex;
flex-direction: column;
}
.header-dragged-over {
position: relative;
&::after {
position: absolute;
inset: 0;
pointer-events: none;
content: "";
border: 2px solid var(--theme-colors-primary);
border-radius: var(--theme-radius-md);
opacity: 0.8;
}
}
.header {
display: flex;
gap: var(--theme-spacing-md);
align-items: center;
width: 100%;
padding: var(--theme-spacing-sm) var(--theme-spacing-md);
font: inherit;
color: inherit;
text-align: left;
cursor: pointer;
background: transparent;
border: 0;
border-radius: var(--theme-radius-md);
&:hover {
background-color: var(--theme-colors-background);
}
}
.chevron {
display: flex;
flex-shrink: 0;
align-items: center;
}
.collapse {
overflow: hidden;
}
.name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.count {
flex-shrink: 0;
}
.children {
padding-left: var(--theme-spacing-md);
}
.tree-children {
padding-left: var(--playlist-folder-tree-indent, 16px);
}
.tree-branch {
position: relative;
padding-left: calc(var(--playlist-folder-tree-indent, 16px) - 4px);
&::before {
position: absolute;
top: 0;
bottom: 50%;
left: 0;
width: calc(var(--playlist-folder-tree-indent, 16px) - 8px);
content: "";
border-bottom: 1px solid var(--playlist-folder-tree-line-color, var(--theme-colors-border));
border-left: 1px solid var(--playlist-folder-tree-line-color, var(--theme-colors-border));
}
&:not(:last-child)::after {
position: absolute;
top: 50%;
bottom: 0;
left: 0;
content: "";
border-left: 1px solid var(--playlist-folder-tree-line-color, var(--theme-colors-border));
}
}
.navigation {
display: flex;
flex-direction: column;
}
.nav-folder {
display: flex;
gap: var(--theme-spacing-md);
align-items: center;
width: 100%;
padding: var(--theme-spacing-xs) var(--theme-spacing-md);
font: inherit;
color: inherit;
text-align: left;
cursor: pointer;
background: transparent;
border: 0;
border-radius: var(--theme-radius-md);
&:hover {
background-color: var(--theme-colors-surface);
}
}
.nav-folder-icon {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 3rem;
min-width: 3rem;
height: 3rem;
background-color: var(--theme-colors-surface);
border-radius: var(--theme-radius-md);
}
.nav-chevron {
flex-shrink: 0;
opacity: 0.6;
}
.root-drop-target-dragged-over {
position: relative;
&::after {
position: absolute;
inset: 0;
pointer-events: none;
content: "";
border: 2px solid var(--theme-colors-primary);
border-radius: var(--theme-radius-md);
opacity: 0.8;
}
}
File diff suppressed because it is too large Load Diff
@@ -4,10 +4,6 @@
padding: var(--theme-spacing-sm) var(--theme-spacing-md); padding: var(--theme-spacing-sm) var(--theme-spacing-md);
} }
.panel {
position: relative;
}
.row { .row {
position: relative; position: relative;
display: flex; display: flex;
@@ -17,28 +13,13 @@
} }
.row-hover { .row-hover {
.metadata, .metadata {
.compact-name {
margin-right: 100px; margin-right: 100px;
} }
background-color: var(--theme-colors-surface); background-color: var(--theme-colors-surface);
} }
.row-compact {
align-items: center;
padding: var(--theme-spacing-sm) var(--theme-spacing-md);
}
.compact-name {
flex: 1;
min-width: 0;
padding: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.controls { .controls {
position: absolute; position: absolute;
top: 50%; top: 50%;
@@ -48,24 +29,9 @@
transform: translateY(-50%); transform: translateY(-50%);
} }
.controls-compact { .row-dragged-over {
position: absolute; border-radius: var(--mantine-radius-sm);
top: 50%; box-shadow: 0 0 0 2px var(--theme-colors-primary);
right: var(--theme-spacing-xs);
flex-shrink: 0;
padding: 0 var(--theme-spacing-sm);
background: none;
transform: translateY(-50%);
}
.row-dragged-over::after {
position: absolute;
inset: 0;
z-index: 1;
pointer-events: none;
content: '';
border: 2px solid var(--theme-colors-primary);
border-radius: var(--theme-radius-md);
opacity: 0.8; opacity: 0.8;
} }
@@ -1,8 +1,7 @@
import { openContextModal } from '@mantine/modals'; import { openContextModal } from '@mantine/modals';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import clsx from 'clsx'; import clsx from 'clsx';
import { motion } from 'motion/react'; import { memo, MouseEvent, useCallback, useMemo, useState } from 'react';
import { createContext, memo, MouseEvent, useCallback, useContext, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { generatePath, Link } from 'react-router'; import { generatePath, Link } from 'react-router';
@@ -13,38 +12,27 @@ import { ContextMenuController } from '/@/renderer/features/context-menu/context
import { usePlayer } from '/@/renderer/features/player/context/player-context'; import { usePlayer } from '/@/renderer/features/player/context/player-context';
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api'; import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
import { openCreatePlaylistModal } from '/@/renderer/features/playlists/components/create-playlist-form'; import { openCreatePlaylistModal } from '/@/renderer/features/playlists/components/create-playlist-form';
import { useIsMutatingSidebarPlaylistFolderMove } from '/@/renderer/features/playlists/mutations/sidebar-playlist-folder-move-mutation';
import { ItemRowPlayControls } from '/@/renderer/features/shared/components/item-row-play-controls';
import { import {
collectFolderPaths, LONG_PRESS_PLAY_BEHAVIOR,
PlaylistFolderDragExpandProvider, PlayTooltip,
PlaylistFolderViews, } from '/@/renderer/features/shared/components/play-button-group';
PlaylistRootAccordionControl, import { usePlayButtonClick } from '/@/renderer/features/shared/hooks/use-play-button-click';
usePlaylistFolderState,
usePlaylistFolderViewState,
usePlaylistNavigationState,
} from '/@/renderer/features/sidebar/components/playlist-folder-tree';
import { useDragDrop } from '/@/renderer/hooks/use-drag-drop'; import { useDragDrop } from '/@/renderer/hooks/use-drag-drop';
import { useDragMonitor } from '/@/renderer/hooks/use-drag-monitor';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { import {
useCurrentServer, useCurrentServer,
useCurrentServerId, useCurrentServerId,
usePermissions, usePermissions,
useSidebarPlaylistListFilterRegex, useSidebarPlaylistListFilterRegex,
useSidebarPlaylistMode,
useSidebarPlaylistSorting, useSidebarPlaylistSorting,
} from '/@/renderer/store'; } from '/@/renderer/store';
import { formatDurationString } from '/@/renderer/utils'; import { formatDurationString } from '/@/renderer/utils';
import { Accordion } from '/@/shared/components/accordion/accordion'; import { Accordion } from '/@/shared/components/accordion/accordion';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; import { ActionIcon, ActionIconGroup } from '/@/shared/components/action-icon/action-icon';
import { animationProps } from '/@/shared/components/animations/animation-props';
import { animationVariants } from '/@/shared/components/animations/animation-variants';
import { ButtonProps } from '/@/shared/components/button/button'; import { ButtonProps } from '/@/shared/components/button/button';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
import { Icon } from '/@/shared/components/icon/icon'; import { Icon } from '/@/shared/components/icon/icon';
import { Image } from '/@/shared/components/image/image'; import { Image } from '/@/shared/components/image/image';
import { LoadingOverlay } from '/@/shared/components/loading-overlay/loading-overlay';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
import { useLocalStorage } from '/@/shared/hooks/use-local-storage'; import { useLocalStorage } from '/@/shared/hooks/use-local-storage';
import { import {
@@ -54,51 +42,15 @@ import {
Song, Song,
SortOrder, SortOrder,
} from '/@/shared/types/domain-types'; } from '/@/shared/types/domain-types';
import { DragData, DragOperation, DragTarget } from '/@/shared/types/drag-and-drop'; import { DragOperation, DragTarget } from '/@/shared/types/drag-and-drop';
import { Play } from '/@/shared/types/types'; import { Play } from '/@/shared/types/types';
const MotionLink = motion.create(Link);
const playlistRowDimVariants = animationVariants.combine(animationVariants.fadeIn, {
hidden: { opacity: 0.5 },
});
const getPlaylistOrderKey = (serverId: string | undefined, scope: 'owned' | 'shared') => { const getPlaylistOrderKey = (serverId: string | undefined, scope: 'owned' | 'shared') => {
const sid = serverId || 'local'; const sid = serverId || 'local';
return `playlist_order:${sid}:${scope}`; return `playlist_order:${sid}:${scope}`;
}; };
export const SidebarPlaylistAddDragContext = createContext(false); interface PlaylistRowButtonProps extends Omit<ButtonProps, 'onContextMenu' | 'onPlay'> {
const isAddToPlaylistDragSource = (source: DragData) => {
return (
source.itemType !== undefined &&
source.type !== DragTarget.PLAYLIST &&
(source.operation?.includes(DragOperation.ADD) ?? false)
);
};
export const useSidebarPlaylistAddDragMonitor = () => {
const [isAddDragActive, setIsAddDragActive] = useState(false);
const handleAddDragStart = useCallback(() => {
setIsAddDragActive(true);
}, []);
const handleAddDragDrop = useCallback(() => {
setIsAddDragActive(false);
}, []);
useDragMonitor({
canMonitor: isAddToPlaylistDragSource,
onDragStart: handleAddDragStart,
onDrop: handleAddDragDrop,
});
return isAddDragActive;
};
export interface PlaylistRowButtonProps extends Omit<ButtonProps, 'onContextMenu' | 'onPlay'> {
item: Playlist; item: Playlist;
name: string; name: string;
onContextMenu: (e: MouseEvent<HTMLAnchorElement>, item: Playlist) => void; onContextMenu: (e: MouseEvent<HTMLAnchorElement>, item: Playlist) => void;
@@ -106,7 +58,7 @@ export interface PlaylistRowButtonProps extends Omit<ButtonProps, 'onContextMenu
to: string; to: string;
} }
export const PlaylistRowButton = memo( const PlaylistRowButton = memo(
({ item, name, onContextMenu, onReorder, to }: PlaylistRowButtonProps) => { ({ item, name, onContextMenu, onReorder, to }: PlaylistRowButtonProps) => {
const url = { const url = {
pathname: generatePath(AppRoute.PLAYLISTS_DETAIL_SONGS, { playlistId: to }), pathname: generatePath(AppRoute.PLAYLISTS_DETAIL_SONGS, { playlistId: to }),
@@ -114,12 +66,8 @@ export const PlaylistRowButton = memo(
}; };
const { t } = useTranslation(); const { t } = useTranslation();
const sidebarPlaylistSorting = useSidebarPlaylistSorting(); const sidebarPlaylistSorting = useSidebarPlaylistSorting();
const sidebarPlaylistMode = useSidebarPlaylistMode();
const isCompact = sidebarPlaylistMode === 'compact';
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
const isSmartPlaylist = Boolean(item.rules);
const isAddDragActive = useContext(SidebarPlaylistAddDragContext);
const { isDraggedOver, isDragging, ref } = useDragDrop<HTMLAnchorElement>({ const { isDraggedOver, isDragging, ref } = useDragDrop<HTMLAnchorElement>({
drag: { drag: {
@@ -137,7 +85,6 @@ export const PlaylistRowButton = memo(
canDrop: (args) => { canDrop: (args) => {
// Allow dropping items into a playlist (ADD) // Allow dropping items into a playlist (ADD)
const canAdd = const canAdd =
!isSmartPlaylist &&
args.source.itemType !== undefined && args.source.itemType !== undefined &&
args.source.type !== DragTarget.PLAYLIST && args.source.type !== DragTarget.PLAYLIST &&
(args.source.operation?.includes(DragOperation.ADD) ?? false); (args.source.operation?.includes(DragOperation.ADD) ?? false);
@@ -159,7 +106,6 @@ export const PlaylistRowButton = memo(
}; };
}, },
onDrag: () => { onDrag: () => {
console.log('started drag');
return; return;
}, },
onDragLeave: () => { onDragLeave: () => {
@@ -192,10 +138,6 @@ export const PlaylistRowButton = memo(
return; return;
} }
if (isSmartPlaylist) {
return;
}
const modalProps: { const modalProps: {
albumId?: string[]; albumId?: string[];
artistId?: string[]; artistId?: string[];
@@ -268,18 +210,12 @@ export const PlaylistRowButton = memo(
type: 'table', type: 'table',
}); });
const isDimmed = isDragging || (isSmartPlaylist && isAddDragActive);
return ( return (
<MotionLink <Link
{...animationProps.fadeIn}
animate={isDimmed ? 'hidden' : 'show'}
className={clsx(styles.row, { className={clsx(styles.row, {
[styles.rowCompact]: isCompact, [styles.rowDraggedOver]: isDraggedOver,
[styles.rowDraggedOver]: isDraggedOver && !isSmartPlaylist,
[styles.rowHover]: isHovered, [styles.rowHover]: isHovered,
})} })}
initial={false}
onContextMenu={(e: MouseEvent<HTMLAnchorElement>) => { onContextMenu={(e: MouseEvent<HTMLAnchorElement>) => {
e.preventDefault(); e.preventDefault();
onContextMenu(e, item); onContextMenu(e, item);
@@ -287,80 +223,136 @@ export const PlaylistRowButton = memo(
onMouseEnter={() => setIsHovered(true)} onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)} onMouseLeave={() => setIsHovered(false)}
ref={ref} ref={ref}
style={{
opacity: isDragging ? 0.5 : 1,
}}
to={url} to={url}
variants={playlistRowDimVariants}
> >
{isCompact ? ( <div className={styles.rowGroup}>
<> <Image containerClassName={styles.imageContainer} src={imageUrl} />
<Text className={styles.compactName} fw={500} size="md"> <div className={styles.metadata}>
<Text className={styles.name} fw={500} size="md">
{name} {name}
</Text> </Text>
{isHovered && ( <div className={styles.metadataGroup}>
<ItemRowPlayControls <div
className={clsx(styles.controls, styles.controlsCompact)} className={clsx(
onPlay={(playType) => handlePlay(to, playType)} styles.metadataGroupItem,
/> styles.metadataGroupItemNoShrink,
)} )}
</> >
) : ( <Icon color="muted" icon="itemSong" size="sm" />
<> <Text isMuted size="sm">
<div className={styles.rowGroup}> {item.songCount || 0}
<Image containerClassName={styles.imageContainer} src={imageUrl} />
<div className={styles.metadata}>
<Text className={styles.name} fw={500} size="md">
{name}
</Text> </Text>
<div className={styles.metadataGroup}>
<div
className={clsx(
styles.metadataGroupItem,
styles.metadataGroupItemNoShrink,
)}
>
<Icon color="muted" icon="itemSong" size="sm" />
<Text isMuted size="sm">
{item.songCount || 0}
</Text>
</div>
<div className={styles.metadataGroupItem}>
<Icon color="muted" icon="duration" size="sm" />
<Text isMuted size="sm">
{formatDurationString(item.duration ?? 0)}
</Text>
</div>
{item.ownerId === permissions.userId &&
Boolean(item.public) && (
<div className={styles.metadataGroupItem}>
<Text isMuted size="sm">
{t('common.public')}
</Text>
</div>
)}
{item.ownerId !== permissions.userId && (
<div className={styles.metadataGroupItem}>
<Icon color="muted" icon="user" size="sm" />
<Text isMuted size="sm">
{item.owner}
</Text>
</div>
)}
</div>
</div> </div>
<div className={styles.metadataGroupItem}>
<Icon color="muted" icon="duration" size="sm" />
<Text isMuted size="sm">
{formatDurationString(item.duration ?? 0)}
</Text>
</div>
{item.ownerId === permissions.userId && Boolean(item.public) && (
<div className={styles.metadataGroupItem}>
<Text isMuted size="sm">
{t('common.public')}
</Text>
</div>
)}
{item.ownerId !== permissions.userId && (
<div className={styles.metadataGroupItem}>
<Icon color="muted" icon="user" size="sm" />
<Text isMuted size="sm">
{item.owner}
</Text>
</div>
)}
</div> </div>
</div>
</div>
{isHovered && ( {isHovered && <RowControls id={to} onPlay={handlePlay} />}
<ItemRowPlayControls </Link>
className={styles.controls}
onPlay={(playType) => handlePlay(to, playType)}
/>
)}
</>
)}
</MotionLink>
); );
}, },
); );
const RowControls = ({
id,
onPlay,
}: {
id: string;
onPlay: (id: string, playType: Play) => void;
}) => {
const handlePlayNext = usePlayButtonClick({
onClick: () => {
onPlay(id, Play.NEXT);
},
onLongPress: () => {
onPlay(id, LONG_PRESS_PLAY_BEHAVIOR[Play.NEXT]);
},
});
const handlePlayNow = usePlayButtonClick({
onClick: () => {
onPlay(id, Play.NOW);
},
onLongPress: () => {
onPlay(id, LONG_PRESS_PLAY_BEHAVIOR[Play.NOW]);
},
});
const handlePlayLast = usePlayButtonClick({
onClick: () => {
onPlay(id, Play.LAST);
},
onLongPress: () => {
onPlay(id, LONG_PRESS_PLAY_BEHAVIOR[Play.LAST]);
},
});
return (
<ActionIconGroup className={styles.controls}>
<PlayTooltip type={Play.NOW}>
<ActionIcon
icon="mediaPlay"
iconProps={{
size: 'md',
}}
size="xs"
variant="subtle"
{...handlePlayNow.handlers}
{...handlePlayNow.props}
/>
</PlayTooltip>
<PlayTooltip type={Play.NEXT}>
<ActionIcon
icon="mediaPlayNext"
iconProps={{
size: 'md',
}}
size="xs"
variant="subtle"
{...handlePlayNext.handlers}
{...handlePlayNext.props}
/>
</PlayTooltip>
<PlayTooltip type={Play.LAST}>
<ActionIcon
icon="mediaPlayLast"
iconProps={{
size: 'md',
}}
size="xs"
variant="subtle"
{...handlePlayLast.handlers}
{...handlePlayLast.props}
/>
</PlayTooltip>
</ActionIconGroup>
);
};
export const SidebarPlaylistList = () => { export const SidebarPlaylistList = () => {
const player = usePlayer(); const player = usePlayer();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -493,64 +485,12 @@ export const SidebarPlaylistList = () => {
openCreatePlaylistModal(server, e); openCreatePlaylistModal(server, e);
}; };
const folderViewState = usePlaylistFolderViewState(playlistItems?.items ?? []);
const { folderView, groups, tree } = folderViewState;
const navigation = usePlaylistNavigationState();
const inNavigation = folderView === 'navigation' && navigation.pathStack.length > 0;
const folderPaths = useMemo(() => {
if (folderView === 'single') {
return groups.reduce<string[]>((acc, g) => {
if (g.type === 'folder') acc.push(g.name);
return acc;
}, []);
}
return collectFolderPaths(tree);
}, [folderView, groups, tree]);
const { expandedSet, setMany, toggle } = usePlaylistFolderState('owned');
const allExpanded =
folderPaths.length > 0 && folderPaths.every((path) => expandedSet.has(path));
const handleToggleAllFolders = useCallback(
(e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
setMany(folderPaths, !allExpanded);
},
[setMany, folderPaths, allExpanded],
);
const handleNavigateUp = useCallback(
(e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
navigation.goUp();
},
[navigation],
);
const showExpandAll = folderView !== 'navigation' && folderPaths.length > 0;
const isFolderMovePending = useIsMutatingSidebarPlaylistFolderMove();
return ( return (
<Accordion.Item value="playlists"> <Accordion.Item value="playlists">
<PlaylistRootAccordionControl allPlaylists={playlistItems?.items ?? []}> <Accordion.Control component="div" role="button" style={{ userSelect: 'none' }}>
<Group gap="xs" justify="space-between" pr="var(--theme-spacing-md)" wrap="nowrap"> <Group justify="space-between" pr="var(--theme-spacing-md)">
<Group gap="xs" style={{ minWidth: 0 }} wrap="nowrap"> <Text fw={500}>{t('page.sidebar.playlists')}</Text>
{inNavigation && ( <Group gap="xs">
<ActionIcon
icon="arrowLeftS"
iconProps={{ size: 'lg' }}
onClick={handleNavigateUp}
size="xs"
tooltip={{ label: t('common.back') }}
variant="subtle"
/>
)}
<Text className={styles.name} fw={500}>
{inNavigation ? navigation.currentName : t('page.sidebar.playlists')}
</Text>
</Group>
<Group gap="xs" wrap="nowrap">
<ActionIcon <ActionIcon
icon="add" icon="add"
iconProps={{ iconProps={{
@@ -563,27 +503,6 @@ export const SidebarPlaylistList = () => {
}} }}
variant="subtle" variant="subtle"
/> />
{showExpandAll && (
<ActionIcon
icon={allExpanded ? 'collapseAll' : 'expandAll'}
iconProps={{
size: 'lg',
}}
onClick={handleToggleAllFolders}
size="xs"
tooltip={{
label: t(
allExpanded
? 'action.collapseAllFolders'
: 'action.expandAllFolders',
{
postProcess: 'sentenceCase',
},
),
}}
variant="subtle"
/>
)}
<ActionIcon <ActionIcon
component={Link} component={Link}
icon="list" icon="list"
@@ -600,20 +519,18 @@ export const SidebarPlaylistList = () => {
/> />
</Group> </Group>
</Group> </Group>
</PlaylistRootAccordionControl> </Accordion.Control>
<Accordion.Panel className={styles.panel}> <Accordion.Panel>
<LoadingOverlay pos="absolute" visible={isFolderMovePending} /> {playlistItems?.items?.map((item, index) => (
<PlaylistFolderDragExpandProvider expandedSet={expandedSet} setMany={setMany}> <PlaylistRowButton
<PlaylistFolderViews item={item}
{...folderViewState} key={index}
allPlaylists={playlistItems?.items ?? []} name={item.name}
expandedSet={expandedSet}
navigation={navigation}
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}
onReorder={handleReorder} onReorder={handleReorder}
onToggleFolder={toggle} to={item.id}
/> />
</PlaylistFolderDragExpandProvider> ))}
</Accordion.Panel> </Accordion.Panel>
</Accordion.Item> </Accordion.Item>
); );
@@ -751,58 +668,28 @@ export const SidebarSharedPlaylistList = () => {
setPlaylistOrder(reorderedIds); setPlaylistOrder(reorderedIds);
}; };
const folderViewState = usePlaylistFolderViewState(playlistItems?.items ?? []);
const navigation = usePlaylistNavigationState();
const { expandedSet, setMany, toggle } = usePlaylistFolderState('shared');
const inNavigation =
folderViewState.folderView === 'navigation' && navigation.pathStack.length > 0;
const handleNavigateUp = useCallback(
(e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
navigation.goUp();
},
[navigation],
);
const isFolderMovePending = useIsMutatingSidebarPlaylistFolderMove();
if (playlistItems?.items?.length === 0) { if (playlistItems?.items?.length === 0) {
return null; return null;
} }
return ( return (
<Accordion.Item value="shared-playlists"> <Accordion.Item value="shared-playlists">
<Accordion.Control component="motion.div" role="button" style={{ userSelect: 'none' }}> <Accordion.Control>
<Group gap="xs" style={{ minWidth: 0 }} wrap="nowrap"> <Text fw={500} variant="secondary">
{inNavigation && ( {t('page.sidebar.shared')}
<ActionIcon </Text>
icon="arrowLeftS"
iconProps={{ size: 'lg' }}
onClick={handleNavigateUp}
size="xs"
tooltip={{ label: t('common.back') }}
variant="subtle"
/>
)}
<Text className={styles.name} fw={500} variant="secondary">
{inNavigation ? navigation.currentName : t('page.sidebar.shared')}
</Text>
</Group>
</Accordion.Control> </Accordion.Control>
<Accordion.Panel className={styles.panel}> <Accordion.Panel>
<LoadingOverlay pos="absolute" visible={isFolderMovePending} /> {playlistItems?.items?.map((item, index) => (
<PlaylistFolderDragExpandProvider expandedSet={expandedSet} setMany={setMany}> <PlaylistRowButton
<PlaylistFolderViews item={item}
{...folderViewState} key={index}
allPlaylists={playlistItems?.items ?? []} name={item.name}
expandedSet={expandedSet}
navigation={navigation}
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}
onReorder={handleReorder} onReorder={handleReorder}
onToggleFolder={toggle} to={item.id}
/> />
</PlaylistFolderDragExpandProvider> ))}
</Accordion.Panel> </Accordion.Panel>
</Accordion.Item> </Accordion.Item>
); );
@@ -16,10 +16,8 @@ import { SidebarCollectionList } from '/@/renderer/features/sidebar/components/s
import { SidebarIcon } from '/@/renderer/features/sidebar/components/sidebar-icon'; import { SidebarIcon } from '/@/renderer/features/sidebar/components/sidebar-icon';
import { SidebarItem } from '/@/renderer/features/sidebar/components/sidebar-item'; import { SidebarItem } from '/@/renderer/features/sidebar/components/sidebar-item';
import { import {
SidebarPlaylistAddDragContext,
SidebarPlaylistList, SidebarPlaylistList,
SidebarSharedPlaylistList, SidebarSharedPlaylistList,
useSidebarPlaylistAddDragMonitor,
} from '/@/renderer/features/sidebar/components/sidebar-playlist-list'; } from '/@/renderer/features/sidebar/components/sidebar-playlist-list';
import { import {
useAppStore, useAppStore,
@@ -47,17 +45,6 @@ import { Tooltip } from '/@/shared/components/tooltip/tooltip';
import { ExplicitStatus, LibraryItem } from '/@/shared/types/domain-types'; import { ExplicitStatus, LibraryItem } from '/@/shared/types/domain-types';
import { Platform } from '/@/shared/types/types'; import { Platform } from '/@/shared/types/types';
const SidebarPlaylistSection = () => {
const isAddDragActive = useSidebarPlaylistAddDragMonitor();
return (
<SidebarPlaylistAddDragContext.Provider value={isAddDragActive}>
<SidebarPlaylistList />
<SidebarSharedPlaylistList />
</SidebarPlaylistAddDragContext.Provider>
);
};
export const Sidebar = () => { export const Sidebar = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -153,7 +140,12 @@ export const Sidebar = () => {
</Accordion.Panel> </Accordion.Panel>
</Accordion.Item> </Accordion.Item>
<SidebarCollectionList /> <SidebarCollectionList />
{sidebarPlaylistList && <SidebarPlaylistSection />} {sidebarPlaylistList && (
<>
<SidebarPlaylistList />
<SidebarSharedPlaylistList />
</>
)}
</Accordion> </Accordion>
</ScrollArea> </ScrollArea>
<AnimatePresence initial={false} mode="popLayout"> <AnimatePresence initial={false} mode="popLayout">
-9
View File
@@ -46,13 +46,7 @@ declare global {
FS_GENERAL_SIDE_QUEUE_TYPE?: string; FS_GENERAL_SIDE_QUEUE_TYPE?: string;
FS_GENERAL_SIDEBAR_COLLAPSE_SHARED?: string; FS_GENERAL_SIDEBAR_COLLAPSE_SHARED?: string;
FS_GENERAL_SIDEBAR_COLLAPSED_NAVIGATION?: string; FS_GENERAL_SIDEBAR_COLLAPSED_NAVIGATION?: string;
FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_SEPARATOR?: string;
FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_TREE_INDENT?: string;
FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_TREE_LINE_COLOR?: string;
FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_VIEW?: string;
FS_GENERAL_SIDEBAR_PLAYLIST_FOLDERS?: string;
FS_GENERAL_SIDEBAR_PLAYLIST_LIST?: string; FS_GENERAL_SIDEBAR_PLAYLIST_LIST?: string;
FS_GENERAL_SIDEBAR_PLAYLIST_MODE?: string;
FS_GENERAL_SIDEBAR_PLAYLIST_SORTING?: string; FS_GENERAL_SIDEBAR_PLAYLIST_SORTING?: string;
FS_GENERAL_THEME?: string; FS_GENERAL_THEME?: string;
FS_GENERAL_THEME_DARK?: string; FS_GENERAL_THEME_DARK?: string;
@@ -71,16 +65,13 @@ 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;
+2 -6
View File
@@ -37,7 +37,7 @@ interface UseDraggableProps {
drop?: { drop?: {
canDrop: (args: { source: DragData }) => boolean; canDrop: (args: { source: DragData }) => boolean;
getData: () => DragData; getData: () => DragData;
onDrag: (args: { edge: Edge | null; source: DragData }) => void; onDrag: (args: { edge: Edge | null }) => void;
onDragLeave: () => void; onDragLeave: () => void;
onDrop: (args: { edge: Edge | null; self: DragData; source: DragData }) => void; onDrop: (args: { edge: Edge | null; self: DragData; source: DragData }) => void;
}; };
@@ -139,14 +139,10 @@ export const useDragDrop = <TElement extends HTMLElement>({
}, },
onDrag: (args) => { onDrag: (args) => {
const closestEdgeOfTarget: Edge | null = extractClosestEdge(args.self.data); const closestEdgeOfTarget: Edge | null = extractClosestEdge(args.self.data);
drop.onDrag?.({ drop.onDrag?.({ edge: closestEdgeOfTarget });
edge: closestEdgeOfTarget,
source: args.source.data as unknown as DragData,
});
setIsDraggedOver(closestEdgeOfTarget); setIsDraggedOver(closestEdgeOfTarget);
}, },
onDragLeave: () => { onDragLeave: () => {
drop.onDragLeave?.();
setIsDraggedOver(null); setIsDraggedOver(null);
}, },
onDrop: (args) => { onDrop: (args) => {
-33
View File
@@ -1,33 +0,0 @@
import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
import { useEffect } from 'react';
import { DragData } from '/@/shared/types/drag-and-drop';
interface UseDragMonitorProps {
canMonitor?: (source: DragData) => boolean;
isEnabled?: boolean;
onDragStart?: (source: DragData) => void;
onDrop?: () => void;
}
export const useDragMonitor = ({
canMonitor,
isEnabled = true,
onDragStart,
onDrop,
}: UseDragMonitorProps) => {
useEffect(() => {
if (!isEnabled) return;
return monitorForElements({
onDragStart: ({ source }) => {
const data = source.data as unknown as DragData;
if (canMonitor && !canMonitor(data)) return;
onDragStart?.(data);
},
onDrop: () => {
onDrop?.();
},
});
}, [canMonitor, isEnabled, onDragStart, onDrop]);
};
+17 -140
View File
@@ -1,73 +1,7 @@
import type { PlayerFilter, SettingsState } from './settings.store'; import type { 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',
@@ -109,8 +43,6 @@ const FONT_TYPES = new Set(['builtIn', 'custom', 'system']);
const HOME_FEATURE_STYLES = new Set(['multiple', 'single']); const HOME_FEATURE_STYLES = new Set(['multiple', 'single']);
const SIDE_QUEUE_TYPES = new Set(['sideDrawerQueue', 'sideQueue']); const SIDE_QUEUE_TYPES = new Set(['sideDrawerQueue', 'sideQueue']);
const SIDE_QUEUE_LAYOUTS = new Set(['horizontal', 'vertical']); const SIDE_QUEUE_LAYOUTS = new Set(['horizontal', 'vertical']);
const SIDEBAR_PLAYLIST_FOLDER_VIEWS = new Set(['navigation', 'single', 'tree']);
const SIDEBAR_PLAYLIST_MODES = new Set(['compact', 'expanded']);
export type EnvSettingsOverrides = DeepPartial< export type EnvSettingsOverrides = DeepPartial<
Pick<SettingsState, 'autoDJ' | 'css' | 'discord' | 'font' | 'general' | 'lyrics' | 'playback'> Pick<SettingsState, 'autoDJ' | 'css' | 'discord' | 'font' | 'general' | 'lyrics' | 'playback'>
@@ -123,29 +55,28 @@ type DeepPartial<T> = {
interface EnvSettingSpec { interface EnvSettingSpec {
enumSet?: Set<string>; enumSet?: Set<string>;
key: string; key: string;
path: readonly string[]; path: [string, string, string] | [string, 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(obj: EnvSettingsOverrides, path: readonly string[], value: unknown): void { function setAtPath(
if (path.length < 2) return; obj: EnvSettingsOverrides,
let cur: Record<string, unknown> = obj as Record<string, unknown>; path: [string, string, string] | [string, string],
for (let i = 0; i < path.length - 1; i++) { value: unknown,
const key = path[i]!; ): void {
const existing = cur[key]; const [a, b, c] = path;
const next: Record<string, unknown> = const root = (obj as Record<string, unknown>)[a] ?? {};
existing !== null && (obj as Record<string, unknown>)[a] = root;
existing !== undefined && const branch = root as Record<string, unknown>;
typeof existing === 'object' && if (c === undefined) {
!Array.isArray(existing) branch[b] = value;
? { ...(existing as Record<string, unknown>) } } else {
: {}; const nested = branch[b] ?? {};
cur[key] = next; branch[b] = nested;
cur = next; (nested as Record<string, unknown>)[c] = value;
} }
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*\)$/;
@@ -258,48 +189,11 @@ const ENV_SETTING_SPECS: EnvSettingSpec[] = [
path: ['general', 'sidebarCollapseShared'], path: ['general', 'sidebarCollapseShared'],
type: 'bool', type: 'bool',
}, },
{
key: 'FS_GENERAL_SIDEBAR_PLAYLIST_FOLDERS',
path: ['general', 'sidebarPlaylistFolders'],
type: 'bool',
},
{
key: 'FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_SEPARATOR',
path: ['general', 'sidebarPlaylistFolderSeparator'],
skipIfEmpty: true,
type: 'string',
},
{
key: 'FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_TREE_INDENT',
path: ['general', 'sidebarPlaylistFolderTreeIndent'],
transform: (s) => {
const n = parseNum(s);
return n !== undefined ? Math.min(64, Math.max(0, Math.round(n))) : undefined;
},
type: 'num',
},
{
key: 'FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_TREE_LINE_COLOR',
path: ['general', 'sidebarPlaylistFolderTreeLineColor'],
type: 'string',
},
{
enumSet: SIDEBAR_PLAYLIST_FOLDER_VIEWS,
key: 'FS_GENERAL_SIDEBAR_PLAYLIST_FOLDER_VIEW',
path: ['general', 'sidebarPlaylistFolderView'],
type: 'enum',
},
{ {
key: 'FS_GENERAL_SIDEBAR_PLAYLIST_LIST', key: 'FS_GENERAL_SIDEBAR_PLAYLIST_LIST',
path: ['general', 'sidebarPlaylistList'], path: ['general', 'sidebarPlaylistList'],
type: 'bool', type: 'bool',
}, },
{
enumSet: SIDEBAR_PLAYLIST_MODES,
key: 'FS_GENERAL_SIDEBAR_PLAYLIST_MODE',
path: ['general', 'sidebarPlaylistMode'],
type: 'enum',
},
{ {
key: 'FS_GENERAL_SIDEBAR_PLAYLIST_SORTING', key: 'FS_GENERAL_SIDEBAR_PLAYLIST_SORTING',
path: ['general', 'sidebarPlaylistSorting'], path: ['general', 'sidebarPlaylistSorting'],
@@ -358,23 +252,6 @@ 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,7 +2,6 @@ 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';
-9
View File
@@ -7,7 +7,6 @@ import { useShallow } from 'zustand/react/shallow';
import { createWithEqualityFn } from 'zustand/traditional'; import { createWithEqualityFn } from 'zustand/traditional';
import { eventEmitter } from '/@/renderer/events/event-emitter'; import { eventEmitter } from '/@/renderer/events/event-emitter';
import { useRadioStore as useRadioPlayerStore } from '/@/renderer/features/radio/hooks/use-radio-player';
import { createSelectors } from '/@/renderer/lib/zustand'; import { createSelectors } from '/@/renderer/lib/zustand';
import { useSettingsStore } from '/@/renderer/store/settings.store'; import { useSettingsStore } from '/@/renderer/store/settings.store';
import { import {
@@ -478,10 +477,6 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
break; break;
} }
case Play.NOW: { case Play.NOW: {
if (useRadioPlayerStore.getState().currentStreamUrl) {
useRadioPlayerStore.getState().actions.stop();
}
set((state) => { set((state) => {
newItems.forEach((item) => { newItems.forEach((item) => {
state.queue.songs[item._uniqueId] = item; state.queue.songs[item._uniqueId] = item;
@@ -536,10 +531,6 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
break; break;
} }
case Play.SHUFFLE: { case Play.SHUFFLE: {
if (useRadioPlayerStore.getState().currentStreamUrl) {
useRadioPlayerStore.getState().actions.stop();
}
set((state) => { set((state) => {
newItems.forEach((item) => { newItems.forEach((item) => {
state.queue.songs[item._uniqueId] = item; state.queue.songs[item._uniqueId] = item;
@@ -1,45 +0,0 @@
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 },
}));
};
-37
View File
@@ -160,7 +160,6 @@ const BindingActionsSchema = z.enum([
'listPlayNext', 'listPlayNext',
'listPlayLast', 'listPlayLast',
'listNavigateToPage', 'listNavigateToPage',
'listShowPlayingSong',
]); ]);
const DiscordDisplayTypeSchema = z.enum(['artist', 'feishin', 'song']); const DiscordDisplayTypeSchema = z.enum(['artist', 'feishin', 'song']);
@@ -176,10 +175,6 @@ const SideQueueLayoutSchema = z.enum(['horizontal', 'vertical']);
const SidebarPanelTypeSchema = z.enum(['queue', 'lyrics', 'visualizer']); const SidebarPanelTypeSchema = z.enum(['queue', 'lyrics', 'visualizer']);
const SidebarPlaylistFolderViewSchema = z.enum(['single', 'tree', 'navigation']);
const SidebarPlaylistModeSchema = z.enum(['compact', 'expanded']);
const CollectionSchema = z.object({ const CollectionSchema = z.object({
filterQueryString: z.string(), filterQueryString: z.string(),
id: z.string(), id: z.string(),
@@ -505,14 +500,8 @@ export const GeneralSettingsSchema = z.object({
sidebarCollapseShared: z.boolean(), sidebarCollapseShared: z.boolean(),
sidebarItems: z.array(SidebarItemTypeSchema), sidebarItems: z.array(SidebarItemTypeSchema),
sidebarPanelOrder: z.array(SidebarPanelTypeSchema), sidebarPanelOrder: z.array(SidebarPanelTypeSchema),
sidebarPlaylistFolders: z.boolean(),
sidebarPlaylistFolderSeparator: z.string().min(1),
sidebarPlaylistFolderTreeIndent: z.number().int().min(0).max(64),
sidebarPlaylistFolderTreeLineColor: z.string(),
sidebarPlaylistFolderView: SidebarPlaylistFolderViewSchema,
sidebarPlaylistList: z.boolean(), sidebarPlaylistList: z.boolean(),
sidebarPlaylistListFilterRegex: z.string(), sidebarPlaylistListFilterRegex: z.string(),
sidebarPlaylistMode: SidebarPlaylistModeSchema,
sidebarPlaylistSorting: z.boolean(), sidebarPlaylistSorting: z.boolean(),
sideQueueLayout: SideQueueLayoutSchema, sideQueueLayout: SideQueueLayoutSchema,
sideQueueType: SideQueueTypeSchema, sideQueueType: SideQueueTypeSchema,
@@ -767,7 +756,6 @@ export enum BindingActions {
LIST_PLAY_LAST = 'listPlayLast', LIST_PLAY_LAST = 'listPlayLast',
LIST_PLAY_NEXT = 'listPlayNext', LIST_PLAY_NEXT = 'listPlayNext',
LIST_PLAY_NOW = 'listPlayNow', LIST_PLAY_NOW = 'listPlayNow',
LIST_SHOW_PLAYING_SONG = 'listShowPlayingSong',
LOCAL_SEARCH = 'localSearch', LOCAL_SEARCH = 'localSearch',
MUTE = 'volumeMute', MUTE = 'volumeMute',
NAVIGATE_HOME = 'navigateHome', NAVIGATE_HOME = 'navigateHome',
@@ -1181,14 +1169,8 @@ const initialState: SettingsState = {
sidebarCollapseShared: false, sidebarCollapseShared: false,
sidebarItems, sidebarItems,
sidebarPanelOrder: ['queue', 'lyrics', 'visualizer'], sidebarPanelOrder: ['queue', 'lyrics', 'visualizer'],
sidebarPlaylistFolders: true,
sidebarPlaylistFolderSeparator: '/',
sidebarPlaylistFolderTreeIndent: 16,
sidebarPlaylistFolderTreeLineColor: '',
sidebarPlaylistFolderView: 'tree',
sidebarPlaylistList: true, sidebarPlaylistList: true,
sidebarPlaylistListFilterRegex: '', sidebarPlaylistListFilterRegex: '',
sidebarPlaylistMode: 'expanded',
sidebarPlaylistSorting: false, sidebarPlaylistSorting: false,
sideQueueLayout: 'horizontal', sideQueueLayout: 'horizontal',
sideQueueType: 'sideQueue', sideQueueType: 'sideQueue',
@@ -1223,7 +1205,6 @@ const initialState: SettingsState = {
listPlayLast: { allowGlobal: false, hotkey: '', isGlobal: false }, listPlayLast: { allowGlobal: false, hotkey: '', isGlobal: false },
listPlayNext: { allowGlobal: false, hotkey: '', isGlobal: false }, listPlayNext: { allowGlobal: false, hotkey: '', isGlobal: false },
listPlayNow: { allowGlobal: false, hotkey: '', isGlobal: false }, listPlayNow: { allowGlobal: false, hotkey: '', isGlobal: false },
listShowPlayingSong: { allowGlobal: false, hotkey: 'mod+l', isGlobal: false },
localSearch: { allowGlobal: false, hotkey: 'mod+f', isGlobal: false }, localSearch: { allowGlobal: false, hotkey: 'mod+f', isGlobal: false },
navigateHome: { allowGlobal: false, hotkey: '', isGlobal: false }, navigateHome: { allowGlobal: false, hotkey: '', isGlobal: false },
next: { allowGlobal: true, hotkey: '', isGlobal: false }, next: { allowGlobal: true, hotkey: '', isGlobal: false },
@@ -2571,27 +2552,9 @@ export const useCollections = () => {
); );
}; };
export const useSidebarPlaylistFolders = () =>
useSettingsStore((state) => state.general.sidebarPlaylistFolders, shallow);
export const useSidebarPlaylistFolderSeparator = () =>
useSettingsStore((state) => state.general.sidebarPlaylistFolderSeparator, shallow);
export const useSidebarPlaylistFolderView = () =>
useSettingsStore((state) => state.general.sidebarPlaylistFolderView, shallow);
export const useSidebarPlaylistFolderTreeIndent = () =>
useSettingsStore((state) => state.general.sidebarPlaylistFolderTreeIndent, shallow);
export const useSidebarPlaylistFolderTreeLineColor = () =>
useSettingsStore((state) => state.general.sidebarPlaylistFolderTreeLineColor, shallow);
export const useSidebarPlaylistList = () => export const useSidebarPlaylistList = () =>
useSettingsStore((state) => state.general.sidebarPlaylistList, shallow); useSettingsStore((state) => state.general.sidebarPlaylistList, shallow);
export const useSidebarPlaylistMode = () =>
useSettingsStore((state) => state.general.sidebarPlaylistMode, shallow);
export const useSidebarPlaylistSorting = () => export const useSidebarPlaylistSorting = () =>
useSettingsStore((state) => state.general.sidebarPlaylistSorting, shallow); useSettingsStore((state) => state.general.sidebarPlaylistSorting, shallow);
+1 -2
View File
@@ -518,9 +518,8 @@ export const sortAlbumList = (albums: Album[], sortBy: AlbumListSort, sortOrder:
} }
return 0; return 0;
}, },
(v) => v.name.toLowerCase(),
], ],
[order, order, 'asc'], [order, order],
); );
break; break;
case AlbumListSort.SONG_COUNT: case AlbumListSort.SONG_COUNT:
@@ -79,10 +79,6 @@ img.size-5xl {
color: rgb(255 49 49); color: rgb(255 49 49);
} }
.color-transparent {
color: transparent;
}
.fill { .fill {
fill: transparent; fill: transparent;
} }
@@ -127,10 +123,6 @@ 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;
} }
+2 -13
View File
@@ -34,10 +34,7 @@ import {
LuChevronLast, LuChevronLast,
LuChevronLeft, LuChevronLeft,
LuChevronRight, LuChevronRight,
LuChevronsDownUp,
LuChevronsUpDown,
LuChevronUp, LuChevronUp,
LuCircle,
LuCircleCheck, LuCircleCheck,
LuCircleX, LuCircleX,
LuClipboardCopy, LuClipboardCopy,
@@ -54,7 +51,6 @@ import {
LuExternalLink, LuExternalLink,
LuFileJson, LuFileJson,
LuFlag, LuFlag,
LuFolderClosed,
LuFolderOpen, LuFolderOpen,
LuGauge, LuGauge,
LuGithub, LuGithub,
@@ -251,9 +247,7 @@ export const AppIcon = {
brandSpotify: SpotifyLogoIcon, brandSpotify: SpotifyLogoIcon,
cache: LuCloudDownload, cache: LuCloudDownload,
check: LuCheck, check: LuCheck,
circle: LuCircle,
clipboardCopy: LuClipboardCopy, clipboardCopy: LuClipboardCopy,
collapseAll: LuChevronsDownUp,
collection: LuPackage2, collection: LuPackage2,
delete: LuTrash, delete: LuTrash,
disc: LuDisc, disc: LuDisc,
@@ -273,13 +267,11 @@ export const AppIcon = {
emptySongImage: LuMusic, emptySongImage: LuMusic,
error: LuShieldAlert, error: LuShieldAlert,
expand: LuExpand, expand: LuExpand,
expandAll: LuChevronsUpDown,
externalLink: LuExternalLink, externalLink: LuExternalLink,
favorite: LuHeart, favorite: LuHeart,
fileJson: LuFileJson, fileJson: LuFileJson,
filter: LuListFilter, filter: LuListFilter,
folder: LuFolderOpen, folder: LuFolderOpen,
folderClosed: LuFolderClosed,
genre: LuFlag, genre: LuFlag,
goToItem: LuCornerDownRight, goToItem: LuCornerDownRight,
hash: LuHash, hash: LuHash,
@@ -392,7 +384,6 @@ 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) => {
@@ -400,19 +391,17 @@ 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-${colorClassToken}`]]: colorClassToken, [styles[`color-${color || fill}`]]: color || fill,
[styles[`fill-${fill}`]]: fill, [styles[`fill-${fill}`]]: fill,
[styles[`size-${size}`]]: true, [styles[`size-${size}`]]: true,
}), }),
[animate, className, colorClassToken, fill, size], [animate, className, color, fill, size],
); );
return ( return (
+7 -14
View File
@@ -60,7 +60,7 @@ export function BaseImage({
enableAnimation = false, enableAnimation = false,
enableDebounce = false, enableDebounce = false,
enableViewport = true, enableViewport = true,
fetchPriority = 'low', fetchPriority,
imageContainerProps, imageContainerProps,
imageRequest, imageRequest,
includeLoader = true, includeLoader = true,
@@ -89,18 +89,11 @@ export function BaseImage({
const effectiveImageRequest = const effectiveImageRequest =
isInSessionCache || !enableDebounce ? rawImageRequest : debouncedImageRequest; isInSessionCache || !enableDebounce ? rawImageRequest : debouncedImageRequest;
const imageCacheKey = src ?? rawImageRequest?.cacheKey;
const trackLoadedInInstance = enableViewport;
const [hasLoadedInInstance, setHasLoadedInInstance] = useState(false); const [hasLoadedInInstance, setHasLoadedInInstance] = useState(false);
useEffect(() => { useEffect(() => {
if (!trackLoadedInInstance) { setHasLoadedInInstance(false);
return; }, [effectiveImageRequest?.cacheKey]);
}
setHasLoadedInInstance((current) => (current ? false : current));
}, [imageCacheKey, trackLoadedInInstance]);
const shouldLoadImage = Boolean( const shouldLoadImage = Boolean(
effectiveImageRequest && effectiveImageRequest &&
@@ -119,13 +112,13 @@ export function BaseImage({
}); });
useEffect(() => { useEffect(() => {
if (!trackLoadedInInstance || !nativeImage.isLoaded || !imageCacheKey) { if (!nativeImage.isLoaded || !effectiveImageRequest?.cacheKey) {
return; return;
} }
loadedImageCacheKeys.add(imageCacheKey); loadedImageCacheKeys.add(effectiveImageRequest.cacheKey);
setHasLoadedInInstance((current) => (current ? current : true)); setHasLoadedInInstance(true);
}, [imageCacheKey, nativeImage.isLoaded, trackLoadedInInstance]); }, [effectiveImageRequest?.cacheKey, nativeImage.isLoaded]);
return ( return (
<ImageContainer <ImageContainer
@@ -26,11 +26,8 @@ export function useNativeImage({
const loadedRequestSignatureRef = useRef<null | string>(null); const loadedRequestSignatureRef = useRef<null | string>(null);
const objectUrlRef = useRef<null | string>(null); const objectUrlRef = useRef<null | string>(null);
const onFetchErrorRef = useRef(onFetchError); const onFetchErrorRef = useRef(onFetchError);
const requestRef = useRef(request);
const [state, setState] = useState<NativeImageState>({ status: 'idle' }); const [state, setState] = useState<NativeImageState>({ status: 'idle' });
requestRef.current = request;
const requestSignature = useMemo(() => { const requestSignature = useMemo(() => {
if (!request) { if (!request) {
return null; return null;
@@ -47,8 +44,6 @@ export function useNativeImage({
onFetchErrorRef.current = onFetchError; onFetchErrorRef.current = onFetchError;
useEffect(() => { useEffect(() => {
const request = requestRef.current;
const abortCurrentRequest = () => { const abortCurrentRequest = () => {
abortControllerRef.current?.abort(); abortControllerRef.current?.abort();
abortControllerRef.current = null; abortControllerRef.current = null;
@@ -67,61 +62,28 @@ export function useNativeImage({
if (!request || !requestSignature) { if (!request || !requestSignature) {
abortCurrentRequest(); abortCurrentRequest();
revokeObjectUrl(); revokeObjectUrl();
setState((currentState) => { setState({ status: 'idle' });
if (currentState.status === 'idle' && !currentState.displaySrc) {
return currentState;
}
return { status: 'idle' };
});
return; return;
} }
if (!enabled) { if (!enabled) {
abortCurrentRequest(); abortCurrentRequest();
setState((currentState) => { setState((currentState) =>
if (currentState.displaySrc) { currentState.displaySrc
if (currentState.status === 'loaded') { ? { ...currentState, status: 'loaded' }
return currentState; : { status: 'idle' },
} );
return { ...currentState, status: 'loaded' };
}
if (currentState.status === 'idle' && !currentState.displaySrc) {
return currentState;
}
return { status: 'idle' };
});
return; return;
} }
if (loadedRequestSignatureRef.current === requestSignature && objectUrlRef.current) { if (loadedRequestSignatureRef.current === requestSignature && objectUrlRef.current) {
const cachedObjectUrl = objectUrlRef.current; setState({ displaySrc: objectUrlRef.current, status: 'loaded' });
setState((currentState) => {
if (
currentState.status === 'loaded' &&
currentState.displaySrc === cachedObjectUrl
) {
return currentState;
}
return { displaySrc: cachedObjectUrl, status: 'loaded' };
});
return; return;
} }
abortCurrentRequest(); abortCurrentRequest();
revokeObjectUrl(); revokeObjectUrl();
setState((currentState) => { setState({ status: 'loading' });
if (currentState.status === 'loading' && !currentState.displaySrc) {
return currentState;
}
return { status: 'loading' };
});
const abortController = new AbortController(); const abortController = new AbortController();
abortControllerRef.current = abortController; abortControllerRef.current = abortController;
@@ -176,7 +138,7 @@ export function useNativeImage({
abortControllerRef.current = null; abortControllerRef.current = null;
} }
}; };
}, [enabled, fetchPriority, requestSignature]); }, [enabled, fetchPriority, request, requestSignature]);
useEffect(() => { useEffect(() => {
return () => { return () => {
@@ -1,7 +0,0 @@
.root {
width: 100%;
}
.section {
transition: width 0.15s ease-out;
}
@@ -1,29 +0,0 @@
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';
-1
View File
@@ -12,7 +12,6 @@ export enum DragTarget {
GRID_ROW = 'gridRow', GRID_ROW = 'gridRow',
PLAYLIST = LibraryItem.PLAYLIST, PLAYLIST = LibraryItem.PLAYLIST,
QUEUE_SONG = LibraryItem.QUEUE_SONG, QUEUE_SONG = LibraryItem.QUEUE_SONG,
SIDEBAR_PLAYLIST_FOLDER = 'sidebarPlaylistFolder',
SONG = LibraryItem.SONG, SONG = LibraryItem.SONG,
TABLE_COLUMN = 'tableColumn', TABLE_COLUMN = 'tableColumn',
} }