Compare commits

..

6 Commits

Author SHA1 Message Date
jeffvli f02307ff2a move search button to top right of LibraryHeader 2026-02-11 21:32:43 -08:00
jeffvli 2647c36326 add compact styling to LibraryHeader 2026-02-11 21:31:17 -08:00
jeffvli 16a9d6e702 update client side song ordering to include album order 2026-02-11 20:01:00 -08:00
jeffvli 0a4d789f08 maintain song order in album view 2026-02-11 20:00:46 -08:00
jeffvli 7f5742119b refactor playlist route state 2026-02-11 18:43:28 -08:00
jeffvli 04d8e013e1 add initial playlist album view 2026-02-11 14:22:47 -08:00
35 changed files with 189 additions and 660 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 645 B

After

Width:  |  Height:  |  Size: 820 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.
+5 -31
View File
@@ -29,11 +29,7 @@
"topSongsFrom": "les millors cançons de {{title}}",
"viewAll": "mostra-ho tot",
"groupingTypeAll": "tots els tipus de llançaments",
"groupingTypePrimary": "tipus principals de llançament",
"favoriteSongs": "Cançons preferides",
"topSongsCommunity": "comunitat",
"topSongsPersonal": "personal",
"favoriteSongsFrom": "cançons preferides de {{title}}"
"groupingTypePrimary": "tipus principals de llançament"
},
"albumArtistList": {
"title": "$t(entity.albumArtist, {\"count\": 2})"
@@ -204,11 +200,6 @@
"collections": {
"overrideExisting": "sobreescriu existents",
"saveAsCollection": "desa com a col·lecció"
},
"releasenotes": {
"commitsSinceStable": "commits des de {{stable}}",
"noNewCommits": "no hi ha hagut commits en aquest període",
"noStableReleaseToCompare": "no hi ha actualitzacions disponibles amb les quals comparar"
}
},
"common": {
@@ -788,7 +779,7 @@
"releaseChannel_optionLatest": "última versió",
"releaseChannel_optionBeta": "beta",
"releaseChannel": "canal de versions",
"releaseChannel_description": "trieu entre versions estables i beta o alfa (diàries) per les actualitzacions automàtiques",
"releaseChannel_description": "tria entre versions estables i versions beta per les actualitzacions automàtiques",
"mediaSession": "activa Media Session",
"mediaSession_description": "activa la integració amb Media Session per mostrar els controls multimèdia i les metadades a l'indicador de volum del sistema i la pantalla de bloqueig",
"crossfadeStyle": "estil de fosa encadenada",
@@ -887,14 +878,7 @@
"sidebarPlaylistSorting": "ordenació de llistes de reproducció de la barra lateral",
"sidebarPlaylistListFilterRegex_description": "amaga les llistes de reproducció de la barra lateral que coincideixin amb aquesta expressió regular",
"sidebarPlaylistListFilterRegex_placeholder": "ex. ^Mescla diària.*",
"sidebarPlaylistListFilterRegex": "regex pel filtre de llistes",
"analyticsEnable": "envia analítiques basades en l'ús",
"analyticsEnable_description": "s'envien dades d'ús anonimitzades al desenvolupar per ajudar a millorar l'aplicació",
"automaticUpdates": "actualitzacions automàtiques",
"automaticUpdates_description": "cerca i instal·la actualitzacions automàticament",
"releaseChannel_optionAlpha": "alfa (diària)",
"blurExplicitImages": "desenfoca imatges explícites",
"blurExplicitImages_description": "les caràtules d'àlbums i cançons marcades com a explícites quedaran desenfocades"
"sidebarPlaylistListFilterRegex": "regex pel filtre de llistes"
},
"table": {
"column": {
@@ -999,8 +983,7 @@
"view": {
"table": "taula",
"grid": "quadrícula",
"list": "llista",
"detail": "detall"
"list": "llista"
}
}
},
@@ -1089,16 +1072,7 @@
"restoreQueueFromServer": "restaura la cua del servidor",
"saveQueueToServer": "desa la cua al servidor",
"artistRadio": "ràdio de l'artista",
"trackRadio": "ràdio de la pista",
"sleepTimer": "temporitzador d'adormir",
"sleepTimer_endOfSong": "final de la cançó actual",
"sleepTimer_minutes": "{{count}} min",
"sleepTimer_hours": "{{count}} h",
"sleepTimer_custom": "personalitzat",
"sleepTimer_off": "apagat",
"sleepTimer_timeRemaining": "queden {{time}}",
"sleepTimer_setCustom": "configura el temporitzador",
"sleepTimer_cancel": "cancel·la el temporitzador"
"trackRadio": "ràdio de la pista"
},
"error": {
"credentialsRequired": "credencials requerides",
+5 -21
View File
@@ -38,16 +38,7 @@
"restoreQueueFromServer": "obnovit frontu ze serveru",
"saveQueueToServer": "uložit frontu na server",
"artistRadio": "rádio umělce",
"trackRadio": "rádio skladby",
"sleepTimer": "časovač spánku",
"sleepTimer_endOfSong": "konec aktuální skladby",
"sleepTimer_minutes": "{{count}} min.",
"sleepTimer_hours": "{{count}} hod.",
"sleepTimer_custom": "vlastní",
"sleepTimer_off": "vypnuto",
"sleepTimer_timeRemaining": "zbývá {{time}}",
"sleepTimer_setCustom": "nastavit časovač",
"sleepTimer_cancel": "zrušit časovač"
"trackRadio": "rádio skladby"
},
"setting": {
"crossfadeStyle_description": "vyberte způsob prolnutí u přehrávače zvuku",
@@ -55,7 +46,7 @@
"hotkey_skipBackward": "přeskočení zpět",
"replayGainMode_description": "úprava zesílení hlasitosti podle hodnot {{ReplayGain}} uložených v metadatech souborů",
"volumeWheelStep_description": "počet procent, o které má být hlasitost posunuta při přejetí kolečkem myši na posuvníku hlasitosti",
"audioDevice_description": "vyberte zvukové zařízení k přehrávání",
"audioDevice_description": "vyberte zvukové zařízení k přehrávání (pouze webový přehrávač)",
"theme_description": "nastavení motivu použitého v aplikaci",
"hotkey_playbackPause": "pozastavení",
"replayGainFallback": "fallback {{ReplayGain}}",
@@ -270,7 +261,7 @@
"neteaseTranslation_description": "Pokud je povoleno, načte a zobrazí přeložené texty ze služby NetEase, pokud jsou dostupné",
"preferLocalLyrics": "preferovat místní texty",
"preferLocalLyrics_description": "preferovat místní texty před vzdálenými, pokud jsou dostupné",
"discordPausedStatus": "zobrazit stav při pozastavení",
"discordPausedStatus": "zobrazit rich presence při pozastavení",
"discordPausedStatus_description": "pokud je povoleno, bude při pozastavení přehrávače zobrazen stav",
"preservePitch": "zachovat výšku",
"preservePitch_description": "zachová výšku při úpravě rychlosti přehrávání",
@@ -394,13 +385,7 @@
"sidebarPlaylistListFilterRegex_description": "v postranní liště skrýt seznamy skladeb, které odpovídají tomuto regulárnímu výrazu",
"sidebarPlaylistListFilterRegex_placeholder": "např. ^Denní mix.*",
"sidebarPlaylistListFilterRegex": "regulární výraz filtru seznamů skladeb",
"releaseChannel_optionAlpha": "alpha (noční)",
"analyticsEnable": "Posílat analytiku založenou na využití",
"analyticsEnable_description": "Anonymizovaná data o používání jsou odesílána vývojáři za účelem zlepšení aplikace",
"automaticUpdates": "Automatické aktualizace",
"automaticUpdates_description": "Kontrolovat a automaticky instalovat aktualizace",
"discordStateIcon": "zobrazit ikonu přehrávání",
"discordStateIcon_description": "zobrazit malou ikonu přehrávání ve stavu na Discordu. ikona pozastavení bude zobrazena vždy, když je povolena možnost „Zobrazit stav při pozastavení“"
"releaseChannel_optionAlpha": "alpha (noční)"
},
"action": {
"editPlaylist": "upravit $t(entity.playlist, {\"count\": 1})",
@@ -573,8 +558,7 @@
"view": {
"table": "tabulka",
"list": "seznam",
"grid": "mřížka",
"detail": "podrobnosti"
"grid": "mřížka"
},
"general": {
"displayType": "typ zobrazení",
+1 -3
View File
@@ -734,7 +734,7 @@
"artistConfiguration_description": "configure what items are shown, and in what order, on the album artist page",
"artistReleaseTypeConfiguration": "artist release type configuration",
"artistReleaseTypeConfiguration_description": "configure what release types are shown, and in what order, on the album artist page",
"audioDevice_description": "select the audio device to use for playback",
"audioDevice_description": "select the audio device to use for playback (web player only)",
"audioDevice": "audio device",
"audioExclusiveMode_description": "enable exclusive output mode. In this mode, the system is usually locked out, and only mpv will be able to output audio",
"audioExclusiveMode": "audio exclusive mode",
@@ -788,8 +788,6 @@
"discordRichPresence_description": "enable playback status in {{discord}} rich presence. Image keys are: {{icon}}, {{playing}}, and {{paused}}",
"discordServeImage": "serve {{discord}} images from server",
"discordServeImage_description": "share cover art for {{discord}} rich presence from server itself, only available for Jellyfin and Navidrome. {{discord}} uses a bot to fetch images, so your server must be reachable from the public internet",
"discordStateIcon": "show playing icon",
"discordStateIcon_description": "show a small playing icon in the rich presence status. the paused icon is always shown when \"Show rich presence when paused\" is enabled",
"discordUpdateInterval": "{{discord}} rich presence update interval",
"discordUpdateInterval_description": "the time in seconds between each update (minimum 15 seconds)",
"enableAutoTranslation_description": "enable translation automatically when lyrics are loaded",
+21 -37
View File
@@ -32,22 +32,13 @@
"playSimilarSongs": "Reproducir canciones similares",
"viewQueue": "ver cola",
"addLastShuffled": "Al final (mezclado)",
"addNextShuffled": "Siguiente (mezclado)",
"addNextShuffled": "Al siguiente (mezclado)",
"holdToShuffle": "Mantener para mezclar",
"lyrics": "Letras",
"restoreQueueFromServer": "Restaurar cola del servidor",
"saveQueueToServer": "Guardar cola en el servidor",
"artistRadio": "Radio de artista",
"trackRadio": "Radio de pista",
"sleepTimer_minutes": "{{count}} min",
"sleepTimer_hours": "{{count}} h",
"sleepTimer_custom": "Personalizado",
"sleepTimer_setCustom": "Configurar temporizador",
"sleepTimer_cancel": "Cancelar temporizador",
"sleepTimer_timeRemaining": "{{time}} restante",
"sleepTimer_off": "Apagado",
"sleepTimer_endOfSong": "Fin de la canción actual",
"sleepTimer": "Temporizador de apagado"
"trackRadio": "Radio de pista"
},
"setting": {
"crossfadeStyle_description": "selecciona el estilo de crossfade a usar por el reproductor de audio",
@@ -168,7 +159,7 @@
"customFontPath": "ruta de fuente personalizada",
"followLyric": "seguir la letra actual",
"crossfadeDuration": "duración del crossfade",
"discordIdleStatus": "mostrar estado inactivo en el estado de actividad",
"discordIdleStatus": "mostrar el estado inactivo en el estado de actividad",
"sidePlayQueueStyle_optionDetached": "separada",
"audioPlayer": "reproductor de audio",
"hotkey_zoomOut": "reducir",
@@ -327,8 +318,8 @@
"playerbarWaveformRadius": "Radio de la forma de onda",
"showLyricsInSidebar_description": "Se añadirá un panel a la cola de reproducción acoplada que muestra las letras",
"showLyricsInSidebar": "Mostrar letras en la barra lateral del reproductor",
"showVisualizerInSidebar_description": "Se añadirá un panel a la barra lateral del reproductor que muestra el visualizador",
"showVisualizerInSidebar": "Mostrar visualizador en la barra lateral del reproductor",
"showVisualizerInSidebar_description": "Se añadirá un panel a la barra lateral de reproducción que muestra el visualizador",
"showVisualizerInSidebar": "Mostrar visualizador en la barra lateral de reproducción",
"queryBuilder": "Generador de consultas",
"queryBuilderCustomFields_inputTag": "Etiqueta",
"queryBuilderCustomFields": "Campos personalizados",
@@ -394,13 +385,7 @@
"sidebarPlaylistListFilterRegex_placeholder": "p. ej. ^Mezcla diaria.*",
"blurExplicitImages": "Desenfocar imágenes explícitas",
"blurExplicitImages_description": "El álbum y la carátula de la canción etiquetados como explícitos serán desenfocados",
"releaseChannel_optionAlpha": "Alpha (nightly)",
"analyticsEnable": "Enviar analíticas basadas en el uso",
"analyticsEnable_description": "Se envían datos de uso anonimizados al desarrollador para ayudar a mejorar la aplicación",
"automaticUpdates": "Actualizaciones automáticas",
"automaticUpdates_description": "Busca e instala actualizaciones automáticamente",
"discordStateIcon": "Mostrar icono de reproducción",
"discordStateIcon_description": "Muestra un icono pequeño de reproducción en el estado de actividad. El icono de pausa se muestra siempre cuando \"Mostrar estado de actividad cuando esté en pausa\" esté activado"
"releaseChannel_optionAlpha": "Alpha (nightly)"
},
"action": {
"editPlaylist": "editar $t(entity.playlist, {\"count\": 1})",
@@ -446,7 +431,7 @@
"backward": "hacia atrás",
"increase": "aumentar",
"rating": "calificación",
"bpm": "bpm",
"bpm": "lpm",
"refresh": "actualizar",
"unknown": "desconocido",
"areYouSure": "seguro?",
@@ -458,7 +443,7 @@
"currentSong": "$t(entity.track, {\"count\": 1}) actual",
"collapse": "contraer",
"trackNumber": "pista",
"descending": "descendente",
"descending": "descendiente",
"add": "añadir",
"ascending": "ascendente",
"dismiss": "descartar",
@@ -485,8 +470,8 @@
"cancel": "cancelar",
"forceRestartRequired": "reiniciar para aplicar cambios... cerrar la notificación para reiniciar",
"setting_one": "configuración",
"setting_many": "configuración",
"setting_other": "configuración",
"setting_many": "configuraciones",
"setting_other": "configuraciones",
"version": "versión",
"title": "título",
"filters": "filtros",
@@ -600,10 +585,10 @@
"noNetworkDescription": "No se pudo conectar a este servidor"
},
"filter": {
"mostPlayed": "más reproducidos",
"mostPlayed": "más reproducido",
"isCompilation": "es una compilación",
"recentlyPlayed": "recientemente reproducido",
"isRated": "Está calificado",
"isRated": "es clasificado",
"title": "título",
"rating": "calificación",
"search": "buscar",
@@ -619,7 +604,7 @@
"albumArtist": "$t(entity.albumArtist, {\"count\": 1})",
"isRecentlyPlayed": "reproducido recientemente",
"isFavorited": "es favorito",
"bpm": "bpm",
"bpm": "lpm",
"releaseYear": "año de lanzamiento",
"disc": "disco",
"biography": "biografía",
@@ -638,10 +623,10 @@
"owner": "$t(common.owner)",
"genre": "$t(entity.genre, {\"count\": 1})",
"id": "id",
"songCount": "número de canciones",
"songCount": "número de canción",
"isPublic": "es público",
"album": "$t(entity.album, {\"count\": 1})",
"albumCount": "Número de $t(entity.album, {\"count\": 2})",
"albumCount": "Contar $t(entity.album, {\"count\": 2})",
"explicitStatus": "$t(common.explicitStatus)",
"sortName": "Ordenar por nombre"
},
@@ -880,8 +865,8 @@
"input_name": "nombre del servidor",
"success": "servidor añadido correctamente",
"input_savePassword": "guardar contraseña",
"ignoreSsl": "Ignorar SSL ($t(common.restartRequired))",
"ignoreCors": "Ignorar CORS ($t(common.restartRequired))",
"ignoreSsl": "ignorar ssl ($t(common.restartRequired))",
"ignoreCors": "ignorar cors ($t(common.restartRequired))",
"error_savePassword": "un error ocurrió cuando se intentó guardar la contraseña",
"input_preferInstantMix": "Preferir mix instantáneo",
"input_preferInstantMixDescription": "Usa solo el mix instantáneo para obtener canciones similares. Útil si tienes complementos que modifican este comportamiento",
@@ -979,7 +964,7 @@
"releaseDate": "fecha de lanzamiento",
"bitrate": "tasa de bits",
"title": "título",
"bpm": "bpm",
"bpm": "lpm",
"dateAdded": "fecha de adición",
"artist": "$t(entity.artist, {\"count\": 1})",
"songCount": "$t(entity.track, {\"count\": 2})",
@@ -1044,8 +1029,8 @@
"followCurrentSong": "seguir la canción actual",
"advancedSettings": "Opciones avanzadas",
"autosize": "Autodimensionar",
"moveUp": "Subir",
"moveDown": "Bajar",
"moveUp": "Ascender",
"moveDown": "Descender",
"pinToLeft": "Anclar a la izquierda",
"pinToRight": "Anclar a la derecha",
"alignLeft": "Alinear a la izquierda",
@@ -1068,8 +1053,7 @@
"view": {
"table": "tabla",
"list": "Lista",
"grid": "Cuadrícula",
"detail": "Detalle"
"grid": "Cuadrícula"
}
}
},
+3 -18
View File
@@ -204,8 +204,7 @@
"mood": "humeur",
"retry": "réessayer",
"filter_single": "unique",
"filter_multiple": "multiple",
"rename": "renommer"
"filter_multiple": "multiple"
},
"error": {
"remotePortWarning": "redémarrer le serveur pour appliquer le nouveau port",
@@ -281,8 +280,7 @@
"artist": "$t(entity.artist, {\"count\": 1})",
"isPublic": "est public",
"album": "$t(entity.album, {\"count\": 1})",
"explicitStatus": "$t(common.explicitStatus)",
"sortName": "tri par nom"
"explicitStatus": "$t(common.explicitStatus)"
},
"page": {
"sidebar": {
@@ -449,12 +447,7 @@
"viewDiscography": "voir la discographie",
"relatedArtists": "$t(entity.artist, {\"count\": 2}) similaires",
"topSongs": "meilleurs titres",
"groupingTypeAll": "toutes les types de sortie",
"favoriteSongs": "titres préférées",
"groupingTypePrimary": "types de parution principale",
"topSongsCommunity": "communauté",
"topSongsPersonal": "personnel",
"favoriteSongsFrom": "meilleurs titres de {{title}}"
"groupingTypeAll": "toutes les types de sortie"
},
"itemDetail": {
"copyPath": "copier le chemin dans le presse-papiers",
@@ -480,14 +473,6 @@
},
"radioList": {
"title": "stations radio"
},
"releasenotes": {
"commitsSinceStable": "commits depuis {{stable}}",
"noNewCommits": "pas de nouveaux commits dans cette plage"
},
"windowBar": {
"paused": "(Pause) ",
"privateMode": "(Mode Privé)"
}
},
"setting": {
+4 -20
View File
@@ -661,16 +661,7 @@
"restoreQueueFromServer": "przywróć kolejkę z serwera",
"saveQueueToServer": "zapisz kolejkę na serwerze",
"artistRadio": "radio wykonawcy",
"trackRadio": "radio utworu",
"sleepTimer": "wyłącznik czasowy",
"sleepTimer_endOfSong": "do końca aktualnej piosenki",
"sleepTimer_minutes": "{{count}} min",
"sleepTimer_hours": "{{count}} godz",
"sleepTimer_custom": "niestandardowy",
"sleepTimer_off": "wyłączony",
"sleepTimer_timeRemaining": "pozostało {{time}}",
"sleepTimer_setCustom": "ustaw wyłącznik",
"sleepTimer_cancel": "anuluj wyłączanie"
"trackRadio": "radio utworu"
},
"setting": {
"crossfadeStyle_description": "wybierz styl przenikania, który ma być używany do odtwarzania dźwięku",
@@ -903,7 +894,7 @@
"releaseChannel_optionBeta": "beta",
"releaseChannel_optionLatest": "najnowsza",
"releaseChannel": "kanał wydań",
"releaseChannel_description": "wybieraj pomiędzy wydaniami stabilnymi, beta lub alpha (nightly) dla automatycznych aktualizacji",
"releaseChannel_description": "wybieraj pomiędzy stabilnymi wydaniami a wydaniami beta dla automatycznych aktualizacji",
"discordDisplayType_artistname": "nazwa(y) wykonawców",
"discordDisplayType_description": "zmienia co jest pokazywane jako słuchane w twoim statusie",
"discordDisplayType_songname": "nazwa piosenki",
@@ -1017,21 +1008,14 @@
"sidebarPlaylistListFilterRegex": "filtr playlist regex",
"blurExplicitImages": "rozmazuj nieodpowiednie obrazy",
"blurExplicitImages_description": "obrazy piosenek oraz albumów oznaczone jako nieodpowiednie będą rozmazywane",
"releaseChannel_optionAlpha": "alpha (nightly)",
"analyticsEnable": "Wysyłaj statystyki na podstawie użytkowania",
"analyticsEnable_description": "Zanonimizowane statystki użytkowania będą wysyłane do twórcy, aby pomóc w poprawie aplikacji",
"automaticUpdates": "Aktualizacje automatyczne",
"automaticUpdates_description": "Sprawdzaj i instaluj aktualizacje automatycznie",
"discordStateIcon": "pokaż ikonę odtwarzania",
"discordStateIcon_description": "pokazuje małą ikonę odtwarzania w statusie. ikona pauzy jest zawsze pokazywana gdy \"Pokaż status podczas pauzy\" jest włączone"
"releaseChannel_optionAlpha": "alpha (nightly)"
},
"table": {
"config": {
"view": {
"table": "tabela",
"grid": "siatka",
"list": "lista",
"detail": "szczegół"
"list": "lista"
},
"general": {
"displayType": "typ wyświetlania",
+1 -122
View File
@@ -22,7 +22,7 @@
"holdToMoveToTop": "утримуйте, щоб перемістити вгору",
"holdToMoveToBottom": "утримувати, щоб перемістити вниз",
"moveItems": "перемістити елементи",
"shuffle": "перемішати",
"shuffle": "відтворити випадково",
"shuffleAll": "все випадково",
"shuffleSelected": "вибране випадково",
"refresh": "$t(common.refresh)",
@@ -415,130 +415,9 @@
"success": "посилання для спільного використання скопійовано в буфер обміну (натисніть тут, щоб відкрити)",
"expireInvalid": "термін дії повинен бути в майбутньому",
"createFailed": "не вдалося створити спільний доступ (чи ввімкнено спільний доступ?)"
},
"shuffleAll": {
"title": "відтворити випадково",
"input_genre": "$t(entity.genre, {\"count\": 1})",
"input_limit": "скільки пісень?",
"input_minYear": "від року",
"input_maxYear": "до року",
"input_played": "відтворити фільтр",
"input_played_optionAll": "всі треки",
"input_played_optionUnplayed": "тільки не відтворені треки",
"input_played_optionPlayed": "тільки відтворені треки"
},
"updateServer": {
"success": "сервер успішно оновлено",
"title": "оновити сервер"
},
"privateMode": {
"enabled": "приватний режим увімкнено, стан відтворення тепер приховано від зовнішніх інтеграцій",
"disabled": "приватний режим вимкнено, стан відтворення тепер видно для увімкнених зовнішніх інтеграцій",
"title": "приватний режим"
}
},
"player": {
"skip": "пропустити"
},
"page": {
"albumArtistDetail": {
"about": "Про {{artist}}",
"appearsOn": "з'являється на",
"favoriteSongs": "улюблені пісні",
"groupingTypeAll": "всі типи випуску",
"groupingTypePrimary": "основні типи випуску",
"recentReleases": "останні випуски",
"viewDiscography": "переглянути дискографію",
"relatedArtists": "подібні $t(entity.artist, {\"count\": 2})",
"topSongs": "найкращі пісні",
"topSongsCommunity": "спільнота",
"topSongsFrom": "найкращі пісні від {{title}}",
"topSongsPersonal": "особисте",
"favoriteSongsFrom": "улюблені пісні від {{title}}",
"viewAll": "показати все",
"viewAllTracks": "показати усі $t(entity.track, {\"count\": 2})"
},
"albumArtistList": {
"title": "$t(entity.albumArtist, {\"count\": 2})"
},
"albumDetail": {
"moreFromArtist": "більше від цього $t(entity.artist, {\"count\": 1})",
"moreFromGeneric": "більше від {{item}}",
"released": "видано"
},
"albumList": {
"artistAlbums": "альбоми виконавця {{artist}}",
"genreAlbums": "\"{{genre}}\" $t(entity.album, {\"count\": 2})",
"title": "$t(entity.album, {\"count\": 2})"
},
"radioList": {
"title": "радіостанції"
},
"releasenotes": {
"commitsSinceStable": "комміти від {{stable}}",
"noNewCommits": "немає нових коммітів у цьому періоді",
"noStableReleaseToCompare": "немає доступної стабільної версії для порівняння"
},
"favorites": {
"title": "$t(entity.favorite, {\"count\": 2})"
},
"windowBar": {
"paused": "(Призупинено) ",
"privateMode": "(Приватний режим)"
},
"appMenu": {
"collapseSidebar": "згорнути бічну панель",
"commandPalette": "відкрити палітру команд",
"expandSidebar": "розгорнути бічну панель",
"goBack": "повернутися назад",
"goForward": "перейти вперед",
"manageServers": "управління серверами",
"privateModeOff": "вимкнути приватний режим",
"privateModeOn": "увімкнути приватний режим",
"openBrowserDevtools": "відкрити інструменти розробника",
"quit": "$t(common.quit)",
"selectServer": "вибрати сервер",
"selectMusicFolder": "вибрати папку з музикою",
"noMusicFolder": "не вибрано папку з музикою",
"multipleMusicFolders": "Вибрано {{count}} папок з музикою",
"settings": "$t(common.setting, {\"count\": 2})",
"version": "версія {{version}}"
},
"manageServers": {
"title": "управління серверами",
"serverDetails": "інформація про сервер",
"url": "URL-адреса",
"username": "Ім'я користувача",
"editServerDetailsTooltip": "редагувати дані сервера",
"removeServer": "видалити сервер"
},
"contextMenu": {
"addFavorite": "$t(action.addToFavorites)",
"addLast": "$t(player.addLast)",
"addNext": "$t(player.addNext)",
"addToFavorites": "$t(action.addToFavorites)",
"addToPlaylist": "$t(action.addToPlaylist)",
"createPlaylist": "$t(action.createPlaylist)",
"deletePlaylist": "$t(action.deletePlaylist)",
"deselectAll": "$t(action.deselectAll)",
"download": "завантажити",
"moveItems": "$t(action.moveItems)",
"moveToNext": "$t(action.moveToNext)",
"moveToBottom": "$t(action.moveToBottom)",
"moveToTop": "$t(action.moveToTop)",
"numberSelected": "{{count}} вибрано",
"play": "$t(player.play)",
"playSimilarSongs": "$t(player.playSimilarSongs)",
"removeFromFavorites": "$t(action.removeFromFavorites)",
"removeFromPlaylist": "$t(action.removeFromPlaylist)",
"removeFromQueue": "$t(action.removeFromQueue)",
"setRating": "$t(action.setRating)",
"playShuffled": "$t(player.shuffle)",
"shareItem": "поділитися елементом",
"goTo": "перейти до",
"goToAlbum": "перейти до $t(entity.album, {\"count\": 1})",
"goToAlbumArtist": "перейти до $t(entity.albumArtist, {\"count\": 1})",
"showDetails": "отримати інформацію"
}
}
}
+1 -1
View File
@@ -463,7 +463,7 @@
"releaseChannel_optionLatest": "最新的",
"releaseChannel_optionBeta": "测试版",
"releaseChannel": "发布通道",
"releaseChannel_description": "选择稳定版测试版或 Alpha(夜间构建版)以启用自动更新",
"releaseChannel_description": "选择稳定版本或测试版以进行自动更新",
"mediaSession": "启用媒体会话",
"mediaSession_description": "启用媒体会话集成,在系统音量叠加层和锁屏界面显示媒体控件和元数据",
"exportImportSettings_control_description": "通过 JSON 导出和导入设置",
+3 -19
View File
@@ -399,16 +399,7 @@
"restoreQueueFromServer": "從伺服器還原播放佇列",
"saveQueueToServer": "將播放佇列儲存至伺服器",
"artistRadio": "藝人電台",
"trackRadio": "曲目電台",
"sleepTimer": "睡眠定時器",
"sleepTimer_endOfSong": "歌曲播完時",
"sleepTimer_minutes": "{{count}} 分鐘",
"sleepTimer_hours": "{{count}} 小時",
"sleepTimer_custom": "自訂",
"sleepTimer_off": "關閉",
"sleepTimer_timeRemaining": "剩餘 {{time}}",
"sleepTimer_setCustom": "設定定時器",
"sleepTimer_cancel": "取消定時器"
"trackRadio": "曲目電台"
},
"setting": {
"audioPlayer_description": "選擇用於播放的音訊播放器",
@@ -755,13 +746,7 @@
"sidebarPlaylistListFilterRegex": "播放清單過濾器正規表達式",
"blurExplicitImages": "模糊露骨圖片",
"blurExplicitImages_description": "標記為露骨的專輯和歌曲封面將被模糊",
"releaseChannel_optionAlpha": "alpha (每日建構版)",
"analyticsEnable": "傳送基於使用情況的分析報告",
"analyticsEnable_description": "匿名化的使用情況資料會傳送給開發者,以協助改進應用程式",
"automaticUpdates": "自動更新",
"automaticUpdates_description": "自動檢查並安裝更新",
"discordStateIcon": "顯示播放中圖示",
"discordStateIcon_description": "在 rich presence 狀態中顯示一個小的播放圖示。啟用「暫停時顯示 rich presence」時,會始終顯示暫停的圖示"
"releaseChannel_optionAlpha": "alpha (每日建構版)"
},
"table": {
"config": {
@@ -837,8 +822,7 @@
"view": {
"table": "表格",
"grid": "網格",
"list": "列表",
"detail": "詳情"
"list": "列表"
}
},
"column": {
-9
View File
@@ -55,12 +55,6 @@ const ALPHA_UPDATER_CONFIG: {
provider: 's3',
};
const GITHUB_UPDATER_CONFIG = {
owner: 'jeffvli',
provider: 'github' as const,
repo: 'feishin',
};
type UpdaterInstance = AppImageUpdater | MacUpdater | NsisUpdater | typeof autoUpdater;
class AppUpdater {
@@ -103,7 +97,6 @@ async function checkAllChannelsAndGetBest(): Promise<{
alphaUpdater.allowDowngrade = true;
try {
console.log('Checking for updates on alpha channel');
const alphaResult = await alphaUpdater.checkForUpdates();
if (
alphaResult?.updateInfo?.version &&
@@ -118,9 +111,7 @@ async function checkAllChannelsAndGetBest(): Promise<{
}
try {
autoUpdater.setFeedURL(GITHUB_UPDATER_CONFIG);
configureAutoUpdaterForChannel('latest');
console.log('Checking for updates on latest channel (GitHub)');
const latestResult = await autoUpdater.checkForUpdates();
if (
latestResult?.updateInfo?.version &&
@@ -196,17 +196,6 @@
min-width: 0;
}
.image-wrapper-outer {
position: relative;
display: block;
width: 100%;
aspect-ratio: 1;
}
.image-wrapper-outer.image-wrapper-dragging {
opacity: 0.5;
}
.image-wrapper {
position: relative;
display: block;
@@ -32,17 +32,14 @@ import styles from './item-detail-list.module.css';
import { ItemCardControls } from '/@/renderer/components/item-card/item-card-controls';
import { ItemImage } from '/@/renderer/components/item-image/item-image';
import { getDraggedItems } from '/@/renderer/components/item-list/helpers/get-dragged-items';
import { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls';
import {
ItemListStateActions,
ItemListStateItemWithRequiredProperties,
useItemDraggingState,
useItemListState,
useItemSelectionState,
} from '/@/renderer/components/item-list/helpers/item-list-state';
import { parseTableColumns } from '/@/renderer/components/item-list/helpers/parse-table-columns';
import { useListHotkeys } from '/@/renderer/components/item-list/helpers/use-list-hotkeys';
import { getDetailListCellComponent } from '/@/renderer/components/item-list/item-detail-list/columns';
import {
getTrackColumnFixed,
@@ -64,7 +61,6 @@ import { usePlayer } from '/@/renderer/features/player/context/player-context';
import { useIsMutatingCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
import { useIsMutatingDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
import { useDragDrop } from '/@/renderer/hooks/use-drag-drop';
import { AppRoute } from '/@/renderer/router/routes';
import { useSettingsStore, useShowRatings } from '/@/renderer/store';
import { formatDateAbsoluteUTC, formatDurationString } from '/@/renderer/utils';
@@ -72,8 +68,6 @@ import { SEPARATOR_STRING } from '/@/shared/api/utils';
import { ExplicitIndicator } from '/@/shared/components/explicit-indicator/explicit-indicator';
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
import { useDoubleClick } from '/@/shared/hooks/use-double-click';
import { useFocusWithin } from '/@/shared/hooks/use-focus-within';
import { useMergedRef } from '/@/shared/hooks/use-merged-ref';
import { Album, LibraryItem, Song, SongListSort, SortOrder } from '/@/shared/types/domain-types';
import { dndUtils, DragData, DragOperation, DragTarget } from '/@/shared/types/drag-and-drop';
import { ItemListKey, Play, TableColumn } from '/@/shared/types/types';
@@ -425,61 +419,6 @@ const MetadataSection = memo(
const [isImageHovered, setIsImageHovered] = useState(false);
const [isMetadataHovered, setIsMetadataHovered] = useState(false);
const getId = useCallback(() => {
const draggedItems = getDraggedItems(item, internalState, false);
return draggedItems.map((i) => i.id);
}, [item, internalState]);
const getItem = useCallback(() => {
return getDraggedItems(item, internalState, false);
}, [item, internalState]);
const onDragStart = useCallback(() => {
const draggedItems = getDraggedItems(item, internalState, false);
internalState?.setDragging(draggedItems);
}, [item, internalState]);
const onDrop = useCallback(() => {
internalState?.setDragging([]);
}, [internalState]);
const drag = useMemo(() => {
const playlistSongs = (item as { _playlistSongs?: Song[] })._playlistSongs;
if (playlistSongs && playlistSongs.length > 0) {
return {
getId,
getItem: () => playlistSongs,
itemType: LibraryItem.SONG,
onDragStart,
onDrop,
operation: [DragOperation.ADD],
target: DragTarget.SONG,
};
}
return {
getId,
getItem,
itemType: item._itemType,
onDragStart,
onDrop,
operation: [DragOperation.ADD],
target: DragTarget.ALBUM,
};
}, [getId, getItem, item, onDragStart, onDrop]);
const { isDragging: isDraggingLocal, ref: dragRef } = useDragDrop<HTMLDivElement>({
drag,
isEnabled: !!item,
});
const isDraggingState = useItemDraggingState(internalState, item.id);
const isDragging = isDraggingState || isDraggingLocal;
const handleLinkDragStart = useCallback((e: React.DragEvent<HTMLAnchorElement>) => {
e.preventDefault();
e.stopPropagation();
}, []);
const isFavorite = item.userFavorite ?? false;
const userRating = item.userRating ?? null;
const hasRating = showRatings && userRating !== null && userRating > 0;
@@ -541,48 +480,39 @@ const MetadataSection = memo(
onMouseEnter={() => setIsMetadataHovered(true)}
onMouseLeave={() => setIsMetadataHovered(false)}
>
<div
className={clsx(styles.imageWrapperOuter, {
[styles.imageWrapperDragging]: isDragging,
<Link
className={styles.imageWrapper}
onMouseEnter={() => setIsImageHovered(true)}
onMouseLeave={() => setIsImageHovered(false)}
state={{ item }}
to={generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, {
albumId: item.id,
})}
ref={dragRef ?? undefined}
>
<Link
className={styles.imageWrapper}
draggable={false}
onDragStart={handleLinkDragStart}
onMouseEnter={() => setIsImageHovered(true)}
onMouseLeave={() => setIsImageHovered(false)}
state={{ item }}
to={generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, {
albumId: item.id,
})}
>
<ItemImage
className={styles.image}
explicitStatus={item.explicitStatus}
id={item.imageId}
itemType={item._itemType}
serverId={item._serverId}
type="itemCard"
/>
{isFavorite && <div className={styles.favoriteBadge} />}
{hasRating && <div className={styles.ratingBadge}>{userRating}</div>}
<AnimatePresence>
{controls && isImageHovered && (
<ItemCardControls
controls={controls}
enableExpansion={false}
internalState={internalState}
item={item}
itemType={item._itemType}
showRating={true}
type="compact"
/>
)}
</AnimatePresence>
</Link>
</div>
<ItemImage
className={styles.image}
explicitStatus={item.explicitStatus}
id={item.imageId}
itemType={item._itemType}
serverId={item._serverId}
type="itemCard"
/>
{isFavorite && <div className={styles.favoriteBadge} />}
{hasRating && <div className={styles.ratingBadge}>{userRating}</div>}
<AnimatePresence>
{controls && isImageHovered && (
<ItemCardControls
controls={controls}
enableExpansion={false}
internalState={internalState}
item={item}
itemType={item._itemType}
showRating={true}
type="compact"
/>
)}
</AnimatePresence>
</Link>
<Link
className={styles.title}
state={{ item }}
@@ -1256,8 +1186,6 @@ export const ItemDetailList = ({
}: ItemDetailListProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const listRef = useListRef(null);
const { focused, ref: focusRef } = useFocusWithin();
const mergedContainerRef = useMergedRef(containerRef, focusRef);
const lastVisibleStartIndexRef = useRef(0);
const queryClient = useQueryClient();
@@ -1449,13 +1377,6 @@ export const ItemDetailList = ({
},
});
useListHotkeys({
controls,
focused,
internalState,
itemType: LibraryItem.SONG,
});
useEffect(() => {
const { current: container } = containerRef;
@@ -1512,7 +1433,7 @@ export const ItemDetailList = ({
trackTableSize={trackTableSize}
/>
)}
<div className={styles.container} ref={mergedContainerRef}>
<div className={styles.container} ref={containerRef}>
<List
listRef={listRef}
onRowsRendered={throttledHandleRowsRendered}
@@ -60,6 +60,6 @@ export function shouldShowHoverOnlyColumnContent(
return (
isRowHovered ||
(columnId === TableColumn.USER_FAVORITE && song.userFavorite !== false) ||
(columnId === TableColumn.USER_RATING && song.userRating !== null && song.userRating !== 0)
(columnId === TableColumn.USER_RATING && song.userRating != null)
);
}
@@ -225,39 +225,6 @@ const AlbumArtistMetadataBiography = ({
);
};
const TABLE_ROW_HEIGHT = {
compact: 40,
default: 64,
large: 88,
} as const;
const TABLE_HEADER_HEIGHT = 40;
interface SongTableListContainerProps {
children: React.ReactNode;
enableHeader?: boolean;
itemCount: number;
maxRows?: number;
tableSize?: 'compact' | 'default' | 'large';
}
function getTableRowHeight(size: 'compact' | 'default' | 'large' | undefined): number {
return size ? TABLE_ROW_HEIGHT[size] : TABLE_ROW_HEIGHT.default;
}
const SongTableListContainer = ({
children,
enableHeader = true,
itemCount,
maxRows = 5,
tableSize = 'default',
}: SongTableListContainerProps) => {
const rowHeight = getTableRowHeight(tableSize);
const headerOffset = enableHeader ? TABLE_HEADER_HEIGHT : 0;
const height = headerOffset + rowHeight * Math.min(itemCount, maxRows);
return <div style={{ height }}>{children}</div>;
};
interface AlbumArtistMetadataTopSongsProps {
detailQuery: ReturnType<typeof useSuspenseQuery<AlbumArtistDetailResponse>>;
routeId: string;
@@ -270,6 +237,7 @@ const AlbumArtistMetadataTopSongsContent = ({
const { t } = useTranslation();
const [searchTerm, setSearchTerm] = useState('');
const [debouncedSearchTerm] = useDebouncedValue(searchTerm, 300);
const [showAll, setShowAll] = useState(false);
const [topSongsQueryType, setTopSongsQueryType] = useLocalStorage<'community' | 'personal'>({
defaultValue: 'community',
key: 'album-artist-top-songs-query-type',
@@ -301,8 +269,13 @@ const AlbumArtistMetadataTopSongsContent = ({
}, [tableConfig?.columns]);
const filteredSongs = useMemo(() => {
return searchLibraryItems(songs, debouncedSearchTerm, LibraryItem.SONG);
}, [songs, debouncedSearchTerm]);
const filtered = searchLibraryItems(songs, debouncedSearchTerm, LibraryItem.SONG);
// When searching, show all results. Otherwise, limit to 5 if not showing all
if (debouncedSearchTerm?.trim() || showAll) {
return filtered;
}
return filtered.slice(0, 5);
}, [songs, debouncedSearchTerm, showAll]);
const { handleColumnReordered } = useItemListColumnReorder({
itemListKey: ItemListKey.SONG,
@@ -486,35 +459,35 @@ const AlbumArtistMetadataTopSongsContent = ({
tableColumnsData={SONG_TABLE_COLUMNS}
/>
</Group>
<SongTableListContainer
<ItemTableList
activeRowId={currentSongId}
autoFitColumns={tableConfig.autoFitColumns}
CellComponent={ItemTableListColumn}
columns={columns}
data={filteredSongs}
enableAlternateRowColors={tableConfig.enableAlternateRowColors}
enableDrag
enableDragScroll={false}
enableExpansion={false}
enableHeader={tableConfig.enableHeader}
itemCount={filteredSongs.length}
maxRows={5}
tableSize={tableConfig.size}
>
<ItemTableList
activeRowId={currentSongId}
autoFitColumns={tableConfig.autoFitColumns}
CellComponent={ItemTableListColumn}
columns={columns}
data={filteredSongs}
enableAlternateRowColors={tableConfig.enableAlternateRowColors}
enableDrag
enableDragScroll={false}
enableExpansion={false}
enableHeader={tableConfig.enableHeader}
enableHorizontalBorders={tableConfig.enableHorizontalBorders}
enableRowHoverHighlight={tableConfig.enableRowHoverHighlight}
enableSelection
enableSelectionDialog={false}
enableVerticalBorders={tableConfig.enableVerticalBorders}
itemType={LibraryItem.SONG}
onColumnReordered={handleColumnReordered}
onColumnResized={handleColumnResized}
overrideControls={overrideControls}
size={tableConfig.size}
/>
</SongTableListContainer>
enableHorizontalBorders={tableConfig.enableHorizontalBorders}
enableRowHoverHighlight={tableConfig.enableRowHoverHighlight}
enableSelection
enableSelectionDialog={false}
enableVerticalBorders={tableConfig.enableVerticalBorders}
itemType={LibraryItem.SONG}
onColumnReordered={handleColumnReordered}
onColumnResized={handleColumnResized}
overrideControls={overrideControls}
size={tableConfig.size}
/>
{!searchTerm.trim() && songs.length > 5 && !showAll && (
<Group justify="center" w="100%">
<Button onClick={() => setShowAll(true)} variant="subtle">
{t('action.viewMore', { postProcess: 'sentenceCase' })}
</Button>
</Group>
)}
</>
) : null}
</Stack>
@@ -550,6 +523,7 @@ const AlbumArtistMetadataFavoriteSongs = ({ routeId }: AlbumArtistMetadataFavori
const { t } = useTranslation();
const [searchTerm, setSearchTerm] = useState('');
const [debouncedSearchTerm] = useDebouncedValue(searchTerm, 300);
const [showAll, setShowAll] = useState(false);
const tableConfig = useSettingsStore((state) => state.lists[ItemListKey.SONG]?.table);
const currentSong = usePlayerSong();
const player = usePlayer();
@@ -574,8 +548,13 @@ const AlbumArtistMetadataFavoriteSongs = ({ routeId }: AlbumArtistMetadataFavori
}, [tableConfig?.columns]);
const filteredSongs = useMemo(() => {
return searchLibraryItems(songs, debouncedSearchTerm, LibraryItem.SONG);
}, [songs, debouncedSearchTerm]);
const filtered = searchLibraryItems(songs, debouncedSearchTerm, LibraryItem.SONG);
// When searching, show all results. Otherwise, limit to 5 if not showing all
if (debouncedSearchTerm?.trim() || showAll) {
return filtered;
}
return filtered.slice(0, 5);
}, [songs, debouncedSearchTerm, showAll]);
const { handleColumnReordered } = useItemListColumnReorder({
itemListKey: ItemListKey.SONG,
@@ -738,35 +717,35 @@ const AlbumArtistMetadataFavoriteSongs = ({ routeId }: AlbumArtistMetadataFavori
tableColumnsData={SONG_TABLE_COLUMNS}
/>
</Group>
<SongTableListContainer
<ItemTableList
activeRowId={currentSongId}
autoFitColumns={tableConfig.autoFitColumns}
CellComponent={ItemTableListColumn}
columns={columns}
data={filteredSongs}
enableAlternateRowColors={tableConfig.enableAlternateRowColors}
enableDrag
enableDragScroll={false}
enableExpansion={false}
enableHeader={tableConfig.enableHeader}
itemCount={filteredSongs.length}
maxRows={5}
tableSize={tableConfig.size}
>
<ItemTableList
activeRowId={currentSongId}
autoFitColumns={tableConfig.autoFitColumns}
CellComponent={ItemTableListColumn}
columns={columns}
data={filteredSongs}
enableAlternateRowColors={tableConfig.enableAlternateRowColors}
enableDrag
enableDragScroll={false}
enableExpansion={false}
enableHeader={tableConfig.enableHeader}
enableHorizontalBorders={tableConfig.enableHorizontalBorders}
enableRowHoverHighlight={tableConfig.enableRowHoverHighlight}
enableSelection
enableSelectionDialog={false}
enableVerticalBorders={tableConfig.enableVerticalBorders}
itemType={LibraryItem.SONG}
onColumnReordered={handleColumnReordered}
onColumnResized={handleColumnResized}
overrideControls={overrideControls}
size={tableConfig.size}
/>
</SongTableListContainer>
enableHorizontalBorders={tableConfig.enableHorizontalBorders}
enableRowHoverHighlight={tableConfig.enableRowHoverHighlight}
enableSelection
enableSelectionDialog={false}
enableVerticalBorders={tableConfig.enableVerticalBorders}
itemType={LibraryItem.SONG}
onColumnReordered={handleColumnReordered}
onColumnResized={handleColumnResized}
overrideControls={overrideControls}
size={tableConfig.size}
/>
{!searchTerm.trim() && songs.length > 5 && !showAll && (
<Group justify="center" w="100%">
<Button onClick={() => setShowAll(true)} variant="subtle">
{t('action.viewMore', { postProcess: 'sentenceCase' })}
</Button>
</Group>
)}
</>
) : null}
</Stack>
@@ -109,18 +109,8 @@ export const useDiscordRpc = () => {
instance: false,
largeImageKey: 'icon',
largeImageText: truncate(stationName || 'Radio'),
smallImageKey:
current[2] === PlayerStatus.PLAYING
? discordSettings.showStateIcon
? 'playing'
: undefined
: 'paused',
smallImageText:
current[2] === PlayerStatus.PLAYING
? discordSettings.showStateIcon
? sentenceCase(current[2])
: undefined
: sentenceCase(current[2]),
smallImageKey: current[2] === PlayerStatus.PLAYING ? 'playing' : 'paused',
smallImageText: sentenceCase(current[2]),
state: truncate(artist),
statusDisplayType: StatusDisplayType.STATE,
type: discordSettings.showAsListening ? 2 : 0,
@@ -209,7 +199,7 @@ export const useDiscordRpc = () => {
(song?.album && song.album.padEnd(2, ' ')) || 'Unknown album',
),
smallImageKey: undefined,
smallImageText: undefined,
smallImageText: sentenceCase(current[2]),
state: truncate((artists && artists.padEnd(2, ' ')) || 'Unknown artist'),
statusDisplayType: statusDisplayMap[discordSettings.displayType],
// I would love to use the actual type as opposed to hardcoding to 2,
@@ -257,13 +247,9 @@ export const useDiscordRpc = () => {
activity.endTimestamp = end;
}
if (discordSettings.showStateIcon) {
activity.smallImageKey = 'playing';
activity.smallImageText = sentenceCase(current[2]);
}
activity.smallImageKey = 'playing';
} else {
activity.smallImageKey = 'paused';
activity.smallImageText = sentenceCase(current[2]);
}
if (discordSettings.showServerImage && song) {
@@ -363,7 +349,6 @@ export const useDiscordRpc = () => {
[
discordSettings.showAsListening,
discordSettings.showServerImage,
discordSettings.showStateIcon,
discordSettings.showPaused,
lastfmApiKey,
discordSettings.clientId,
@@ -82,7 +82,16 @@ const HomeRoute = () => {
},
};
const sortedItems = homeItems.filter((item) => !item.disabled);
const sortedItems = homeItems.filter((item) => {
if (item.disabled) {
return false;
}
if (isJellyfin && item.id === HomeItem.RECENTLY_PLAYED) {
return false;
}
return true;
});
const sortedCarousel = sortedItems
.filter((item) => item.id !== HomeItem.GENRES)
@@ -6,10 +6,6 @@
filter: drop-shadow(0 0 5px rgb(0 0 0 / 40%)) drop-shadow(0 0 5px rgb(0 0 0 / 40%));
}
.censored.image {
filter: blur(30px);
}
.image-container {
position: relative;
display: flex;
@@ -11,12 +11,7 @@ import {
useRadioPlayer,
} from '/@/renderer/features/radio/hooks/use-radio-player';
import { AppRoute } from '/@/renderer/router/routes';
import {
useGeneralSettings,
useNativeAspectRatio,
usePlayerData,
usePlayerSong,
} from '/@/renderer/store';
import { useNativeAspectRatio, usePlayerData, usePlayerSong } from '/@/renderer/store';
import { Badge } from '/@/shared/components/badge/badge';
import { Center } from '/@/shared/components/center/center';
import { Flex } from '/@/shared/components/flex/flex';
@@ -25,7 +20,7 @@ import { Icon } from '/@/shared/components/icon/icon';
import { Stack } from '/@/shared/components/stack/stack';
import { Text } from '/@/shared/components/text/text';
import { useSetState } from '/@/shared/hooks/use-set-state';
import { ExplicitStatus, LibraryItem } from '/@/shared/types/domain-types';
import { LibraryItem } from '/@/shared/types/domain-types';
const imageVariants: Variants = {
closed: {
@@ -54,14 +49,9 @@ const MotionImage = motion.img;
const ImageWithPlaceholder = ({
className,
explicit,
placeholderIcon = 'itemAlbum',
...props
}: HTMLMotionProps<'img'> & {
explicit?: boolean;
placeholder?: string;
placeholderIcon?: 'itemAlbum' | 'radio';
}) => {
}: HTMLMotionProps<'img'> & { placeholder?: string; placeholderIcon?: 'itemAlbum' | 'radio' }) => {
const nativeAspectRatio = useNativeAspectRatio();
if (!props.src) {
@@ -81,9 +71,7 @@ const ImageWithPlaceholder = ({
return (
<MotionImage
className={clsx(styles.image, className, {
[styles.censored]: explicit,
})}
className={clsx(styles.image, className)}
style={{
objectFit: nativeAspectRatio ? 'contain' : 'cover',
width: nativeAspectRatio ? 'auto' : '100%',
@@ -101,7 +89,6 @@ export const FullScreenPlayerImage = () => {
const currentSong = usePlayerSong();
const { nextSong } = usePlayerData();
const { blurExplicitImages } = useGeneralSettings();
const isPlayingRadio = isRadioActive && isRadioPlaying;
@@ -120,10 +107,8 @@ export const FullScreenPlayerImage = () => {
});
const [imageState, setImageState] = useSetState({
bottomExplicit: nextSong?.explicitStatus === ExplicitStatus.EXPLICIT,
bottomImage: nextImageUrl,
current: 0,
topExplicit: currentSong?.explicitStatus === ExplicitStatus.EXPLICIT,
topImage: currentImageUrl,
});
@@ -148,14 +133,8 @@ export const FullScreenPlayerImage = () => {
const isTop = imageStateRef.current.current === 0;
setImageState({
bottomExplicit:
(isTop ? currentSong?.explicitStatus : nextSong?.explicitStatus) ===
ExplicitStatus.EXPLICIT,
bottomImage: isTop ? currentImageUrl : nextImageUrl,
current: isTop ? 1 : 0,
topExplicit:
(isTop ? nextSong?.explicitStatus : currentSong?.explicitStatus) ===
ExplicitStatus.EXPLICIT,
topImage: isTop ? nextImageUrl : currentImageUrl,
});
@@ -167,8 +146,6 @@ export const FullScreenPlayerImage = () => {
nextSong?._uniqueId,
nextImageUrl,
setImageState,
currentSong?.explicitStatus,
nextSong?.explicitStatus,
]);
return (
@@ -188,7 +165,6 @@ export const FullScreenPlayerImage = () => {
custom={{ isOpen: imageState.current === 0 }}
draggable={false}
exit="closed"
explicit={blurExplicitImages && imageState.topExplicit}
initial="closed"
key={`top-${currentSong?._uniqueId || 'none'}`}
placeholder="var(--theme-colors-foreground-muted)"
@@ -204,7 +180,6 @@ export const FullScreenPlayerImage = () => {
custom={{ isOpen: imageState.current === 1 }}
draggable={false}
exit="closed"
explicit={blurExplicitImages && imageState.bottomExplicit}
initial="closed"
key={`bottom-${currentSong?._uniqueId || 'none'}`}
placeholder="var(--theme-colors-foreground-muted)"
@@ -12,7 +12,6 @@ import { ItemTableList } from '/@/renderer/components/item-list/item-table-list/
import { ItemTableListColumn } from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
import { DefaultItemControlProps, ItemControls } from '/@/renderer/components/item-list/types';
import { useListContext } from '/@/renderer/context/list-context';
import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller';
import { usePlayer } from '/@/renderer/features/player/context/player-context';
import { usePlaylistSongListFilters } from '/@/renderer/features/playlists/hooks/use-playlist-song-list-filters';
import { type PlaylistAlbumRow, playlistSongsToAlbums } from '/@/renderer/features/playlists/utils';
@@ -23,7 +22,6 @@ import { sortSongList } from '/@/shared/api/utils';
import {
LibraryItem,
PlaylistSongListResponse,
Song,
SongListSort,
SortOrder,
} from '/@/shared/types/domain-types';
@@ -67,43 +65,12 @@ export const PlaylistDetailAlbumView = ({ data }: { data: PlaylistSongListRespon
const albumControlOverrides = useMemo<Partial<ItemControls>>(() => {
return {
onMore: ({ event, internalState, item }: DefaultItemControlProps) => {
if (!event) return;
const selected = internalState?.getSelected();
if (selected?.length === 0 && !item) {
return;
}
let itemsToUse: (PlaylistAlbumRow | Song)[];
if ((selected?.length ?? 0) > 0) {
itemsToUse = selected as (PlaylistAlbumRow | Song)[];
} else {
itemsToUse = [item as PlaylistAlbumRow | Song];
}
const songs: Song[] = [];
for (const item of itemsToUse) {
if (item._itemType === LibraryItem.ALBUM) {
songs.push(...((item as PlaylistAlbumRow)._playlistSongs ?? []));
} else if (item._itemType === LibraryItem.SONG) {
songs.push(item as Song);
}
}
ContextMenuController.call({
cmd: { items: songs, type: LibraryItem.PLAYLIST_SONG },
event,
});
},
onPlay: ({
item,
itemType,
playType,
}: DefaultItemControlProps & { playType: Play }) => {
if (!item) return;
const rowSongs = (item as PlaylistAlbumRow)._playlistSongs;
if (itemType === LibraryItem.ALBUM && rowSongs?.length) {
player.addToQueueByData(rowSongs, playType);
@@ -95,7 +95,7 @@ export const PlaylistDetailSongListGrid = forwardRef<any, PlaylistDetailSongList
type: 'offset',
}}
itemsPerRow={gridProps.itemsPerRowEnabled ? gridProps.itemsPerRow : undefined}
itemType={LibraryItem.PLAYLIST_SONG}
itemType={LibraryItem.SONG}
onScrollEnd={handleOnScrollEnd}
rows={rows}
size={gridProps.size}
@@ -98,28 +98,6 @@ export const DiscordSettings = memo(() => {
postProcess: 'sentenceCase',
}),
},
{
control: (
<Switch
checked={settings.showStateIcon}
onChange={(e) => {
setSettings({
discord: {
showStateIcon: e.currentTarget.checked,
},
});
}}
/>
),
description: t('setting.discordStateIcon', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: !isElectron(),
title: t('setting.discordStateIcon', {
postProcess: 'sentenceCase',
}),
},
{
control: (
<Switch
+16 -7
View File
@@ -124,12 +124,23 @@ export const createFuseForLibraryItem = <T extends FuseSearchableItem>(
});
}
const stringKeys: string[] = [];
const sampleItem = items[0];
const stringKeys = Object.keys(sampleItem).filter(
(key) =>
typeof sampleItem[key as keyof T] === 'string' &&
!key.startsWith('_') &&
key !== 'id' &&
key !== 'albumId' &&
key !== 'streamUrl' &&
key !== 'serverId' &&
key !== 'ownerId',
) as string[];
const nestedKeys: Array<{ getFn: (item: T) => string; name: string }> = [];
switch (itemType) {
case LibraryItem.ALBUM: {
stringKeys.push('name', 'releaseType');
nestedKeys.push(
{
getFn: (item) => {
@@ -157,7 +168,6 @@ export const createFuseForLibraryItem = <T extends FuseSearchableItem>(
}
case LibraryItem.ALBUM_ARTIST: {
stringKeys.push('name');
nestedKeys.push({
getFn: (item) => {
const aa = item as AlbumArtist;
@@ -171,10 +181,9 @@ export const createFuseForLibraryItem = <T extends FuseSearchableItem>(
case LibraryItem.ARTIST:
case LibraryItem.GENRE:
case LibraryItem.RADIO_STATION:
stringKeys.push('name');
break;
case LibraryItem.PLAYLIST: {
stringKeys.push('name');
nestedKeys.push({
getFn: (item) => {
const p = item as Playlist;
@@ -187,8 +196,7 @@ export const createFuseForLibraryItem = <T extends FuseSearchableItem>(
case LibraryItem.PLAYLIST_SONG:
case LibraryItem.QUEUE_SONG:
case LibraryItem.SONG:
stringKeys.push('album', 'name');
case LibraryItem.SONG: {
nestedKeys.push(
{
getFn: (item) => {
@@ -206,6 +214,7 @@ export const createFuseForLibraryItem = <T extends FuseSearchableItem>(
},
);
break;
}
}
return new Fuse(items, {
@@ -53,10 +53,6 @@
border-radius: var(--theme-radius-md);
}
.censored.sidebar-image {
filter: blur(20px);
}
.accordion-root {
height: 100%;
}
@@ -24,7 +24,6 @@ import {
useAppStore,
useAppStoreActions,
useFullScreenPlayerStore,
useGeneralSettings,
usePlayerSong,
useSetFullScreenPlayerStore,
} from '/@/renderer/store';
@@ -43,7 +42,7 @@ import { ImageUnloader } from '/@/shared/components/image/image';
import { ScrollArea } from '/@/shared/components/scroll-area/scroll-area';
import { Text } from '/@/shared/components/text/text';
import { Tooltip } from '/@/shared/components/tooltip/tooltip';
import { ExplicitStatus, LibraryItem } from '/@/shared/types/domain-types';
import { LibraryItem } from '/@/shared/types/domain-types';
import { Platform } from '/@/shared/types/types';
export const Sidebar = () => {
@@ -168,7 +167,6 @@ const SidebarImage = () => {
const currentSong = usePlayerSong();
const isRadioActive = useIsRadioActive();
const { isPlaying: isRadioPlaying } = useRadioPlayer();
const { blurExplicitImages } = useGeneralSettings();
const imageUrl = useItemImageUrl({
id: currentSong?.imageId || undefined,
@@ -237,15 +235,7 @@ const SidebarImage = () => {
<Icon color="muted" icon="radio" size="40%" />
</Center>
) : imageUrl ? (
<img
className={clsx(styles.sidebarImage, {
[styles.censored]:
currentSong?.explicitStatus === ExplicitStatus.EXPLICIT &&
blurExplicitImages,
})}
loading="eager"
src={imageUrl}
/>
<img className={styles.sidebarImage} loading="eager" src={imageUrl} />
) : (
<ImageUnloader icon="emptySongImage" />
)}
-2
View File
@@ -263,7 +263,6 @@ const DiscordSettingsSchema = z.object({
showAsListening: z.boolean(),
showPaused: z.boolean(),
showServerImage: z.boolean(),
showStateIcon: z.boolean(),
});
const FontSettingsSchema = z.object({
@@ -996,7 +995,6 @@ const initialState: SettingsState = {
showAsListening: false,
showPaused: true,
showServerImage: false,
showStateIcon: true,
},
font: {
builtIn: 'Inter',
@@ -39,7 +39,3 @@
.muted {
opacity: 0.85;
}
.with-space {
padding-right: var(--theme-spacing-sm);
}
@@ -32,12 +32,10 @@ export const ExplicitIndicator = ({
return (
<span
aria-label={explicitStatus === ExplicitStatus.EXPLICIT ? 'Explicit' : 'Clean'}
className={clsx(styles.root, styles[`size-${size}`], className, {
[styles.withSpace]: withSpace,
})}
className={clsx(styles.root, styles[`size-${size}`], className)}
{...rest}
>
{symbol}
{withSpace ? `${symbol} ` : symbol}
</span>
);
};
+1 -21
View File
@@ -24,24 +24,7 @@ export default defineConfig({
),
},
output: {
assetFileNames: (assetInfo) => {
const stableNames = [
'32x32',
'64x64',
'128x128',
'256x256',
'512x512',
'1024x1024',
'favicon',
'preview_full_screen_player',
];
if (assetInfo.name && stableNames.includes(assetInfo.name)) {
return 'assets/[name][extname]';
}
return 'assets/[name]-[hash][extname]';
},
assetFileNames: 'assets/[name].[ext]',
sourcemapExcludeSources: false,
},
},
@@ -131,10 +114,7 @@ export default defineConfig({
registerType: 'autoUpdate',
scope: '/assets/',
workbox: {
cleanupOutdatedCaches: true,
clientsClaim: true,
maximumFileSizeToCacheInBytes: 1000000 * 5, // 5 MB
skipWaiting: true,
},
}),
],