Compare commits

..

5 Commits

Author SHA1 Message Date
jeffvli b35b805a8e update to v0.22.0 2025-11-29 19:28:01 -08:00
jeffvli 683705aa9b update translations 11/28
- manually upload new translations due to failing weblate push
2025-11-28 02:31:33 -08:00
Kendall Garner cad0265e3c fix(rpc): truncate certain fields (#1263)
* fix(rpc): truncate certain fields

* off by one...

* use elipses character
2025-11-17 02:24:52 +00:00
Damien Erambert 6a236c803a Small light theme fixes (#1248)
* Use grey border instead of black shadow in light mode below filter-bar

* Fix some theme colors not updating when theme is changed in real-time
2025-11-09 00:25:58 -08:00
Kendall Garner d74e7b52cc position original release date after release types 2025-11-08 15:38:19 -08:00
89 changed files with 5195 additions and 6052 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "feishin",
"version": "0.21.2",
"version": "0.22.0",
"description": "A modern self-hosted music player.",
"keywords": [
"subsonic",
+4 -1
View File
@@ -301,7 +301,10 @@
"discordRichPresence": "stav na {{discord}}u",
"enableAutoTranslation_description": "povolit automatický překlad při načtení textů",
"enableAutoTranslation": "povolit automatický překlad",
"language": "jazyk"
"language": "jazyk",
"notify": "povolit oznámení o skladbách",
"notify_description": "zobrazit oznámení při změně aktuální skladby",
"transcode": "povolit překódování"
},
"action": {
"editPlaylist": "upravit $t(entity.playlist_one)",
+4 -1
View File
@@ -301,7 +301,10 @@
"enableAutoTranslation": "Activar traducción automática",
"discordRichPresence": "Estado de actividad de {{discord}}",
"crossfadeStyle": "Estilo del crossfade",
"language": "Idioma"
"language": "Idioma",
"notify": "Activar notificaciones de canciones",
"notify_description": "Muestra notificaciones cuando se cambia la canción actual",
"transcode": "Activar transcodificación"
},
"action": {
"editPlaylist": "editar $t(entity.playlist_one)",
+12 -3
View File
@@ -538,7 +538,12 @@
"imageAspectRatio_description": "gaituta badago, azaleko artea jatorrizko aspektu-erlazioa erabiliz erakutsiko da. 1:1 ez den arterako, gainerako espazioa hutsik egongo da",
"crossfadeStyle": "crossfade estiloa",
"discordRichPresence": "{{discord}}-en jarduera-egoera",
"enableAutoTranslation": "gaitu itzulpen automatikoa"
"enableAutoTranslation": "gaitu itzulpen automatikoa",
"exportImportSettings_control_exportText": "esportatu ezarpenak",
"exportImportSettings_control_importText": "inportatu ezarpenak",
"exportImportSettings_control_title": "inportatu / esportatu ezarpenak",
"exportImportSettings_importBtn": "inportatu ezarpenak",
"exportImportSettings_importModalTitle": "inportatu feishin ezarpenak"
},
"form": {
"addServer": {
@@ -768,10 +773,14 @@
},
"releaseType": {
"primary": {
"album": "$t(entity.album_one)"
"album": "$t(entity.album_one)",
"other": "bestelakoa"
},
"secondary": {
"compilation": "konpilazioa"
"compilation": "konpilazioa",
"audiobook": "audioliburua",
"interview": "elkarrizketa",
"remix": "nahasketa"
}
}
}
+63 -8
View File
@@ -94,7 +94,14 @@
"newVersion": "uusi versio on asennettu ({{version}})",
"viewReleaseNotes": "katsele julkaisutietoja",
"bitDepth": "bittisyvyys",
"sampleRate": "näytteenottotaajuus"
"sampleRate": "näytteenottotaajuus",
"private": "yksityinen",
"public": "julkinen",
"explicitStatus": "eksplisiittinen tila",
"recordLabel": "levy-yhtiö",
"releaseType": "julkaisun tyyppi",
"explicit": "eksplisiittinen",
"clean": "puhdas"
},
"entity": {
"album_one": "albumi",
@@ -225,7 +232,8 @@
"note": "muistiinpano",
"owner": "$t(common.owner)",
"path": "polku",
"songCount": "kappalemäärä"
"songCount": "kappalemäärä",
"explicitStatus": "$t(common.explicitStatus)"
},
"form": {
"addServer": {
@@ -239,7 +247,9 @@
"error_savePassword": "salasanaa tallentaessa tapahtui virhe",
"input_password": "salasana",
"input_username": "käyttäjänimi",
"success": "palvelin lisätty onnistuneesti"
"success": "palvelin lisätty onnistuneesti",
"input_preferInstantMix": "suosi pika-miksausta",
"input_preferInstantMixDescription": "käytä vain pika-miksausta saadaksesi samankaltaisia kappaleita. käytännöllinen jos sinulla on lisäosia, jotka muuttavat tätä käytöstä"
},
"createPlaylist": {
"input_public": "julkinen",
@@ -253,7 +263,9 @@
"input_skipDuplicates": "ohita kaksoiskappaleet",
"success": "$t(entity.trackWithCount, {\"count\": {{message}} }) lisätty $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
"title": "lisää soittolistalle $t(entity.playlist_one)",
"input_playlists": "$t(entity.playlist_other)"
"input_playlists": "$t(entity.playlist_other)",
"create": "luo $t(entity.playlist_one) {{playlist}}",
"searchOrCreate": "hae $t(entity.playlist_other) tai tyyppiä luodaksesi uuden"
},
"updateServer": {
"success": "palvelin on päivitetty onnistuneesti",
@@ -286,6 +298,11 @@
"input_optionMatchAny": "sovita joku",
"input_optionMatchAll": "sovita kaikki",
"title": "kyselyeditori"
},
"privateMode": {
"enabled": "yksityinen tila käytössä, toistotila on nyt piilotettu ulkoisilta integraatioilta",
"disabled": "yksityinen tila poissa käytössä, toistotila on nyt näkyvillä ulkoisille integraatioille",
"title": "yksityinen tila"
}
},
"setting": {
@@ -521,7 +538,17 @@
"discordPausedStatus": "näytä rich presence tauotettuna",
"discordPausedStatus_description": "ollessak käytössä, status näyttää milloin soitin on tautotettuna",
"preservePitch": "säilytä sävelkorkeus",
"preservePitch_description": "säilytä sävelkorkeus toistonopeutta muokatessa"
"preservePitch_description": "säilytä sävelkorkeus toistonopeutta muokatessa",
"artistBackground": "artistin taustakuva",
"artistBackground_description": "lisää taustakuvan artistin sivuille, jotak sisältävät artistin kuvia",
"artistBackgroundBlur": "artistin taustakuvan kuvan sumennuksen koko",
"artistBackgroundBlur_description": "säätää artistin taustakuvaan käytettävän sumennuksen määrää",
"crossfadeStyle": "ristihäivytyksen tyylli",
"releaseChannel_optionBeta": "beeta",
"releaseChannel_optionLatest": "viimeisin",
"releaseChannel": "julkaisulinja",
"releaseChannel_description": "valitse vakaiden ja beetaversioiden välillä automaattisille päivityksille",
"discordDisplayType_artistname": "artistin nimi / artistien nimet"
},
"page": {
"itemDetail": {
@@ -552,7 +579,9 @@
"goForward": "mene eteenpäin",
"manageServers": "hallitse palvelimia",
"collapseSidebar": "kutista sivupalkki",
"version": "versio {{version}}"
"version": "versio {{version}}",
"privateModeOff": "käännä yksityinen tila pois käytöstä",
"privateModeOn": "käännä yksityinen tila käyttöön"
},
"contextMenu": {
"playSimilarSongs": "$t(player.playSimilarSongs)",
@@ -576,7 +605,9 @@
"addFavorite": "$t(action.addToFavorites)",
"addLast": "$t(player.addLast)",
"moveToNext": "$t(action.moveToNext)",
"removeFromQueue": "$t(action.removeFromQueue)"
"removeFromQueue": "$t(action.removeFromQueue)",
"goToAlbum": "mene $t(entity.album_one)",
"goToAlbumArtist": "mene $t(entity.albumArtist_one)"
},
"sidebar": {
"albumArtists": "$t(entity.albumArtist_other)",
@@ -641,7 +672,8 @@
"recentlyPlayed": "hiljattain soitetut",
"title": "$t(common.home)",
"mostPlayed": "eniten soitetut",
"newlyAdded": "hiljattain lisätyt julkaisut"
"newlyAdded": "hiljattain lisätyt julkaisut",
"recentlyReleased": "hiljattain julkaistu"
},
"albumArtistDetail": {
"about": "{{artist}}{sta/stä",
@@ -783,5 +815,28 @@
"rating": "arvostelu",
"releaseDate": "julkaisupäivämäärä"
}
},
"releaseType": {
"primary": {
"album": "$t(entity.album_one)",
"broadcast": "lähetys",
"ep": "EP",
"other": "muu",
"single": "single"
},
"secondary": {
"audiobook": "änikirja",
"audioDrama": "kuunnelma",
"compilation": "kokoomateos",
"djMix": "DJ mix",
"demo": "demo",
"fieldRecording": "kenttä-äänitys",
"interview": "haastattelu",
"live": "live",
"mixtape": "mixtape",
"remix": "remiksi",
"soundtrack": "elokuvamusiikki",
"spokenWord": "puhetta"
}
}
}
+4 -1
View File
@@ -662,7 +662,10 @@
"exportImportSettings_importModalTitle": "paramètres d'importation feishin",
"crossfadeStyle": "style de fondu enchaîné",
"discordRichPresence": "{{discord}} Rich Presence",
"language": "langage"
"language": "langage",
"notify_description": "affiche une notification lorsque la chanson en cours change",
"transcode": "activer le transcodage",
"notify": "activer les notifications de chansons"
},
"form": {
"deletePlaylist": {
+669 -47
View File
@@ -2,8 +2,8 @@
"action": {
"moveToNext": "ugrás a következőre",
"deletePlaylist": "$t(entity.playlist_one) törlése",
"removeFromFavorites": "eltávolítás innen: $t(entity.favorite_other)",
"setRating": "értékelés",
"removeFromFavorites": "eltávolítás innen $t(entity.favorite_other)",
"setRating": "értékelés beállítása",
"viewPlaylists": "$t(entity.playlist_other) megtekintése",
"openIn": {
"lastfm": "Megnyitás Last.fm-ben",
@@ -11,16 +11,17 @@
},
"clearQueue": "műsorlista kiürítése",
"createPlaylist": "$t(entity.playlist_one) létrehozása",
"deselectAll": "kijelölések törlése",
"deselectAll": "kijelölés törlése",
"editPlaylist": "$t(entity.playlist_one) szerkesztése",
"goToPage": "oldal meglátogatása",
"moveToBottom": "ugrás az utolsóhoz",
"moveToTop": "ugrás az elsőhöz",
"removeFromPlaylist": "eltávolítás innen: $t(entity.playlist_one)",
"goToPage": "menj az oldalra",
"moveToBottom": "ugrás az aljára",
"moveToTop": "ugrás a tetejére",
"removeFromPlaylist": "eltávolítás innen $t(entity.playlist_one)",
"removeFromQueue": "eltávolítás a műsorlistáról",
"toggleSmartPlaylistEditor": "$t(entity.smartPlaylist) szerkesztője",
"addToFavorites": "$t(entity.favorite_other) kedvelése",
"addToPlaylist": "hozzáadás lejátszási listához: $t(entity.playlist_one)"
"addToPlaylist": "hozzáadás lejátszási listához: $t(entity.playlist_one)",
"refresh": "$t(common.refresh)"
},
"common": {
"collapse": "összecsukás",
@@ -33,8 +34,8 @@
"delete": "törlés",
"description": "leírás",
"comingSoon": "hamarosan…",
"decrease": "csökkentés",
"enable": "engedélyes",
"decrease": "csökkenés",
"enable": "engedélyez",
"disable": "letiltás",
"disc": "lemez",
"modified": "módosult",
@@ -44,12 +45,12 @@
"action_one": "művelet",
"action_other": "műveletek",
"add": "hozzáadás",
"albumGain": "album erősség",
"albumPeak": "album csúcs",
"areYouSure": "biztos vagy benne?",
"ascending": "növekvő",
"backward": "visszafelé",
"biography": "biográfia",
"albumGain": "album hangerőssége",
"albumPeak": "album hangcsúcs",
"areYouSure": "Biztos vagy benne?",
"ascending": "emelkedő",
"backward": "vissza",
"biography": "Életrajz",
"bitrate": "bitráta",
"cancel": "mégse",
"center": "közép",
@@ -67,7 +68,7 @@
"filter_other": "szűrők",
"filters": "szűrők",
"forward": "előre",
"gap": "rés",
"gap": "gap",
"increase": "megnövelés",
"left": "bal",
"limit": "korlát",
@@ -75,10 +76,10 @@
"maximize": "maximalizálás",
"menu": "menü",
"minimize": "minimalizálás",
"mbid": "MusicBrainz azonosító",
"mbid": "MusicBrainz ID",
"noResultsFromQuery": "nincsenek találatok a keresett kifejezésre",
"note": "jegyzet",
"ok": "rendben",
"ok": "ok",
"owner": "tulajdonos",
"path": "elérési út",
"playerMustBePaused": "a lejátszónak szüneteltetve kell lennie",
@@ -87,13 +88,13 @@
"quit": "kilépés",
"random": "véletlenszerű",
"refresh": "frissítés",
"reset": "reszetelés",
"resetToDefault": "visszaállítás alapértelmezettekre",
"reset": "visszaállítás",
"resetToDefault": "visszaállítás alapértelmezettre",
"right": "jobb",
"save": "mentés",
"search": "keresés",
"title": "cím",
"trackNumber": "dalszám",
"trackNumber": "sáv",
"unknown": "ismeretlen",
"version": "verzió",
"yes": "igen",
@@ -101,7 +102,7 @@
"restartRequired": "újraindítás szükséges",
"setting": "beállítás",
"translation": "fordítás",
"rating": "értékelések",
"rating": "értékelés",
"reload": "újratöltés",
"share": "megosztás",
"sortOrder": "sorrend",
@@ -109,8 +110,22 @@
"saveAs": "mentés másként",
"size": "méret",
"year": "év",
"trackGain": "dal erősség",
"trackPeak": "dal csúcs"
"trackGain": "sáv erőssége",
"trackPeak": "sáv csúcsa",
"newVersion": "({{version}}) Új verzió telepítve",
"viewReleaseNotes": "kiadási jegyzetek megtekintése",
"bpm": "bpm",
"bitDepth": "bitmélység",
"additionalParticipants": "további közreműködök",
"private": "privát",
"public": "publikus",
"recordLabel": "lemezkiadó",
"explicit": "nyílt",
"clean": "tiszta",
"sampleRate": "mintavételi frekvencia",
"releaseType": "kiadás típusa",
"explicitStatus": "nyílt státusz",
"tags": "címkék"
},
"entity": {
"albumArtist_one": "album szerzője",
@@ -118,7 +133,7 @@
"albumArtistCount_one": "{{count}} album szerző",
"albumArtistCount_other": "{{count}} album szerzők",
"albumWithCount_one": "{{count}} album",
"albumWithCount_other": "{{count}} album",
"albumWithCount_other": "{{count}} albumok",
"artist_one": "előadó",
"artist_other": "előadók",
"favorite_one": "kedvelés",
@@ -126,31 +141,31 @@
"folder_one": "mappa",
"folder_other": "mappák",
"genreWithCount_one": "{{count}} műfaj",
"genreWithCount_other": "{{count}} műfaj",
"track_one": "dal",
"track_other": "dalok",
"song_one": "zene",
"song_other": "zenék",
"genreWithCount_other": "{{count}} műfajok",
"track_one": "sáv",
"track_other": "sávok",
"song_one": "dal",
"song_other": "dalok",
"album_one": "album",
"album_other": "albumok",
"smartPlaylist": "intelligens $t(entity.playlist_one)",
"artistWithCount_one": "{{count}} előadó",
"artistWithCount_other": "{{count}} előadó",
"artistWithCount_other": "{{count}} előadók",
"playlist_one": "lejátszási lista",
"playlist_other": "lejátszási listák",
"playlistWithCount_one": "{{count}} lejátszási lista",
"playlistWithCount_other": "{{count}} lejátszási lista",
"playlistWithCount_other": "{{count}} lejátszási listák",
"folderWithCount_one": "{{count}} mappa",
"folderWithCount_other": "{{count}} mappa",
"folderWithCount_other": "{{count}} mappák",
"genre_one": "műfaj",
"genre_other": "műfajok",
"play_one": "{{count}} lejátszás",
"play_other": "{{count}} lejátszás",
"trackWithCount_one": "{{count}} dal",
"trackWithCount_other": "{{count}} dal"
"play_other": "{{count}} lejátszások",
"trackWithCount_one": "{{count}} sáv",
"trackWithCount_other": "{{count}} sávok"
},
"error": {
"apiRouteError": "a kérést nem sikerült célbajuttatni",
"apiRouteError": "a kérést nem sikerült célba juttatni",
"audioDeviceFetchError": "hiba történt a hangeszközök lekérésekor",
"authenticationFailed": "sikertelen hitelesítés",
"credentialsRequired": "hitelesítési adatok szükségesek",
@@ -171,13 +186,15 @@
"sessionExpiredError": "a munkameneted lejárt",
"systemFontError": "hiba történt a rendszer betűtípusainak lekérésekor",
"serverRequired": "szerver szükséges",
"serverNotSelectedError": "nincs szerver kiválasztva"
"serverNotSelectedError": "nincs szerver kiválasztva",
"notificationDenied": "Az értesítések engedélyezése megtagadva. Ez a beállítás hatástalan",
"badValue": "érvénytelen opció \"{{value}}\". ez az érték már nem létezik"
},
"filter": {
"albumCount": "$t(entity.album_other) darab",
"bitrate": "bitráta",
"comment": "megjegyzés",
"dateAdded": "hozzáadva",
"dateAdded": "hozzáadás ideje",
"duration": "hossz",
"fromYear": "évtől",
"isCompilation": "gyűjtemény",
@@ -196,13 +213,13 @@
"criticRating": "kritikusok értékelése",
"communityRating": "közösségi értékelés",
"albumArtist": "$t(entity.albumArtist_one)",
"biography": "biográfia",
"biography": "életrajz",
"album": "$t(entity.album_one)",
"favorited": "kedvelt",
"isRecentlyPlayed": "mostanában lejátszott",
"name": "név",
"owner": "$t(common.owner)",
"id": "azonosító",
"id": "id",
"recentlyPlayed": "nemrég lejátszott",
"isFavorited": "kedvelt",
"search": "keresés",
@@ -211,7 +228,12 @@
"recentlyUpdated": "nemrég módosult",
"path": "elérési út",
"toYear": "évhez",
"trackNumber": "dalszám"
"trackNumber": "sáv",
"artist": "$t(entity.artist_one)",
"bpm": "bpm",
"channels": "$t(common.channel_other)",
"genre": "$t(entity.genre_one)",
"explicitStatus": "$t(common.explicitStatus)"
},
"form": {
"addServer": {
@@ -225,19 +247,23 @@
"title": "szerver hozzáadása",
"input_name": "szervernév",
"input_savePassword": "jelszó mentése",
"input_legacyAuthentication": "klasszikus hitelesítés bekapcsolása"
"input_legacyAuthentication": "klasszikus hitelesítés bekapcsolása",
"input_preferInstantMix": "az instant mixet részesítem előnyben",
"input_preferInstantMixDescription": "Az instant mix használata csak a hasonló dalokhoz javasolt. Hasznos, ha vannak olyan bővítmények, amelyek módosítják ezt a viselkedést"
},
"addToPlaylist": {
"input_skipDuplicates": "duplikátumok átugrása",
"input_playlists": "$t(entity.playlist_other)",
"success": "hozzáadtuk ezt: $t(entity.trackWithCount, {\"count\": {{message}} }) a következőhöz: $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })",
"title": "hozzáadás a következőhöz: $t(entity.playlist_one)"
"title": "hozzáadás a következőhöz: $t(entity.playlist_one)",
"create": "létrehoz $t(entity.playlist_one) {{playlist}}",
"searchOrCreate": "keresés $t(entity.playlist_other) vagy új létrehozása"
},
"createPlaylist": {
"input_description": "$t(common.description)",
"input_name": "$t(common.name)",
"input_owner": "$t(common.owner)",
"input_public": "nyilvános",
"input_public": "publikus",
"title": "$t(entity.playlist_one) létrehozása",
"success": "$t(entity.playlist_one) sikeresen létrehozva"
},
@@ -248,7 +274,603 @@
},
"editPlaylist": {
"success": "$t(entity.playlist_one) sikeresen módosítva",
"publicJellyfinNote": "A Jellyfin valamiért nem teszi közzé, hogy egy lejátszási lista nyilvános-e. Amennyiben azt szeretnéd, hogy nyilvános maradjon, válaszd ki az alábbi beviteli mezőt"
"publicJellyfinNote": "A Jellyfin valamiért nem teszi közzé, hogy egy lejátszási lista publikus-e vagy sem. Amennyiben azt szeretnéd, hogy publikus maradjon, válaszd ki az alábbi beviteli mezőt",
"title": "szerkesztés $t(entity.playlist_one)"
},
"lyricSearch": {
"input_artist": "$t(entity.artist_one)",
"input_name": "$t(common.name)",
"title": "dalszöveg kereső"
},
"queryEditor": {
"title": "lekérdezés szerkesztő",
"input_optionMatchAll": "összes egyezés",
"input_optionMatchAny": "bármelyik egyező"
},
"shareItem": {
"allowDownloading": "letöltés engedélyezése",
"description": "leírás",
"setExpiration": "lejárat beállítása",
"success": "link másolva a vágólapra (vagy kattintson ide a megnyitáshoz)",
"expireInvalid": "A lejáratnak a jövőben kell lennie",
"createFailed": "a megosztás létrehozása sikertelen (a megosztás engedélyezve van?)"
},
"updateServer": {
"success": "Szerver sikeresen frissítve",
"title": "Szerver frissítés"
},
"privateMode": {
"enabled": "privát mód engedélyezve, a lejátszási állapot mostantól rejtve marad a külső integrációk elől",
"disabled": "A privát mód le van tiltva, a lejátszási állapot mostantól látható az engedélyezett külső integrációk számára",
"title": "Privát mód"
}
},
"dragDropZone": {
"error_oneFileOnly": "Kérlek, csak 1 fájlt válassz",
"error_readingFile": "probléma merült fel a fájl olvasásakor: {{errorMessage}}",
"mainText": "húzd ide a fájlt"
},
"page": {
"albumArtistDetail": {
"about": "Az eladóról {{artist}}",
"appearsOn": "megjelenik",
"recentReleases": "legújabb kiadványok",
"viewDiscography": "Diszkográfia megtekintése",
"relatedArtists": "kapcsolódik $t(entity.artist_other)",
"topSongs": "sláger dalok",
"topSongsFrom": "sláger dalok tőle {{title}}",
"viewAll": "mindet megtekint",
"viewAllTracks": "mindet megtekint $t(entity.track_other)"
},
"albumArtistList": {
"title": "$t(entity.albumArtist_other)"
},
"albumDetail": {
"moreFromArtist": "még több ettől $t(entity.artist_one)",
"moreFromGeneric": "Még több {{item}}",
"released": "megjelent"
},
"albumList": {
"artistAlbums": "albumok tőle {{artist}}",
"genreAlbums": "\"{{genre}}\" $t(entity.album_other)",
"title": "$t(entity.album_other)"
},
"appMenu": {
"collapseSidebar": "összecsukni az oldalsávot",
"expandSidebar": "kibővíteni az oldalsávot",
"goBack": "menj vissza",
"goForward": "menj előre",
"manageServers": "szerverek kezelése",
"privateModeOff": "Privát mód kikapcsolása",
"privateModeOn": "Privát mód bekapcsolása",
"openBrowserDevtools": "böngésző fejlesztői eszközeinek megnyitása",
"quit": "$t(common.quit)",
"selectServer": "Szerver választása",
"settings": "$t(common.setting_other)",
"version": "verzió {{version}}"
},
"manageServers": {
"title": "Szerverek kezelés",
"serverDetails": "szerver adatok",
"url": "URL",
"username": "felhasználónév",
"editServerDetailsTooltip": "Szerver adok szerkesztése",
"removeServer": "távoli szerver"
},
"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": "letöltés",
"moveToNext": "$t(action.moveToNext)",
"moveToBottom": "$t(action.moveToBottom)",
"moveToTop": "$t(action.moveToTop)",
"numberSelected": "{{count}} kiválasztott",
"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": "elem megosztása",
"goToAlbum": "menj a $t(entity.album_one)",
"goToAlbumArtist": "menj a $t(entity.albumArtist_one)",
"showDetails": "információkérés"
},
"fullscreenPlayer": {
"config": {
"dynamicBackground": "dinamikus háttér",
"dynamicImageBlur": "kép elmosódás mérete",
"dynamicIsImage": "háttérkép engedélyezése",
"followCurrentLyric": "kövesd az aktuális dalszöveget",
"lyricAlignment": "dalszöveg igazítás",
"lyricOffset": "dalszöveg eltolása (ms)",
"lyricGap": "Dalszöveg hézag",
"lyricSize": "Dalszöveg méret",
"opacity": "áttetszőség",
"showLyricMatch": "mutasd az egyező dalszöveget",
"showLyricProvider": "mutasd a dalszöveg szolgáltatót",
"synchronized": "szinkronizálva",
"unsynchronized": "szinkronizálatlan",
"useImageAspectRatio": "kép arányának használata"
},
"lyrics": "dalszöveg",
"related": "kapcsolódó",
"upNext": "következik",
"visualizer": "vizualizáló",
"noLyrics": "nem található dalszöveg"
},
"genreList": {
"showAlbums": "mutasd a $t(entity.genre_one) $t(entity.album_other)",
"showTracks": "mutasd a $t(entity.genre_one) $t(entity.track_other)",
"title": "$t(entity.genre_other)"
},
"globalSearch": {
"commands": {
"goToPage": "menj az oldalra",
"searchFor": "keresd a {{query}}",
"serverCommands": "szerver parancsok"
},
"title": "parancsok"
},
"home": {
"explore": "fedezd fel könyvtárából",
"mostPlayed": "legtöbbet játszott",
"newlyAdded": "újonnan hozzáadott megjelenések",
"recentlyPlayed": "nemrég játszott",
"recentlyReleased": "nemrég megjelent",
"title": "$t(common.home)"
},
"itemDetail": {
"copyPath": "másolja az útvonalat a vágólapra",
"copiedPath": "útvonal sikeresen másolva",
"openFile": "mutasd a sávot a fájlkezelőbe"
},
"playlist": {
"reorder": "átrendezés csak ID szerinti rendezés esetén engedélyezett"
},
"playlistList": {
"title": "$t(entity.playlist_other)"
},
"setting": {
"advanced": "haladó",
"generalTab": "általános",
"hotkeysTab": "gyorsbillentyűk",
"windowTab": "ablak",
"playbackTab": "visszajátszás"
},
"sidebar": {
"albumArtists": "$t(entity.albumArtist_other)",
"albums": "$t(entity.album_other)",
"artists": "$t(entity.artist_other)",
"folders": "$t(entity.folder_other)",
"genres": "$t(entity.genre_other)",
"home": "$t(common.home)",
"myLibrary": "Könyvtáram",
"nowPlaying": "most játszott",
"playlists": "$t(entity.playlist_other)",
"search": "$t(common.search)",
"settings": "$t(common.setting_other)",
"shared": "megosztott $t(entity.playlist_other)",
"tracks": "$t(entity.track_other)"
},
"trackList": {
"artistTracks": "sávok tőle {{artist}}",
"genreTracks": "\"{{genre}}\" $t(entity.track_other)",
"title": "$t(entity.track_other)"
}
},
"player": {
"addLast": "utoljára hozzáadva",
"addNext": "következő hozzáadása",
"favorite": "kedvenc",
"mute": "némítás",
"muted": "némítva",
"next": "következő",
"play": "lejátszás",
"playbackFetchCancel": "Ez eltart egy ideig… zárja be az értesítést a megszakításhoz",
"playbackFetchInProgress": "dalok betöltése…",
"playbackFetchNoResults": "nem találhatóak dalok",
"playbackSpeed": "lejátszási sebesség",
"playRandom": "véletlenszerű lejátszás",
"playSimilarSongs": "hasonló dalok lejátszása",
"previous": "előző",
"queue_clear": "műsorlista kiürítése",
"queue_moveToBottom": "kiválasztott elem feljebb mozgatása",
"queue_moveToTop": "kiválasztott elem lejjebb mozgatása",
"queue_remove": "kiválasztott elem eltávolítása",
"repeat": "ismétlés",
"repeat_all": "összes ismétlése",
"repeat_off": "ismétlés kikapcsolva",
"shuffle": "kevert lejátszás",
"skip": "ugrás",
"skip_back": "visszaugrás",
"skip_forward": "előre ugrás",
"stop": "állj",
"toggleFullscreenPlayer": "teljes képernyős lejátszó bekapcsolása",
"unfavorite": "kedvencekből eltávolítás",
"pause": "szünet",
"viewQueue": "műsorlista megtekintése",
"shuffle_off": "kevert lejátszás ki"
},
"releaseType": {
"primary": {
"album": "$t(entity.album_one)",
"broadcast": "sugárzás",
"ep": "ep",
"other": "más",
"single": "kislemez"
},
"secondary": {
"audiobook": "hangoskönyv",
"audioDrama": "rádió dráma",
"compilation": "összeállítás",
"djMix": "dj mix",
"demo": "demo",
"fieldRecording": "helyszíni felvétel",
"interview": "interjú",
"live": "élő",
"mixtape": "kazetta mix",
"remix": "remix",
"soundtrack": "filmzene",
"spokenWord": "beszélt szó"
}
},
"setting": {
"accentColor_description": "beállítja az alkalmazás kiemelő színét",
"accentColor": "kiemelő szín",
"albumBackground_description": "háttérképet ad hozzá az albumborítót tartalmazó albumoldalakhoz",
"albumBackground": "album háttérkép",
"albumBackgroundBlur_description": "beállítja az album háttérképre alkalmazott elmosódás mértékét",
"albumBackgroundBlur": "album háttérkép elmosódás mérete",
"applicationHotkeys_description": "alkalmazás gyorsbillentyűk konfigurálása. jelöld be a jelölőnégyzetet, ha globális gyorsbillentyűként szeretnéd beállítani (csak asztali számítógépen)",
"applicationHotkeys": "alkalmazás gyorsbillentyűk",
"artistBackground": "előadó háttérkép",
"artistBackground_description": "háttérképet ad hozzá a előadó oldalához, amely tartalmazza a előadó stílusát",
"artistBackgroundBlur": "előadó háttérkép elmosódás mértéke",
"artistBackgroundBlur_description": "beállítja az előadó háttérképére alkalmazott elmosódás mértékét",
"artistConfiguration": "album előadójának oldal konfigurációja",
"artistConfiguration_description": "konfigurálja, hogy mely elemek jelenjenek meg, és milyen sorrendben, az album előadó oldalán",
"audioDevice_description": "válaszd ki a lejátszáshoz használni kívánt hangeszközt (csak webes lejátszó esetén)",
"audioDevice": "hangeszköz",
"audioExclusiveMode_description": "engedélyezze az exkluzív kimeneti módot. Ebben a módban a rendszer általában le van zárva, és csak az mpv képes hangot kiadni",
"audioExclusiveMode": "exkluzív audio mód",
"audioPlayer_description": "válaszd ki a lejátszáshoz használni kívánt audiolejátszót",
"audioPlayer": "audiolejátszó",
"buttonSize_description": "a lejátszó sáv gombjainak mérete",
"buttonSize": "lejátszó sáv gomb méret",
"clearCache_description": "a Feishin „kemény törlése”. a Feishin cache-ének törlése mellett ürítse ki a böngésző cache-ét (mentett képek és egyéb eszközök). a szerver hitelesítő adatai és beállításai megmaradnak",
"clearCache": "böngésző gyorsítótár törlése",
"clearCacheSuccess": "gyorsítótár sikeresen törölve",
"clearQueryCache_description": "A Feishin „soft clear” funkciója. Ez frissíti a lejátszási listákat, a zeneszámok metaadatait és visszaállítja a mentett dalszövegeket. A beállítások, a szerver hitelesítő adatok és a gyorsítótárban tárolt képek megmaradnak",
"clearQueryCache": "Feishin gyorsítótár törlése",
"contextMenu_description": "lehetővé teszi, hogy elrejtsd azokat az elemeket, amelyek a menüben megjelennek, amikor jobb gombbal kattintasz egy elemre. A bejelölés nélküli elemek el lesznek rejtve",
"releaseChannel_optionBeta": "béta",
"releaseChannel_optionLatest": "legújabb",
"releaseChannel": "kiadási csatorna",
"customCss": "egyéni css",
"customCssEnable_description": "lehetővé teszi az egyéni css írását",
"customCssEnable": "egyéni css engedélyezése",
"disableAutomaticUpdates": "automatikus frissítés kikapcsolása",
"customFontPath": "egyéni betűtípus elérési út",
"customCss_description": "egyéni css tartalom. Megjegyzés: a tartalom és a távoli URL-ek nem megengedett tulajdonságok. A tartalom előnézete az alábbiakban látható. A tisztítás miatt további mezők is megjelennek, amelyeket te nem állítottál be",
"customCssNotice": "Figyelem: bár van némi tisztítás (az url() és a content: használata nem engedélyezett), az egyéni css használata továbbra is kockázatot jelenthet, mivel megváltoztatja a felületet",
"customFontPath_description": "beállítja az alkalmazáshoz használandó egyéni betűtípus elérési útját",
"contextMenu": "kontextusmenü (jobb klikk) beállítás",
"crossfadeDuration_description": "beállítja a crossfade effekt időtartamát",
"crossfadeDuration": "crossfade időtartam",
"crossfadeStyle": "crossfade stílus",
"crossfadeStyle_description": "válaszd ki az audiolejátszóhoz használni kívánt crossfade stílust",
"releaseChannel_description": "válassz a stabil kiadás vagy a béta kiadás közül az automatikus frissítésekhez",
"disableLibraryUpdateOnStartup": "új verziók ellenőrzését indításkor letiltása",
"discordDisplayType_artistname": "előadó név",
"discordDisplayType_description": "megváltoztatja, hogy mit hallgatsz az állapotodban",
"discordDisplayType_songname": "dal címe",
"discordApplicationId_description": "az alkalmazás {{discord}} rich presence ID-ja (alapértelmezés szerint {{defaultId}})",
"discordApplicationId": "{{discord}} applikáció id",
"discordDisplayType": "{{discord}} presence megjelenítési típus",
"discordIdleStatus_description": "ha engedélyezve van, frissítse az állapotot, amikor a lejátszó nem aktív",
"discordIdleStatus": "mutasd a rich presence üresjárati állapotát",
"discordLinkType_description": "külső linkeket ad hozzá a {{lastfm}} vagy {{musicbrainz}} mezőkhöz a {{discord}} rich presence dal és előadó mezőiben. A {{musicbrainz}} a legpontosabb, de címkéket igényel, de nem biztosít előadói linkeket, míg a {{lastfm}} mindig biztosít linket, de nem generál extra hálózati kéréseket",
"discordLinkType_mbz_lastfm": "{{musicbrainz}} {{lastfm}} tartalék megoldással",
"discordLinkType_none": "$t(common.none)",
"discordLinkType": "{{discord}} presence linkek",
"discordListening_description": "állapot megjelenítése hallgatásként a lejátszás helyett",
"discordListening": "állapot megjelenítése hallgatásként",
"discordPausedStatus_description": "ha engedélyezve van, a lejátszó szüneteltetésekor megjelenik az állapot",
"discordPausedStatus": "rich presence megjelenítése szüneteltetéskor",
"discordRichPresence": "{{discord}} rich presence",
"discordRichPresence_description": "engedélyezze a lejátszási állapotot a {{discord}} rich presence-ben. A kép kulcsok: {{icon}}, {{playing}} és {{paused}}",
"discordServeImage": "{{discord}} képek kiszolgálása a szerverről",
"discordServeImage_description": "megosztja a {{discord}} borítóképet a szerverről (csak Jellyfin és Navidrome esetén elérhető). A {{discord}} botot használ a képek letöltéséhez, ezért a szervernek elérhetőnek kell lennie a nyilvános interneten",
"discordUpdateInterval": "{{discord}} rich presence frissítési intervallum",
"discordUpdateInterval_description": "az egyes frissítések közötti idő másodpercben (minimum 15 másodperc)",
"doubleClickBehavior_description": "Ha igaz, akkor a keresés során talált összes megfelelő szám a műsorlistára kerül. Ellenkező esetben csak a rákattintott szám kerül a műsorlistára",
"doubleClickBehavior": "dupla kattintással az összes keresett zeneszámot műsorlistára teszi",
"enableAutoTranslation_description": "a dalszöveg betöltésekor automatikusan engedélyezze a fordítást",
"enableAutoTranslation": "automatikus fordítás engedélyezése",
"enableRemote_description": "lehetővé teszi egy távoli vezérlő szerver számára, hogy más eszközök vezéreljék az alkalmazást",
"enableRemote": "távoli vezérlő szerver engedélyezése",
"exitToTray_description": "kilépés az alkalmazásból a tálcára",
"exitToTray": "kilépés a tálcára",
"exportImportSettings_control_description": "beállítások exportálása és importálása JSON-on keresztül",
"exportImportSettings_control_exportText": "beállítások exportálása",
"exportImportSettings_control_importText": "beállítások importálása",
"exportImportSettings_control_title": "beállítások exportálása és importálása",
"exportImportSettings_destructiveWarning": "A beállítások importálása végleges, ezért kérlek, olvasd el a fenti információkat, mielőtt rákattintasz az alábbi „Importálás” gombra!",
"exportImportSettings_importBtn": "beállítások importálása",
"exportImportSettings_importModalTitle": "Feishin beállítások importálása",
"exportImportSettings_importSuccess": "A beállítások sikeresen importálva!",
"exportImportSettings_notValidJSON": "A megadott fájl nem érvényes JSON",
"exportImportSettings_offendingKeyError": "\"{{offendingKey}}\" helytelen - {{reason}}",
"externalLinks_description": "lehetővé teszi külső linkek (Last.fm, MusicBrainz) megjelenítését az előadó/album oldalakon",
"externalLinks": "külső linkek megjelenítése",
"floatingQueueArea_description": "Lebegő ikon megjelenítése a képernyő jobb oldalán a műsorlista megnyitásához",
"floatingQueueArea": "Lebegő műsorlista területének megjelenítése",
"followLyric_description": "görgess a dalszöveghez az aktuális lejátszási pozícióig",
"followLyric": "kövesd az aktuális dalszöveget",
"font_description": "beállítja az alkalmazáshoz használandó betűtípust",
"font": "betűtípus",
"fontType_description": "A beépített betűtípus a Feishin által biztosított betűtípusok közül választ. A rendszerbetűtípus lehetővé teszi az operációs rendszer által biztosított bármely betűtípus kiválasztását. Az egyéni beállítás lehetővé teszi saját betűtípus megadását",
"fontType_optionBuiltIn": "beépített betűtípus",
"fontType_optionCustom": "egyedi betűtípus",
"fontType_optionSystem": "rendszer betűtípus",
"fontType": "Font típusa",
"gaplessAudio_description": "Beállítja az MPV résmentes (gapless) lejátszását",
"gaplessAudio_optionWeak": "gyenge (ajánlott)",
"gaplessAudio": "hézagmentes hang",
"genreBehavior_description": "meghatározza, hogy egy műfajra kattintva alapértelmezés szerint a zeneszámok vagy az albumok listája nyílik-e meg",
"genreBehavior": "műfaj oldal alapértelmezett viselkedése",
"globalMediaHotkeys_description": "engedélyezheted vagy letilthatod a rendszer média gyorsbillentyűinek használatát a lejátszás vezérléséhez",
"globalMediaHotkeys": "globális média gyorsbillentyűk",
"homeConfiguration_description": "beállíthatod, hogy mely elemek jelenjenek meg, és milyen sorrendben a kezdőlapon",
"homeConfiguration": "kezdőlap konfigurációja",
"homeFeature_description": "ellenőrzi, hogy megjelenjen-e a nagy, kiemelt csúszka a kezdőlapon",
"hotkey_favoriteCurrentSong": "kedvenc $t(common.currentSong)",
"hotkey_favoritePreviousSong": "kedvenc $t(common.previousSong)",
"hotkey_globalSearch": "globális keresés",
"hotkey_localSearch": "oldalon belüli keresés",
"hotkey_browserBack": "Vissza a böngészőben",
"hotkey_navigateHome": "Ugrás a kezdőlapra",
"hotkey_browserForward": "Ugrás előre",
"homeFeature": "Kiemelt tartalmak csúszkája a kezdőlapon",
"hotkey_playbackNext": "következő szám",
"hotkey_playbackPause": "szünet",
"hotkey_playbackPlay": "lejátszás",
"hotkey_playbackPlayPause": "lejátszás/szünet",
"hotkey_playbackPrevious": "előző szám",
"hotkey_playbackStop": "állj",
"hotkey_rate0": "értékelés törlés",
"hotkey_rate1": "értékelés 1 csillag",
"hotkey_rate2": "értékelés 2 csillag",
"hotkey_rate3": "értékelés 3 csillag",
"hotkey_rate4": "értékelés 4 csillag",
"hotkey_rate5": "értékelés 5 csillag",
"hotkey_skipBackward": "visszaugrás",
"hotkey_skipForward": "előre ugrás",
"hotkey_volumeDown": "hangerő le",
"hotkey_volumeMute": "elnémítás",
"hotkey_volumeUp": "hangerő fel",
"hotkey_toggleCurrentSongFavorite": "kedvencek $t(common.currentSong) közé",
"hotkey_toggleFullScreenPlayer": "teljes képernyős lejátszóra váltás",
"hotkey_togglePreviousSongFavorite": "kedvencek $t(common.previousSong) közé",
"hotkey_toggleQueue": "lejátszási sorra váltása",
"hotkey_toggleRepeat": "ismétlésre váltás",
"hotkey_toggleShuffle": "keverésre váltás",
"hotkey_zoomIn": "nagyítás",
"hotkey_zoomOut": "kicsinyítés",
"imageAspectRatio_description": "Ha engedélyezve van, a borítóképet natív képarányban jeleníti meg. Az 1:1-től eltérő képarányú borítók esetében a fennmaradó hely üresen marad",
"imageAspectRatio": "használja a borító eredeti képarányát",
"language": "nyelv",
"hotkey_unfavoriteCurrentSong": "kedvencekből eltávolítás $t(common.currentSong)",
"hotkey_unfavoritePreviousSong": "kedvencekből eltávolítás $t(common.previousSong)",
"lastfmApiKey": "{{lastfm}} API kulcs",
"lyricFetch_description": "dalszövegek lekérése különböző internetes forrásokból",
"lyricFetch": "dalszövegek letöltése az internetről",
"lastfm": "mutasd a last.fm linkeket",
"lastfmApiKey_description": "a {{lastfm}} API kulcsa. szükséges a borítóhoz",
"language_description": "beállítja az alkalmazás nyelvét ($t(common.restartRequired))",
"lastfm_description": "mutasd a Last.fm linkeket az előadó/album oldalakon",
"lyricFetchProvider_description": "válaszd ki azokat a szolgáltatókat, amelyekről a dalszövegeket szeretnéd letölteni. A szolgáltatók sorrendje megegyezik a lekérdezés sorrendjével",
"lyricFetchProvider": "Dalszöveg szolgáltatók",
"lyricOffset_description": "a szöveget a megadott ms értékkel eltolja",
"lyricOffset": "szövegeltolás (ms)",
"minimizeToTray": "minimalizálás a tálcára",
"minimizeToTray_description": "alkalmazás minimalizálása a tálcára",
"musicbrainz": "mutasd a MusicBrainz linkeket",
"playbackStyle_optionNormal": "normal",
"playbackStyle": "lejátszás stílusa",
"playButtonBehavior_description": "beállítja a lejátszás gomb alapértelmezett viselkedését, amikor dalokat adunk a műsorlistához",
"playButtonBehavior_optionAddLast": "$t(player.addLast)",
"playButtonBehavior_optionAddNext": "$t(player.addNext)",
"playButtonBehavior_optionPlay": "$t(player.play)",
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
"playButtonBehavior": "lejátszás gomb viselkedése",
"playerAlbumArtResolution_description": "A nagy lejátszó albumborító-előnézetének felbontása. A nagyobb érték élesebb képet ad, de lassíthatja a betöltést. Alapértelmezés: 0, ami az automatikus módot jelenti",
"minimumScrobblePercentage_description": "a szám lejátszásának minimális százaléka, amelynek el kell hangzania, mielőtt Scrobble-nak számít",
"minimumScrobblePercentage": "Minimális Scrobble arány (százalék)",
"minimumScrobbleSeconds": "Minimum Scrobble arány (mp)",
"mpvExecutablePath": "Mpv elérési útja",
"mpvExecutablePath_description": "beállítja az Mpv elérési útját. Ha üresen hagyod, az alapértelmezett elérési út lesz használva",
"minimumScrobbleSeconds_description": "a szám lejátszásának minimális hossza, amelynek el kell hangzania, mielőtt Scrobble-nak számít",
"mpvExtraParameters_help": "soronként",
"musicbrainz_description": "mutasd a MusicBrainz linkjeit az előadó/album oldalakon, ahol MusicBrainz ID létezik",
"neteaseTranslation_description": "Ha engedélyezve van, letölti és megjeleníti a NetEase-ről a lefordított dalszövegeket, ha azok rendelkezésre állnak",
"neteaseTranslation": "Engedélyezi a NetEase fordításokat",
"notify": "bekapcsolja a dal értesítéseket",
"notify_description": "értesítések megjelenítése az aktuális dal megváltoztatásakor",
"playbackStyle_description": "válaszd ki az lejátszóhoz használni kívánt lejátszási stílust",
"playerAlbumArtResolution": "lejátszó albumborító felbontás",
"playerbarOpenDrawer_description": "lehetővé teszi a lejátszósávra kattintással a teljes képernyős lejátszó megnyitását",
"playerbarOpenDrawer": "lejátszósáv teljes képernyőre váltás",
"preferLocalLyrics_description": "ha elérhető, akkor a távoli dalszövegek helyett a helyi dalszövegeket részesítse előnyben",
"preferLocalLyrics": "előnybe részesített dalszöveg",
"preservePitch_description": "megőrzi a hangmagasságot a lejátszási sebesség módosításakor",
"preservePitch": "hangmagasság megőrzése",
"preventSleepOnPlayback_description": "megakadályozza, hogy a kijelző alvó módba kerüljön zene lejátszása közben",
"preventSleepOnPlayback": "megakadályozza az alvó módot lejátszás közben",
"remotePassword_description": "beállítja a távoli vezérlő szerver jelszavát. Ezek a hitelesítő adatok alapértelmezés szerint nem biztonságosan kerülnek továbbításra, ezért olyan egyedi jelszót kell használnod, amely nem fontosak neked",
"remotePassword": "távoli vezérlő szerver jelszava",
"remotePort_description": "beállítja a távoli vezérlő szerver portját",
"remotePort": "távoli vezérlő szerver portja",
"remoteUsername_description": "beállítja a távoli vezérlő szerver felhasználónevét. Ha mind a felhasználónév, mind a jelszó üres, a hitelesítés le lesz tiltva",
"remoteUsername": "távoli vezérlő szerver felhasználónév",
"passwordStore_description": "jelszó/titkos tároló kiválasztása. Módosítsd, ha problémát tapasztalsz a jelszavak tárolásánál",
"passwordStore": "jelszó/titkos tároló",
"playbackStyle_optionCrossFade": "crossfade",
"replayGainMode_optionAlbum": "$t(entity.album_one)",
"replayGainMode_optionNone": "$t(common.none)",
"replayGainMode_optionTrack": "$t(entity.track_one)",
"replayGainMode": "{{ReplayGain}} mód",
"replayGainClipping_description": "A {{ReplayGain}} által okozott torzítás megelőzése az erősítés automatikus csökkentésével",
"replayGainClipping": "{{ReplayGain}} torzítás",
"replayGainMode_description": "A hangerő erősítés beállítása a fájl metaadataiban tárolt {{ReplayGain}} értékek szerint",
"replayGainFallback_description": "Az alkalmazandó erősítés dB-ben, ha a fájl nem tartalmaz {{ReplayGain}} címkét",
"sampleRate_description": "A kimeneti mintavételi frekvencia kiválasztása, ha a beállított frekvencia eltér a jelenlegi médiáétól. 8000 alatti érték esetén az alapértelmezett frekvencia kerül használatra",
"sampleRate": "mintavételi frekvencia",
"savePlayQueue_description": "A műsorlista mentése bezárásakor, és visszaállítása az alkalmazás megnyitásakor",
"replayGainPreamp": "{{ReplayGain}} előerősítés (dB)",
"replayGainFallback": "{{ReplayGain}} fallback",
"replayGainPreamp_description": "állítsd be a {{ReplayGain}} -re vonatkozó előerősítő értéket",
"scrobble": "Scrobble",
"showSkipButton_description": "a lejátszó sávon megjelenő átugrás gombok megjelenítése vagy elrejtése",
"showSkipButton": "mutasd az átugrás gombot",
"savePlayQueue": "műsorlista mentése",
"scrobble_description": "a lejátszás Scrobble-elése a médiaszerveredre",
"showSkipButtons_description": "a lejátszó sávon megjelenő átugrás gombok megjelenítése vagy elrejtése",
"showSkipButtons": "mutasd az átugrás gombot",
"sidebarConfiguration_description": "Válaszd ki az oldalsávban megjelenő elemeket és azok sorrendjét",
"sidebarCollapsedNavigation_description": "Navigáció megjelenítése vagy elrejtése az összecsukott oldalsávban",
"sidebarCollapsedNavigation": "Összecsukott oldalsáv navigációja",
"sidebarConfiguration": "oldalsáv konfigurációja",
"theme": "téma",
"sidebarPlaylistList_description": "lejátszási lista megjelenítése vagy elrejtése az oldalsávban",
"sidebarPlaylistList": "oldalsáv lejátszási lista",
"sidePlayQueueStyle_description": "beállítja az oldalsó műsorlista stílusát",
"mediaSession_description": "lehetővé teszi a Windows Media Session integrációját, a médiavezérlők és metaadatok megjelenítését a rendszer hangerő-átfedésben és a zárolási képernyőn (csak Windows)",
"mediaSession": "média munkamenet engedélyezése",
"sidePlayQueueStyle": "oldalsó műsorlista stílusa",
"skipDuration": "átugrás hossza",
"skipPlaylistPage_description": "lejátszási listához való navigáláskor az alapértelmezett oldal helyett a lejátszási lista dalainak listájára mutató oldalra lépjen",
"skipPlaylistPage": "lejátszási lista oldalának átugrása",
"sidePlayQueueStyle_optionAttached": "rögzítve",
"sidePlayQueueStyle_optionDetached": "leválasztva",
"skipDuration_description": "beállítja az átugrás hosszát a lejátszósáv ugrás gombjainak használatakor",
"startMinimized_description": "Az alkalmazás indítása a tálcán",
"themeDark": "téma (sötét)",
"themeLight": "téma (világos)",
"themeDark_description": "sötét téma beállítása",
"themeLight_description": "világos téma beállítása",
"translationApiKey": "fordítási api kulcs",
"translationApiProvider_description": "fordításhoz szükséges api szolgáltató",
"translationApiProvider": "fordítási api szolgáltató",
"translationApiKey_description": "fordításhoz szükséges api kulcs (csak globális szolgáltatás végpont)",
"transcode_description": "átkódolás más formátumba",
"transcodeBitrate_description": "bitráta választás az átkódoláshoz. A 0 azt jelenti, hogy a szerver választ",
"transcodeBitrate": "átkódolás bitráta",
"transcodeFormat_description": "formátum választás átkódoláshoz.Ha üresen hagyod, akkor a szerver választ",
"transcodeFormat": "átkódolás formátuma",
"transcode": "átkódolás engedélyezése",
"startMinimized": "indítás minimalizálva",
"theme_description": "Beállítja az alkalmazás témáját",
"translationTargetLanguage_description": "fordítás célnyelve",
"trayEnabled": "tálca megjelenítése",
"useSystemTheme_description": "A rendszer által megadott világos/sötét mód követése",
"translationTargetLanguage": "fordítás célnyelve",
"trayEnabled_description": "tálcaikon/menü megjelenítése/elrejtése (kikapcsolva: nincs tálcára küldés)",
"useSystemTheme": "rendszer téma használata",
"zoom": "nagyítási arány",
"webAudio": "web audio használata",
"windowBarStyle_description": "válaszd ki az címsor stílusát",
"windowBarStyle": "címsor stílusa",
"zoom_description": "beállítja az alkalmazás nagyítási arányát",
"volumeWheelStep_description": "A hangerő változásának mértéke az egérgörgő használatakor a hangerő sávon",
"volumeWheelStep": "hangerőgörgő lépés",
"volumeWidth": "hangerő sáv szélessége",
"webAudio_description": "Web Audio használata. Ez lehetővé teszi a fejlettebb funkciókat, például a ReplayGain-t. (Kapcsold ki, ha problémát tapasztalsz)",
"volumeWidth_description": "hangerő sáv szélessége"
},
"table": {
"config": {
"label": {
"path": "$t(common.path)",
"playCount": "lejátszások száma",
"rating": "$t(common.rating)",
"releaseDate": "megjelenés dátuma",
"rowIndex": "sor index",
"size": "$t(common.size)",
"songCount": "$t(entity.track_other)",
"title": "$t(common.title)",
"titleCombined": "$t(common.title) (kombinált)",
"trackNumber": "szám",
"year": "$t(common.year)",
"genre": "$t(entity.genre_one)",
"lastPlayed": "utoljára játszott",
"note": "$t(common.note)",
"owner": "$t(common.owner)",
"albumArtist": "$t(entity.albumArtist_one)",
"artist": "$t(entity.artist_one)",
"biography": "$t(common.biography)",
"bitrate": "$t(common.bitrate)",
"bpm": "$t(common.bpm)",
"channels": "$t(common.channel_other)",
"codec": "$t(common.codec)",
"dateAdded": "hozzáadva",
"discNumber": "lemezszám",
"duration": "$t(common.duration)",
"favorite": "$t(common.favorite)",
"actions": "$t(common.action_other)",
"album": "$t(entity.album_one)"
},
"view": {
"card": "kártya",
"grid": "rács",
"list": "lista",
"poster": "poszter",
"table": "táblázat"
},
"general": {
"autoFitColumns": "oszlopok automatikus igazítása",
"followCurrentSong": "kövesd az jelenlegi dalt",
"displayType": "kijelző típusa",
"gap": "$t(common.gap)",
"size": "$t(common.size)",
"tableColumns": "táblázat oszlopai",
"itemGap": "elemek közötti távolság (px)",
"itemSize": "elem mérete (px)"
}
},
"column": {
"albumCount": "$t(entity.album_other)",
"artist": "$t(entity.artist_one)",
"biography": "életrajz",
"bitrate": "bitráta",
"bpm": "bpm",
"channels": "$t(common.channel_other)",
"codec": "$t(common.codec)",
"comment": "komment",
"dateAdded": "hozzáadva",
"discNumber": "lemez",
"favorite": "kedvenc",
"genre": "$t(entity.genre_one)",
"lastPlayed": "utoljára játszott",
"path": "elérési út",
"playCount": "lejátszások",
"rating": "értékelés",
"releaseDate": "megjelenés",
"releaseYear": "év",
"size": "$t(common.size)",
"songCount": "$t(entity.track_other)",
"title": "cím",
"trackNumber": "sáv",
"album": "album",
"albumArtist": "album előadó"
}
}
}
+315 -64
View File
@@ -22,13 +22,15 @@
"queue_clear": "キューをクリア",
"muted": "ミュート中",
"unfavorite": "お気に入り解除",
"queue_moveToTop": "選択項目を先末尾に移動",
"queue_moveToTop": "選択項目を一番下に移動",
"queue_moveToBottom": "選択項目を先頭に移動",
"shuffle_off": "シャッフル無効",
"addLast": "最後へ追加",
"mute": "ミュート",
"skip_forward": "次へスキップ",
"pause": "一時停止"
"pause": "一時停止",
"playSimilarSongs": "似たような曲を再生する",
"viewQueue": "キューを表示する"
},
"setting": {
"crossfadeStyle_description": "オーディオプレーヤーが使用するクロスフェードのスタイルを選択します",
@@ -36,14 +38,14 @@
"hotkey_skipBackward": "前にスキップ",
"replayGainMode_description": "ファイルのメタデータに保存されている {{ReplayGain}} 値に従って音量ゲインを調整します",
"volumeWheelStep_description": "音量スライダーでマウスホイールをスクロールしたときに変化する音量を設定します",
"audioDevice_description": "再生に使用するオーディオデバイスを選択します (Webプレーヤーのみ)",
"audioDevice_description": "再生に使用するオーディオデバイスを選択します (Web プレーヤーのみ)",
"theme_description": "アプリケーションに使用するテーマを設定します",
"hotkey_playbackPause": "一時停止",
"replayGainFallback": "{{ReplayGain}} フォールバック",
"sidebarCollapsedNavigation_description": "折りたたみサイドバーのナビゲーションを表示/非表示にします",
"hotkey_volumeUp": "音量を上げる",
"skipDuration": "スキップの長さ",
"discordIdleStatus_description": "プレヤーがアイドル状態でもステータスを更新します",
"discordIdleStatus_description": "有効にすると、プレヤーがアイドル状態でもステータスを更新します",
"showSkipButtons": "スキップボタンを表示",
"playButtonBehavior_optionPlay": "$t(player.play)",
"minimumScrobblePercentage": "最小 Scrobble 時間 (%)",
@@ -52,20 +54,20 @@
"skipDuration_description": "プレーヤーバーのスキップボタンでスキップする時間を設定します",
"enableRemote_description": "リモートコントロール サーバーを有効化し、他のデバイスからアプリケーションを制御できるようにします",
"fontType_optionSystem": "システムフォント",
"mpvExecutablePath_description": "mpvの実行ファイルが存在するパスを設定します",
"mpvExecutablePath_description": "MPV を実行するファイルパスを設定します。空のままにすると、デフォルトのパスが使用されます",
"replayGainClipping_description": "自動的にゲインを下げて {{ReplayGain}} によるクリッピングを防ぎます",
"replayGainPreamp": "{{ReplayGain}} プリアンプ (dB)",
"hotkey_favoriteCurrentSong": "$t(common.currentSong) をお気に入り",
"sampleRate": "サンプルレート",
"sidePlayQueueStyle_optionAttached": "結合",
"sidebarConfiguration": "サイドバー設定",
"sampleRate_description": "設定とメディアのサンプル周波数が異なる場合に使用する出力サンプルレートを選択します",
"sampleRate_description": "選択したサンプル周波数が現在のメディアの周波数と異なる場合に使用する出力サンプルレートを選択します。8000 未満の値を指定するとデフォルトの周波数が使用されます",
"replayGainMode_optionNone": "$t(common.none)",
"replayGainClipping": "{{ReplayGain}} クリッピング",
"hotkey_zoomIn": "拡大",
"scrobble_description": "再生した音楽をメディアサーバーから Scrobble します",
"hotkey_browserForward": "ブラウザ 進む",
"audioExclusiveMode_description": "専用の排他出力モードを有効にします。 このモードでは、システムの他の出力がロックされ、mpvだけがオーディオを出力できるようになります",
"audioExclusiveMode_description": "排他出力モードを有効にします。このモードでは、システムの他の出力がロックされ、MPV のみがオーディオを出力できるようになります",
"discordUpdateInterval": "{{discord}} Rich Presenceアップデート間隔",
"themeLight": "テーマ (ライト)",
"fontType_optionBuiltIn": "組み込みフォント",
@@ -73,7 +75,7 @@
"hotkey_rate1": "1つ星で評価",
"hotkey_skipForward": "次へスキップ",
"disableLibraryUpdateOnStartup": "起動時の新バージョンチェックを無効にします",
"discordApplicationId_description": "{{discord}} にRich Presenceステータスを表示するためのアプリケーションID (デフォルトは {{defaultId}} です)",
"discordApplicationId_description": "{{discord}} に Rich Presence ステータスを表示するためのアプリケーション ID (デフォルトは {{defaultId}} です)",
"sidePlayQueueStyle": "サイド再生キュースタイル",
"gaplessAudio": "ギャップレス再生",
"playButtonBehavior_optionAddLast": "$t(player.addLast)",
@@ -86,19 +88,19 @@
"audioPlayer_description": "再生に使用するオーディオプレーヤーを選択します",
"globalMediaHotkeys": "グローバルメディアホットキー",
"hotkey_globalSearch": "グローバル検索",
"gaplessAudio_description": "mpv用にギャップレス再生を設定します",
"gaplessAudio_description": "MPV 向けのギャップレス再生を設定します",
"remoteUsername_description": "リモートコントロール サーバーのユーザ名を設定します。 ユーザー名とパスワードの両方が空の場合、認証は無効になります",
"disableAutomaticUpdates": "自動更新を無効化",
"exitToTray_description": "アプリケーション終了ボタンが押された際、システムトレイに格納します",
"followLyric_description": "現在の再生位置に歌詞をスクロールします",
"hotkey_favoritePreviousSong": "$t(common.previousSong) をお気に入り",
"replayGainMode_optionAlbum": "$t(entity.album_one)",
"lyricOffset": "歌詞オフセット (ミリ秒)",
"lyricOffset": "歌詞オフセット (ミリ秒)",
"discordUpdateInterval_description": "更新間隔 (秒単位, 最小15秒)",
"fontType_optionCustom": "カスタムフォント",
"themeDark_description": "アプリケーションに使用するダークテーマを設定します",
"audioExclusiveMode": "オーディオ排他モード",
"remotePassword": "リモートコントロール サーバー パスワード",
"remotePassword": "リモートコントロールサーバーパスワード",
"lyricFetchProvider": "歌詞取得先",
"language_description": "アプリケーションの言語を設定します ($t(common.restartRequired))",
"playbackStyle_optionCrossFade": "クロスフェード",
@@ -119,20 +121,20 @@
"hotkey_toggleShuffle": "シャッフルの切り替え",
"theme": "テーマ",
"playbackStyle_description": "オーディオプレーヤーに使用する再生スタイルを選択します",
"discordRichPresence_description": "{{discord}} Rich Presence再生ステータスを表示するようにします。画像キー: {{icon}}, {{playing}}, {{paused}}",
"mpvExecutablePath": "mpv 実行ファイルパス",
"discordRichPresence_description": "{{discord}} Rich Presence再生ステータスを有効にします。画像キー: {{icon}}, {{playing}}, {{paused}}",
"mpvExecutablePath": "MPV 実行ファイルパス",
"audioDevice": "オーディオデバイス",
"hotkey_rate2": "2つ星で評価",
"playButtonBehavior_description": "キューに曲を追加するときの再生ボタンのデフォルトの動作を設定します",
"minimumScrobblePercentage_description": "Scrobbleされるために必要な最短の再生時間(%)",
"minimumScrobblePercentage_description": "Scrobble されるために必要な最短の再生時間 (%)",
"exitToTray": "終了時にシステムトレイに格納",
"hotkey_rate4": "4つ星で評価",
"enableRemote": "リモートコントロール サーバーを有効化",
"showSkipButton_description": "プレーヤーバーのスキップボタンを表示/非表示にします",
"savePlayQueue": "再生キューを保存",
"minimumScrobbleSeconds_description": "Scrobbleされるために必要な最短の再生時間(秒)",
"minimumScrobbleSeconds_description": "Scrobble されるために必要な最短の再生時間 (秒)",
"skipPlaylistPage_description": "プレイリストに移動するときに、デフォルトページではなくプレイリストの曲リストページに移動します",
"fontType_description": "組み込みフォントの場合、feishin が提供するフォントから1つを選択します。 システムフォントの場合、OSにインストール済みの任意のフォントを選択できます。 カスタムフォントの場合、フォントファイルを自身で選択できます",
"fontType_description": "組み込みフォントの場合、Feishin が提供するフォントの中から 1 つ選択します。 システムフォントの場合、OS が提供する任意のフォントを選択できます。 カスタムフォントの場合、フォントファイルを自身で選択できます",
"playButtonBehavior": "再生ボタンの動作",
"volumeWheelStep": "音量ホイールステップ",
"sidebarPlaylistList_description": "サイドバーでプレイリストのリストを表示/非表示にします",
@@ -143,13 +145,13 @@
"playbackStyle_optionNormal": "通常",
"windowBarStyle": "ウィンドウバースタイル",
"floatingQueueArea": "フローティング再生キューエリアの表示",
"replayGainFallback_description": "ファイルに{{ReplayGain}}タグがない場合に適用するゲイン (dB単位)",
"replayGainPreamp_description": "{{ReplayGain}}の値に適用されるプリアンプゲインを調整します",
"replayGainFallback_description": "ファイルに {{ReplayGain}} タグがない場合に適用するゲイン (dB 単位)",
"replayGainPreamp_description": "{{ReplayGain}} の値に適用されるプリアンプゲインを調整します",
"hotkey_toggleRepeat": "リピートの切り替え",
"lyricOffset_description": "歌詞のオフセットをミリ秒単位で指定します",
"sidebarConfiguration_description": "サイドバーに表示されるアイテムと並び順を選択します",
"fontType": "フォントタイプ",
"remotePort": "リモートコントロール サーバー ポート",
"remotePort": "リモートコントロールサーバーポート",
"applicationHotkeys": "アプリケーションホットキー",
"hotkey_playbackNext": "次のトラック",
"useSystemTheme_description": "システム設定のライト/ダークテーマに従います",
@@ -160,18 +162,18 @@
"customFontPath": "カスタムフォントパス",
"followLyric": "歌詞を再生位置に追従",
"crossfadeDuration": "クロスフェードの長さ",
"discordIdleStatus": "アイドル状態でRich Presenceステータスを表示",
"discordIdleStatus": "アイドル状態で Rich Presence ステータスを表示",
"sidePlayQueueStyle_optionDetached": "分離",
"audioPlayer": "オーディオプレーヤー",
"hotkey_zoomOut": "縮小",
"hotkey_unfavoriteCurrentSong": "$t(common.currentSong) をお気に入り解除",
"hotkey_rate0": "評価をクリア",
"discordApplicationId": "{{discord}} アプリケーション ID",
"applicationHotkeys_description": "アプリケーションのホットキーを設定します。 チェックボックスを切り替えて、グローバルホットキー(デスクトップのみ)として設定できます",
"applicationHotkeys_description": "アプリケーションのホットキーを設定します。チェックボックスを切り替えて、グローバルホットキーとして設定します (デスクトップのみ)",
"floatingQueueArea_description": "画面右側に、再生キューをフローティング表示するためのホバーアイコンが表示されます",
"hotkey_volumeMute": "音量をミュート",
"hotkey_toggleCurrentSongFavorite": "$t(common.currentSong) をお気に入り登録/解除",
"remoteUsername": "リモートコントロール サーバー ユーザー名",
"remoteUsername": "リモートコントロールサーバーユーザー名",
"hotkey_browserBack": "ブラウザ 戻る",
"showSkipButton": "スキップボタンを表示",
"sidebarPlaylistList": "サイドバー プレイリスト リスト",
@@ -186,7 +188,123 @@
"windowBarStyle_description": "ウィンドウバーのスタイルを選択します",
"font_description": "アプリケーションに使用するフォントを設定します",
"savePlayQueue_description": "アプリケーション終了時に再生キューを保存し、アプリケーション開始時に復元します",
"useSystemTheme": "システムテーマを使用"
"useSystemTheme": "システムテーマを使用",
"webAudio": "Web Audio を使用する",
"mediaSession_description": "Windows Media Session の統合を有効にし、システムボリュームオーバーレイとロック画面にメディアコントロールとメタデータを表示します (Windows のみ)",
"mediaSession": "Media Session を有効にする",
"startMinimized_description": "システムトレイでアプリケーションを起動する",
"startMinimized": "最小化して起動する",
"translationApiKey_description": "翻訳用の API キー (グローバルサービスエンドポイントのみ)",
"translationApiKey": "翻訳 API キー",
"translationApiProvider_description": "翻訳用の API プロバイダー",
"translationApiProvider": "翻訳 API プロバイダー",
"translationTargetLanguage_description": "翻訳対象言語",
"translationTargetLanguage": "翻訳対象言語",
"trayEnabled_description": "トレイアイコン/メニューの表示/非表示。無効にすると、最小化/トレイへの終了も無効になります",
"trayEnabled": "トレイを表示する",
"volumeWidth_description": "音量スライダーの幅",
"volumeWidth": "音量スライダーの幅",
"webAudio_description": "Web Audio を使用します。これにより、リプレイゲインなどの高度な機能が有効になります。それ以外の場合は無効にしてください",
"playerAlbumArtResolution_description": "大画面プレーヤーのアルバムアートプレビューの解像度。解像度が高いほど鮮明になりますが、読み込みが遅くなる可能性があります。デフォルトは 0 (自動設定) です",
"mpvExtraParameters_help": "1 行に 1 つずつ",
"musicbrainz_description": "MusicBrainz ID が存在するアーティスト/アルバムページに MusicBrainz へのリンクを表示します",
"musicbrainz": "MusicBrainz リンクを表示する",
"neteaseTranslation_description": "有効にすると、利用可能な場合は NetEase から翻訳された歌詞を取得して表示します",
"neteaseTranslation": "NetEase 翻訳歌詞を有効にする",
"passwordStore_description": "使用するパスワード/シークレットストア。パスワードの保存に問題がある場合はこれを変更してください",
"passwordStore": "パスワード/シークレットストア",
"playerAlbumArtResolution": "プレーヤーのアルバムアートの解像度",
"playerbarOpenDrawer_description": "プレーヤーバーをクリックすると全画面プレーヤーが開きます",
"preferLocalLyrics_description": "利用可能な場合は、リモート歌詞よりもローカル歌詞を優先します",
"preferLocalLyrics": "ローカル歌詞を優先する",
"preservePitch_description": "再生速度を変更してもピッチを保持します",
"preservePitch": "ピッチを保持する",
"preventSleepOnPlayback_description": "音楽の再生中にディスプレイがスリープ状態にならないようにします",
"preventSleepOnPlayback": "再生中のスリープを防止する",
"lastfmApiKey_description": "{{lastfm}} の API キー。カバーアートの表示に必要です",
"imageAspectRatio": "ネイティブのカバーアートの縦横比を使用する",
"language": "言語",
"imageAspectRatio_description": "有効にすると、カバーアートはネイティブの縦横比で表示されます。縦横比が 1:1 でない場合、残りのスペースは空白になります",
"lastfm_description": "アーティスト/アルバムページに Last.fm へのリンクを表示する",
"lastfm": "Last.fm リンクを表示する",
"lastfmApiKey": "{{lastfm}} API キー",
"homeConfiguration_description": "ホーム画面に表示する項目と表示順序を設定します",
"homeConfiguration": "ホーム画面の設定",
"externalLinks": "外部リンクを表示する",
"externalLinks_description": "アーティスト/アルバムページで外部リンク (Last.fm、MusicBrainz) を表示できるようにします",
"doubleClickBehavior_description": "true の場合、トラック検索で一致するすべてのトラックがキューに入ります。そうでない場合は、クリックされたトラックのみがキューに入ります",
"enableAutoTranslation": "自動翻訳を有効にする",
"enableAutoTranslation_description": "歌詞が読み込まれたときに自動的に翻訳を有効にします",
"genreBehavior_description": "ジャンルをクリックした際に、デフォルトでトラックリストまたはアルバムリストのどちらを開くか決定します",
"genreBehavior": "ジャンルページのデフォルトの動作",
"albumBackground_description": "アルバムアートを含むアルバムページに背景画像を追加します",
"albumBackground": "アルバムの背景画像",
"albumBackgroundBlur": "アルバムの背景画像のぼかしサイズ",
"albumBackgroundBlur_description": "アルバムの背景画像に適用されるぼかしの量を調整します",
"artistBackground": "アーティストの背景画像",
"artistBackground_description": "アーティストアートを含むアーティストページに背景画像を追加します",
"artistBackgroundBlur": "アーティストの背景画像のぼかしサイズ",
"artistBackgroundBlur_description": "アーティストの背景画像に適用されるぼかしの量を調整します",
"artistConfiguration": "アルバムアーティストページの設定",
"artistConfiguration_description": "アルバムアーティストページに表示するアイテムと順序を設定します",
"buttonSize_description": "プレーヤーバーのボタンのサイズ",
"buttonSize": "プレーヤーバーのボタンサイズ",
"clearCache_description": "Feishin の「ハードクリア」。Feishin のキャッシュをクリアするだけでなく、ブラウザのキャッシュ (保存された画像やその他のアセット) も空にします。サーバーの資格情報や設定は保持されます",
"clearCache": "ブラウザのキャッシュをクリアする",
"clearCacheSuccess": "キャッシュが正常にクリアされました",
"clearQueryCache_description": "Feishin の「ソフトクリア」。これにより、プレイリストとトラックのメタデータが更新され、保存された歌詞がリセットされます。設定、サーバーの資格情報、キャッシュされた画像は保持されます",
"clearQueryCache": "Feishin のキャッシュをクリアする",
"contextMenu_description": "アイテムを右クリックした際にメニューに表示される項目を非表示にすることができます。チェックされていない項目は非表示になります",
"contextMenu": "コンテキストメニュー (右クリック) の設定",
"crossfadeStyle": "クロスフェードのスタイル",
"customCss_description": "カスタム CSS コンテンツ。注: コンテンツとリモート URL は許可されていないプロパティです。コンテンツのプレビューは以下に表示されます。設定していない追加フィールドは、サニタイズ処理により表示されています",
"customCss": "カスタム CSS",
"customCssEnable_description": "カスタム CSS の記述を許可します",
"customCssEnable": "カスタム CSS を有効にする",
"customCssNotice": "警告: ある程度のサニタイズ (url() と content: の禁止) はありますが、カスタム CSS を使用するとインターフェースの変更によりリスクが生じる可能性があります",
"releaseChannel_optionBeta": "beta",
"releaseChannel_optionLatest": "最新",
"releaseChannel": "リリースチャンネル",
"releaseChannel_description": "自動更新のために安定版リリースまたはベータ版リリースを選択してください",
"discordDisplayType_artistname": "アーティスト名",
"discordDisplayType_songname": "曲名",
"discordLinkType_description": "{{discord}} Rich Presence において、曲とアーティストのフィールドに {{lastfm}} または {{musicbrainz}} への外部リンクを追加します。{{musicbrainz}} は最も正確ですが、タグが必要でアーティストリンクを提供しません。一方、{{lastfm}} は常にリンクを提供します。追加のネットワークリクエストは発生しません",
"discordPausedStatus": "一時停止時に Rich Presence を表示する",
"discordRichPresence": "{{discord}} Rich Presence",
"discordServeImage_description": "{{discord}} Rich Presence 用のカバーアートをサーバーから共有します。Jellyfin と Navidrome でのみ利用できます。{{discord}} は bot を使用して画像を取得するため、サーバーはパブリックインターネットからアクセスできる必要があります",
"exportImportSettings_control_exportText": "設定をエクスポート",
"exportImportSettings_control_importText": "設定をインポート",
"exportImportSettings_control_title": "設定をインポート/エクスポート",
"exportImportSettings_control_description": "JSON 経由で設定をエクスポートおよびインポートする",
"exportImportSettings_destructiveWarning": "設定のインポートは破壊的です。下の「インポート」をクリックする前に、上記の内容を必ずご確認ください!",
"hotkey_navigateHome": "ホームに移動",
"playButtonBehavior_optionPlayShuffled": "$t(player.shuffle)",
"playerbarOpenDrawer": "プレーヤーバーの全画面表示切り替え",
"transcode": "トランスコーディングを有効にする",
"transcode_description": "さまざまなフォーマットへのトランスコーディングを可能にします",
"discordServeImage": "サーバーから {{discord}} に画像を提供する",
"transcodeFormat": "トランスコードする形式",
"transcodeFormat_description": "トランスコードする形式を選択します。サーバーに決定させる場合は空白のままにしておきます",
"transcodeBitrate": "トランスコードするビットレート",
"transcodeBitrate_description": "トランスコードするビットレートを選択します。0 はサーバーが選択することを意味します",
"notify_description": "現在の曲を変更するときに通知を表示します",
"notify": "曲の通知を有効にする",
"exportImportSettings_offendingKeyError": "「{{offendingKey}}」は正しくありません - {{reason}}",
"discordDisplayType": "{{discord}} Presence 表示タイプ",
"discordLinkType_none": "$t(common.none)",
"discordListening_description": "Playing ではなく Listening としてステータスを表示します",
"discordListening": "ステータスを Listening として表示する",
"discordPausedStatus_description": "有効にすると、プレーヤーが一時停止されているときにもステータスを表示します",
"discordDisplayType_description": "ステータスで聴いている内容を変更します",
"discordLinkType": "{{discord}} Presence リンク",
"doubleClickBehavior": "ダブルクリック時に検索した全トラックをキューに追加する",
"discordLinkType_mbz_lastfm": "{{musicbrainz}} と {{lastfm}} のフォールバック",
"homeFeature": "ホーム画面の注目カルーセル",
"homeFeature_description": "ホーム画面に大きな注目カルーセルを表示するかどうかを制御します",
"exportImportSettings_notValidJSON": "渡されたファイルは有効な JSON ではありません",
"exportImportSettings_importSuccess": "設定が正常にインポートされました!",
"exportImportSettings_importModalTitle": "Feishin 設定をインポート",
"exportImportSettings_importBtn": "設定をインポート"
},
"action": {
"editPlaylist": "$t(entity.playlist_one) を編集",
@@ -203,12 +321,12 @@
"removeFromQueue": "キューから削除",
"deselectAll": "すべて選択解除",
"moveToBottom": "末尾に移動",
"setRating": "評価",
"setRating": "評価を設定する",
"toggleSmartPlaylistEditor": "$t(entity.smartPlaylist) エディタの切り替え",
"removeFromFavorites": "$t(entity.favorite_other) から削除",
"openIn": {
"lastfm": "Last.fmで開く",
"musicbrainz": "MusicBrainzで開く"
"lastfm": "Last.fm で開く",
"musicbrainz": "MusicBrainz で開く"
},
"moveToNext": "次"
},
@@ -264,7 +382,7 @@
"bitrate": "ビットレート",
"saveAndReplace": "保存して変更",
"action_other": "アクション",
"playerMustBePaused": "プレヤーを一時停止する必要があります",
"playerMustBePaused": "プレヤーを一時停止する必要があります",
"confirm": "確認",
"resetToDefault": "デフォルトにリセット",
"home": "ホーム",
@@ -287,21 +405,48 @@
"random": "ランダム",
"size": "サイズ",
"biography": "バイオグラフィー",
"note": "ノート"
"note": "ノート",
"explicitStatus": "明示的なステータス",
"additionalParticipants": "追加参加者",
"newVersion": "新しいバージョン ({{version}}) がインストールされました",
"viewReleaseNotes": "リリースノートを表示する",
"bitDepth": "ビット深度",
"close": "閉じる",
"codec": "コーデック",
"mbid": "MusicBrainz ID",
"sampleRate": "サンプルレート",
"preview": "プレビュー",
"private": "プライベート",
"public": "パブリック",
"share": "共有",
"tags": "タグ",
"trackGain": "トラックゲイン",
"trackPeak": "トラックピーク",
"translation": "翻訳",
"reload": "リロード",
"explicit": "明示的",
"albumGain": "アルバムゲイン",
"albumPeak": "アルバムピーク",
"releaseType": "リリースタイプ"
},
"table": {
"config": {
"view": {
"card": "カード",
"table": "テーブル",
"poster": "ポスター"
"poster": "ポスター",
"grid": "グリッド",
"list": "リスト"
},
"general": {
"displayType": "表示タイプ",
"gap": "$t(common.gap)",
"tableColumns": "テーブル カラム",
"autoFitColumns": "カラム長を自動調整",
"size": "$t(common.size)"
"size": "$t(common.size)",
"itemSize": "項目のサイズ (px)",
"itemGap": "項目の間隔 (px)",
"followCurrentSong": "現在の曲をフォロー"
},
"label": {
"releaseDate": "リリース日時",
@@ -329,7 +474,9 @@
"discNumber": "ディスク番号",
"favorite": "$t(common.favorite)",
"year": "$t(common.year)",
"albumArtist": "$t(entity.albumArtist_one)"
"albumArtist": "$t(entity.albumArtist_one)",
"codec": "$t(common.codec)",
"songCount": "$t(entity.track_other)"
}
},
"column": {
@@ -342,7 +489,7 @@
"releaseYear": "年",
"lastPlayed": "最後に再生",
"biography": "バイオグラフィー",
"releaseDate": "リリース日時",
"releaseDate": "発売日",
"bitrate": "ビットレート",
"title": "タイトル",
"bpm": "BPM",
@@ -355,7 +502,8 @@
"path": "パス",
"discNumber": "ディスク",
"channels": "$t(common.channel_other)",
"size": "$t(common.size)"
"size": "$t(common.size)",
"codec": "$t(common.codec)"
}
},
"error": {
@@ -373,11 +521,16 @@
"localFontAccessDenied": "ローカルフォントへのアクセスが拒否されました",
"serverNotSelectedError": "サーバーが選択されていません",
"remoteDisableError": "リモートサーバーを $t(common.disable) にする際にエラーが発生しました",
"mpvRequired": "MPVが必要です",
"mpvRequired": "MPV が必要です",
"audioDeviceFetchError": "オーディオデバイスの取得時にエラーが発生しました",
"invalidServer": "無効なサーバー",
"loginRateError": "ログイン試行回数が多すぎます数秒後に再試行してください",
"endpointNotImplementedError": "{{serverType}} にはエンドポイント {{endpoint}} が実装されていません"
"loginRateError": "ログイン試行回数が多すぎます数秒後に再試行してください",
"endpointNotImplementedError": "{{serverType}} にはエンドポイント {{endpoint}} が実装されていません",
"badAlbum": "このページが表示されたのは、この曲がアルバムに含まれていないためです。この問題は、音楽フォルダの最上位に曲がある場合に発生する可能性が高くなります。Jellyfin は、トラックがフォルダ内にある場合にのみトラックをグループ化します",
"networkError": "ネットワークエラーが発生しました",
"notificationDenied": "通知の許可が拒否されました。この設定は効果がありません",
"openError": "ファイルを開けませんでした",
"badValue": "無効なオプション「{{value}}」。この値は存在しません"
},
"filter": {
"mostPlayed": "最も多く再生",
@@ -393,7 +546,7 @@
"note": "ノート",
"name": "名前",
"dateAdded": "追加された日時",
"releaseDate": "リリース日時",
"releaseDate": "発売日",
"communityRating": "コミュニティの評価",
"path": "パス",
"favorited": "お気に入り",
@@ -401,7 +554,7 @@
"isRecentlyPlayed": "最近再生済み",
"isFavorited": "お気に入り済み",
"bpm": "BPM",
"releaseYear": "リリース年",
"releaseYear": "発売年",
"disc": "ディスク",
"biography": "バイオグラフィー",
"songCount": "曲数",
@@ -420,8 +573,9 @@
"owner": "$t(common.owner)",
"genre": "$t(entity.genre_one)",
"albumCount": "$t(entity.album_other) 個",
"id": "id",
"album": "$t(entity.album_one)"
"id": "ID",
"album": "$t(entity.album_one)",
"explicitStatus": "$t(common.explicitStatus)"
},
"page": {
"sidebar": {
@@ -435,7 +589,9 @@
"settings": "$t(common.setting_other)",
"home": "$t(common.home)",
"artists": "$t(entity.artist_other)",
"albumArtists": "$t(entity.albumArtist_other)"
"albumArtists": "$t(entity.albumArtist_other)",
"myLibrary": "マイライブラリ",
"shared": "$t(entity.playlist_other) を共有"
},
"fullscreenPlayer": {
"config": {
@@ -449,14 +605,19 @@
"unsynchronized": "非同期",
"lyricAlignment": "歌詞の位置",
"useImageAspectRatio": "画像のアスペクト比を使用する",
"lyricGap": "歌詞の間隔"
"lyricGap": "歌詞の間隔",
"dynamicImageBlur": "画像のぼかしサイズ",
"dynamicIsImage": "背景画像を有効にする",
"lyricOffset": "歌詞のオフセット (ms)"
},
"upNext": "次へ",
"lyrics": "歌詞",
"related": "関連"
"related": "関連",
"visualizer": "ビジュアライザー",
"noLyrics": "歌詞が見つかりません"
},
"appMenu": {
"selectServer": "サーバを選択",
"selectServer": "サーバを選択",
"version": "バージョン {{version}}",
"settings": "$t(common.setting_other)",
"manageServers": "サーバーの管理",
@@ -465,7 +626,9 @@
"openBrowserDevtools": "ブラウザの開発者ツールを開く",
"quit": "$t(common.quit)",
"goBack": "戻る",
"goForward": "進む"
"goForward": "進む",
"privateModeOff": "プライベートモードをオフにする",
"privateModeOn": "プライベートモードをオンにする"
},
"contextMenu": {
"addToPlaylist": "$t(action.addToPlaylist)",
@@ -483,33 +646,48 @@
"addFavorite": "$t(action.addToFavorites)",
"play": "$t(player.play)",
"numberSelected": "{{count}} 個 選択",
"removeFromQueue": "$t(action.removeFromQueue)"
"removeFromQueue": "$t(action.removeFromQueue)",
"download": "ダウンロード",
"moveToNext": "$t(action.moveToNext)",
"playSimilarSongs": "$t(player.playSimilarSongs)",
"shareItem": "アイテムを共有する",
"goToAlbum": "$t(entity.album_one) に移動",
"goToAlbumArtist": "$t(entity.albumArtist_one) に移動",
"showDetails": "情報を取得する",
"playShuffled": "$t(player.shuffle)"
},
"home": {
"mostPlayed": "最も多く再生",
"newlyAdded": "新規追加リリース",
"title": "$t(common.home)",
"explore": "ライブラリから検索",
"recentlyPlayed": "最近の再生"
"recentlyPlayed": "最近の再生",
"recentlyReleased": "最近のリリース"
},
"albumDetail": {
"moreFromArtist": "$t(entity.artist_one) の他の項目",
"moreFromGeneric": "{{item}} の他の項目"
"moreFromGeneric": "{{item}} の他の項目",
"released": "リリース"
},
"setting": {
"playbackTab": "再生",
"generalTab": "一般",
"hotkeysTab": "ホットキー",
"windowTab": "ウィンドウ"
"windowTab": "ウィンドウ",
"advanced": "高度"
},
"albumArtistList": {
"title": "$t(entity.albumArtist_other)"
},
"genreList": {
"title": "$t(entity.genre_other)"
"title": "$t(entity.genre_other)",
"showAlbums": "$t(entity.genre_one) $t(entity.album_other) を表示",
"showTracks": "$t(entity.genre_one) $t(entity.track_other) を表示"
},
"trackList": {
"title": "$t(entity.track_other)"
"title": "$t(entity.track_other)",
"artistTracks": "{{artist}} のトラック",
"genreTracks": "\"{{genre}}\" $t(entity.track_other)"
},
"globalSearch": {
"commands": {
@@ -523,7 +701,36 @@
"title": "$t(entity.playlist_other)"
},
"albumList": {
"title": "$t(entity.album_other)"
"title": "$t(entity.album_other)",
"artistAlbums": "{{artist}} のアルバム",
"genreAlbums": "\"{{genre}}\" $t(entity.album_other)"
},
"albumArtistDetail": {
"about": "{{artist}} について",
"appearsOn": "出演",
"recentReleases": "最近のリリース",
"viewDiscography": "ディスコグラフィーを見る",
"topSongs": "人気曲",
"topSongsFrom": "{{title}} からの人気曲",
"viewAll": "すべて表示",
"viewAllTracks": "$t(entity.track_other) をすべて表示",
"relatedArtists": "関連の $t(entity.artist_other)"
},
"manageServers": {
"title": "サーバーの管理",
"serverDetails": "サーバーの詳細",
"url": "URL",
"username": "ユーザーネーム",
"editServerDetailsTooltip": "サーバーの詳細を編集する",
"removeServer": "リモートサーバー"
},
"itemDetail": {
"openFile": "ファイルマネージャーでトラックを表示する",
"copyPath": "パスをクリップボードにコピーする",
"copiedPath": "パスが正常にコピーされました"
},
"playlist": {
"reorder": "ID によるソート時のみ並べ替えが可能です"
}
},
"form": {
@@ -549,15 +756,19 @@
"input_name": "サーバー名",
"success": "サーバーが追加されました",
"input_savePassword": "パスワードを保存",
"ignoreSsl": "SSLを無視 ($t(common.restartRequired))",
"ignoreSsl": "SSL を無視 ($t(common.restartRequired))",
"ignoreCors": "CORSを無視 ($t(common.restartRequired))",
"error_savePassword": "パスワードを保存する際にエラーが発生しました"
"error_savePassword": "パスワードを保存する際にエラーが発生しました",
"input_preferInstantMixDescription": "類似曲を取得するにはインスタントミックスのみを使用してください。この動作を変更するプラグインがある場合に役立ちます",
"input_preferInstantMix": "インスタントミックスを優先する"
},
"addToPlaylist": {
"success": "{{message}} $t(entity.track_other) を {{numOfPlaylists}} $t(entity.playlist_other) に追加しました",
"success": "$t(entity.trackWithCount, {\"count\": {{message}} }) を $t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} }) に追加しました",
"title": "$t(entity.playlist_one) に追加",
"input_skipDuplicates": "重複をスキップ",
"input_playlists": "$t(entity.playlist_other)"
"input_playlists": "$t(entity.playlist_other)",
"create": "$t(entity.playlist_one) {{playlist}} を作成する",
"searchOrCreate": "$t(entity.playlist_other) を検索するか、入力して新しいものを作成してください"
},
"updateServer": {
"title": "サーバーをアップデート",
@@ -565,7 +776,8 @@
},
"queryEditor": {
"input_optionMatchAll": "すべて一致",
"input_optionMatchAny": "一部一致"
"input_optionMatchAny": "一部一致",
"title": "クエリエディタ"
},
"lyricSearch": {
"input_name": "$t(common.name)",
@@ -573,25 +785,64 @@
"title": "歌詞検索"
},
"editPlaylist": {
"title": "$t(entity.playlist_one) を編集"
"title": "$t(entity.playlist_one) を編集",
"publicJellyfinNote": "Jellyfin では、何らかの理由でプレイリストが公開されているかどうかが表示されません。公開されたままにしたい場合は、以下の項目を選択してください",
"success": "$t(entity.playlist_one) が正常に更新されました"
},
"shareItem": {
"allowDownloading": "ダウンロードを許可",
"description": "説明",
"setExpiration": "有効期限を設定",
"success": "共有リンクがクリップボードにコピーされました (またはここをクリックして開きます)",
"expireInvalid": "有効期限は将来の日時である必要があります",
"createFailed": "共有リンクを作成できませんでした (共有は有効になっていますか?)"
},
"privateMode": {
"enabled": "プライベートモードが有効になりました。再生ステータスは外部連携から非表示になっています",
"disabled": "プライベートモードが無効になりました。再生ステータスは有効になっている外部連携に表示されています",
"title": "プライベートモード"
}
},
"entity": {
"genre_other": "ジャンル",
"playlistWithCount_other": "{{count}} プレイリスト",
"playlistWithCount_other": "{{count}} 個のプレイリスト",
"playlist_other": "プレイリスト",
"artist_other": "アーティスト",
"folderWithCount_other": "{{count}} フォルダ",
"folderWithCount_other": "{{count}} 個のフォルダ",
"albumArtist_other": "アルバムアーティスト",
"track_other": "トラック",
"albumArtistCount_other": "{{count}} アルバムアーティスト",
"albumWithCount_other": "{{count}} アルバム",
"albumWithCount_other": "{{count}} 枚のアルバム",
"favorite_other": "お気に入り",
"artistWithCount_other": "{{count}} アーティスト",
"artistWithCount_other": "{{count}} 人のアーティスト",
"folder_other": "フォルダ",
"smartPlaylist": "スマート $t(entity.playlist_one)",
"album_other": "アルバム",
"genreWithCount_other": "{{count}} ジャンル",
"trackWithCount_other": "{{count}} トラック"
"genreWithCount_other": "{{count}} 個のジャンル",
"trackWithCount_other": "{{count}} 個のトラック",
"play_other": "{{count}} 回再生",
"song_other": "曲"
},
"dragDropZone": {
"error_oneFileOnly": "1 つのファイルのみ選択してください",
"error_readingFile": "ファイルの読み取り中に問題が発生しました: {{errorMessage}}",
"mainText": "ここにファイルをドロップしてください"
},
"releaseType": {
"primary": {
"broadcast": "放送",
"ep": "EP",
"other": "その他",
"single": "シングル",
"album": "$t(entity.album_one)"
},
"secondary": {
"audiobook": "オーディオブック",
"audioDrama": "オーディオドラマ",
"compilation": "コンピレーション",
"djMix": "DJ Mix",
"demo": "デモ",
"soundtrack": "サウンドトラック"
}
}
}
+53 -3
View File
@@ -115,7 +115,14 @@
"owner": "소유자",
"sampleRate": "샘플레이트",
"tags": "태그",
"additionalParticipants": "추가 참여자"
"additionalParticipants": "추가 참여자",
"explicitStatus": "성인컨텐츠",
"private": "비공개",
"public": "공개",
"recordLabel": "레이블",
"releaseType": "발매형태",
"explicit": "성인컨텐츠",
"clean": "클린"
},
"entity": {
"albumWithCount_other": "{{count}} 앨범",
@@ -228,7 +235,9 @@
"input_skipDuplicates": "중복 건너뛰기",
"title": "$t(entity.playlist_one) 에 추가",
"input_playlists": "$t(entity.playlist_other)",
"success": "$t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })에 $t(entity.trackWithCount, {\"count\": {{message}} })가 추가되었습니다"
"success": "$t(entity.playlistWithCount, {\"count\": {{numOfPlaylists}} })에 $t(entity.trackWithCount, {\"count\": {{message}} })가 추가되었습니다",
"create": "$t(entity.playlist_one) {{playlist}} 생성",
"searchOrCreate": "$t(entity.playlist_other) 검색 또는 입력하여 새로 만들기"
},
"lyricSearch": {
"title": "가사 검색",
@@ -443,6 +452,47 @@
"accentColor_description": "앱의 강조색상 설정",
"accentColor": "강조색상",
"albumBackground_description": "앨범아트가 있는 경우 앨범화면에 배경이미지 추가",
"albumBackground": "앨범 배경이미지"
"albumBackground": "앨범 배경이미지",
"albumBackgroundBlur_description": "앨범 배경이미지의 흐려짐 정도 조정",
"albumBackgroundBlur": "앨범배경이미지 흐려짐 크기",
"applicationHotkeys_description": "앱의 단축키 설정. 앱 전체에 적용되는 단축키를 설정하기 위해서는 체크박스에 체크하세요(PC만 가능)",
"applicationHotkeys": "앱 단축키",
"artistBackground": "아티스트 배경이미지",
"artistBackground_description": "아티스트 페이지에 아티스트가 포함된 배경이미지를 추가",
"artistBackgroundBlur": "아티스트 배경이미지 흐려짐 크기",
"artistBackgroundBlur_description": "아티스트 배경이미지에 적용된 흐려짐 크기 조정",
"artistConfiguration": "앨범아티스트 페이지 설정",
"artistConfiguration_description": "앨범아티스트 페이지에 표시할 정보 및 순서 설정",
"audioDevice_description": "음악재생에 사용할 장치 선택(웹플레이어만 가능)",
"audioDevice": "오디오 장치",
"audioExclusiveMode_description": "단독재생모드 켜기. 이 모드에서는 일반적으로 시스템의 재생장치가 고정되며 MPV로만 오디오가 재생됩니다",
"audioExclusiveMode": "오디오 단독재생모드",
"audioPlayer_description": "재생을 위한 오디오 플레이어 선택",
"audioPlayer": "오디오 플레이어",
"buttonSize_description": "플레이어 바 버튼 크기",
"buttonSize": "플레이어 바 버튼 크기",
"clearCache_description": "페이신 하드클리어. 페이신의 캐시 및 브라우저의 캐시 삭제(저장된 이미지 및 기타 정보). 서버 접속정보 및 설정은 유지됩니다"
},
"releaseType": {
"primary": {
"broadcast": "방송",
"ep": "ep앨범",
"other": "기타",
"single": "싱글"
},
"secondary": {
"audiobook": "오디오북",
"audioDrama": "오디오 드라마",
"compilation": "컴필레이션",
"djMix": "DJ 믹스",
"demo": "데모",
"fieldRecording": "현지녹음",
"interview": "인터뷰",
"live": "라이브",
"mixtape": "믹스테이프",
"remix": "리믹스",
"soundtrack": "사운드트랙",
"spokenWord": "보컬사운드"
}
}
}
+19
View File
@@ -0,0 +1,19 @@
{
"common": {
"confirm": "confirmă",
"create": "creează",
"biography": "biografie",
"areYouSure": "ești sigur?",
"no": "nu",
"name": "nume",
"ok": "ok",
"note": "notă",
"yes": "da",
"explicit": "explicit",
"year": "an",
"menu": "meniu"
},
"filter": {
"biography": "biografie"
}
}
+19 -3
View File
@@ -440,8 +440,8 @@
"removeFromQueue": "$t(action.removeFromQueue)",
"showDetails": "получить информацию",
"shareItem": "поделиться",
"goToAlbum": "Перейти $t(entity.album_one)",
"goToAlbumArtist": "Перейти $t(entity.albumArtist_one)"
"goToAlbum": "Перейти к $t(entity.album_one)",
"goToAlbumArtist": "Перейти к $t(entity.albumArtist_one)"
},
"home": {
"mostPlayed": "слушают чаще всего",
@@ -782,6 +782,22 @@
"lyricOffset": "синхронизация текста треков (мс)",
"audioExclusiveMode": "эксклюзивный режим аудио",
"audioExclusiveMode_description": "включить режим эксклюзивного вывода. В этом режиме система обычно блокируется, и только mpv сможет выводить звук",
"artistBackground": "Фоновое изображение исполнителя"
"artistBackground": "Фоновое изображение исполнителя",
"artistBackground_description": "Добавляет фоновое изображение для страниц исполнителя, содержащих обложку исполнителя",
"artistBackgroundBlur": "процент размытия обложки исполнителя",
"artistBackgroundBlur_description": "регулирует процент размытия к заднему фону исполнителя"
},
"releaseType": {
"secondary": {
"demo": "демо",
"audiobook": "аудиокнига",
"compilation": "подборка",
"interview": "интервью",
"remix": "ремикс",
"live": "прямой эфир"
},
"primary": {
"other": "другие"
}
}
}
+19 -2
View File
@@ -118,7 +118,9 @@
"year": "yıl",
"yes": "evet",
"trackGain": "parça kazancı",
"trackPeak": "parça zirvesi"
"trackPeak": "parça zirvesi",
"private": "gizli",
"clean": "temiz"
},
"entity": {
"album_one": "albüm",
@@ -723,7 +725,11 @@
"discordDisplayType_artistname": "Sanatçı adı(ları)",
"hotkey_navigateHome": "ana sayfaya git",
"preventSleepOnPlayback": "oynatma sırasında uykuyu önle",
"preventSleepOnPlayback_description": "müzik çalarken ekranın uyku moduna geçmesini önle"
"preventSleepOnPlayback_description": "müzik çalarken ekranın uyku moduna geçmesini önle",
"releaseChannel_optionBeta": "beta",
"releaseChannel_optionLatest": "en son",
"language": "dil",
"notify": "müzik bildirimi aktivleştir"
},
"table": {
"column": {
@@ -801,5 +807,16 @@
"table": "tablo"
}
}
},
"releaseType": {
"secondary": {
"demo": "demo",
"live": "canlı",
"remix": "remix"
}
},
"dragDropZone": {
"error_oneFileOnly": "lütfen sadece 1 dosya seç",
"error_readingFile": "bu dosyayi okurken bir sorun oluştu :{{errorMessage}}"
}
}
+3 -3
View File
@@ -3,7 +3,7 @@ import { BrowserWindow, globalShortcut, systemPreferences } from 'electron';
import { isMacOS, isWindows } from '../../../utils';
import { store } from '../settings';
import { PlayerType } from '/@/shared/types/types';
import { PlaybackType } from '/@/shared/types/types';
export const enableMediaKeys = (window: BrowserWindow | null) => {
if (isMacOS()) {
@@ -26,9 +26,9 @@ export const enableMediaKeys = (window: BrowserWindow | null) => {
}
const enableWindowsMediaSession = store.get('mediaSession', false) as boolean;
const playbackType = store.get('playbackType', PlayerType.WEB) as PlayerType;
const playbackType = store.get('playbackType', PlaybackType.WEB) as PlaybackType;
if (!enableWindowsMediaSession || !isWindows() || playbackType !== PlayerType.WEB) {
if (!enableWindowsMediaSession || !isWindows() || playbackType !== PlaybackType.WEB) {
globalShortcut.register('MediaStop', () => {
window?.webContents.send('renderer-player-stop');
});
+4 -4
View File
@@ -382,7 +382,7 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
getMainWindow()?.webContents.send('request-favorite', {
favorite,
id,
serverId: currentState.song._serverId,
serverId: currentState.song.serverId,
});
}
break;
@@ -443,7 +443,7 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
getMainWindow()?.webContents.send('request-rating', {
id,
rating,
serverId: currentState.song._serverId,
serverId: currentState.song.serverId,
});
}
break;
@@ -578,7 +578,7 @@ ipcMain.on('remote-username', (_event, username: string) => {
});
ipcMain.on('update-favorite', (_event, favorite: boolean, serverId: string, ids: string[]) => {
if (currentState.song?._serverId !== serverId) return;
if (currentState.song?.serverId !== serverId) return;
const id = currentState.song.id;
@@ -592,7 +592,7 @@ ipcMain.on('update-favorite', (_event, favorite: boolean, serverId: string, ids:
});
ipcMain.on('update-rating', (_event, rating: number, serverId: string, ids: string[]) => {
if (currentState.song?._serverId !== serverId) return;
if (currentState.song?.serverId !== serverId) return;
const id = currentState.song.id;
+3 -3
View File
@@ -38,7 +38,7 @@ import {
} from './utils';
import './features';
import { PlayerType, TitleTheme } from '/@/shared/types/types';
import { PlaybackType, TitleTheme } from '/@/shared/types/types';
export default class AppUpdater {
constructor() {
@@ -549,9 +549,9 @@ async function createWindow(first = true): Promise<void> {
}
const enableWindowsMediaSession = store.get('mediaSession', false) as boolean;
const playbackType = store.get('playbackType', PlayerType.WEB) as PlayerType;
const playbackType = store.get('playbackType', PlaybackType.WEB) as PlaybackType;
const shouldDisableMediaFeatures =
!isWindows() || !enableWindowsMediaSession || playbackType !== PlayerType.WEB;
!isWindows() || !enableWindowsMediaSession || playbackType !== PlaybackType.WEB;
if (shouldDisableMediaFeatures) {
app.commandLine.appendSwitch(
'disable-features',
+1 -1
View File
@@ -96,7 +96,7 @@ export const RemoteContainer = () => {
}}
variant="transparent"
/>
{(song?._serverType === 'navidrome' || song?._serverType === 'subsonic') && (
{(song?.serverType === 'navidrome' || song?.serverType === 'subsonic') && (
<div style={{ margin: 'auto' }}>
<Tooltip label="Double click to clear" openDelay={1000}>
<Rating
+121 -12
View File
@@ -2,41 +2,62 @@ import { ClientSideRowModelModule } from '@ag-grid-community/client-side-row-mod
import { ModuleRegistry } from '@ag-grid-community/core';
import { InfiniteRowModelModule } from '@ag-grid-community/infinite-row-model';
import { MantineProvider } from '@mantine/core';
import '@mantine/core/styles.css';
import '@mantine/dates/styles.css';
import { Notifications } from '@mantine/notifications';
import '@mantine/notifications/styles.css';
import isElectron from 'is-electron';
import { useEffect, useMemo, useRef, useState } from 'react';
import '@mantine/core/styles.css';
import '@mantine/notifications/styles.css';
import '@mantine/dates/styles.css';
import '/@/shared/styles/global.css';
import '@ag-grid-community/styles/ag-grid.css';
import 'overlayscrollbars/overlayscrollbars.css';
import '/styles/overlayscrollbars.css';
import i18n from '/@/i18n/i18n';
import { useDiscordRpc } from '/@/renderer/features/discord-rpc/use-discord-rpc';
import { PlayerProvider } from '/@/renderer/features/player/context/player-context';
import { PlayQueueHandlerContext } from '/@/renderer/features/player/context/play-queue-handler-context';
import { WebAudioContext } from '/@/renderer/features/player/context/webaudio-context';
import { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-handle-playqueue-add';
import { updateSong } from '/@/renderer/features/player/update-remote-song';
import { getMpvProperties } from '/@/renderer/features/settings/components/playback/mpv-settings';
import { useServerVersion } from '/@/renderer/hooks/use-server-version';
import { IsUpdatedDialog } from '/@/renderer/is-updated-dialog';
import { AppRouter } from '/@/renderer/router/app-router';
import { useCssSettings, useHotkeySettings, useSettingsStore } from '/@/renderer/store';
import {
PlayerState,
useCssSettings,
useHotkeySettings,
usePlaybackSettings,
usePlayerStore,
useQueueControls,
useRemoteSettings,
useSettingsStore,
} from '/@/renderer/store';
import { useAppTheme } from '/@/renderer/themes/use-app-theme';
import { sanitizeCss } from '/@/renderer/utils/sanitize';
import '/styles/overlayscrollbars.css';
import { WebAudio } from '/@/shared/types/types';
import { setQueue } from '/@/renderer/utils/set-transcoded-queue-data';
import { toast } from '/@/shared/components/toast/toast';
import { PlaybackType, PlayerStatus, WebAudio } from '/@/shared/types/types';
ModuleRegistry.registerModules([ClientSideRowModelModule, InfiniteRowModelModule]);
const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
const ipc = isElectron() ? window.api.ipc : null;
const remote = isElectron() ? window.api.remote : null;
const utils = isElectron() ? window.api.utils : null;
export const App = () => {
const { mode, theme } = useAppTheme();
const language = useSettingsStore((store) => store.general.language);
const { content, enabled } = useCssSettings();
const { type: playbackType } = usePlaybackSettings();
const { bindings } = useHotkeySettings();
const handlePlayQueueAdd = useHandlePlayQueueAdd();
const { clearQueue, restoreQueue } = useQueueControls();
const remoteSettings = useRemoteSettings();
const cssRef = useRef<HTMLStyleElement | null>(null);
useDiscordRpc();
useServerVersion();
@@ -63,16 +84,104 @@ export const App = () => {
return () => {};
}, [content, enabled]);
const providerValue = useMemo(() => {
return { handlePlayQueueAdd };
}, [handlePlayQueueAdd]);
const webAudioProvider = useMemo(() => {
return { setWebAudio, webAudio };
}, [webAudio]);
// Start the mpv instance on startup
useEffect(() => {
const initializeMpv = async () => {
if (playbackType === PlaybackType.LOCAL) {
const isRunning: boolean | undefined = await mpvPlayer?.isRunning();
mpvPlayer?.stop();
if (!isRunning) {
const extraParameters = useSettingsStore.getState().playback.mpvExtraParameters;
const properties: Record<string, any> = {
speed: usePlayerStore.getState().speed,
...getMpvProperties(useSettingsStore.getState().playback.mpvProperties),
};
await mpvPlayer?.initialize({
extraParameters,
properties,
});
mpvPlayer?.volume(properties.volume);
}
}
utils?.restoreQueue();
};
if (isElectron()) {
initializeMpv();
}
return () => {
clearQueue();
mpvPlayer?.stop();
mpvPlayer?.cleanup();
};
}, [clearQueue, playbackType]);
useEffect(() => {
if (isElectron()) {
ipc?.send('set-global-shortcuts', bindings);
}
}, [bindings]);
useEffect(() => {
if (utils) {
utils.onSaveQueue(() => {
const { current, queue } = usePlayerStore.getState();
const stateToSave: Partial<Pick<PlayerState, 'current' | 'queue'>> = {
current: {
...current,
status: PlayerStatus.PAUSED,
},
queue,
};
utils.saveQueue(stateToSave);
});
utils.onRestoreQueue((_event: any, data) => {
const playerData = restoreQueue(data);
if (playbackType === PlaybackType.LOCAL) {
setQueue(playerData, true);
}
updateSong(playerData.current.song);
});
}
return () => {
ipc?.removeAllListeners('renderer-restore-queue');
ipc?.removeAllListeners('renderer-save-queue');
};
}, [playbackType, restoreQueue]);
useEffect(() => {
if (remote) {
remote
?.updateSetting(
remoteSettings.enabled,
remoteSettings.port,
remoteSettings.username,
remoteSettings.password,
)
.catch((error) => {
toast.warn({ message: error, title: 'Failed to enable remote' });
});
}
// We only want to fire this once
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (language) {
i18n.changeLanguage(language);
@@ -80,7 +189,7 @@ export const App = () => {
}, [language]);
return (
<MantineProvider defaultColorScheme={mode as 'dark' | 'light'} theme={theme}>
<MantineProvider forceColorScheme={mode} theme={theme}>
<Notifications
containerWidth="300px"
position="bottom-center"
@@ -91,11 +200,11 @@ export const App = () => {
}}
zIndex={50000}
/>
<WebAudioContext.Provider value={webAudioProvider}>
<PlayerProvider>
<PlayQueueHandlerContext.Provider value={providerValue}>
<WebAudioContext.Provider value={webAudioProvider}>
<AppRouter />
</PlayerProvider>
</WebAudioContext.Provider>
</WebAudioContext.Provider>
</PlayQueueHandlerContext.Provider>
<IsUpdatedDialog />
</MantineProvider>
);
@@ -1,4 +1,4 @@
import type { QueueSong, Song } from '/@/shared/types/domain-types';
import type { Song } from '/@/shared/types/domain-types';
import type { CrossfadeStyle } from '/@/shared/types/types';
import type { ReactPlayerProps } from 'react-player';
@@ -18,17 +18,12 @@ import { api } from '/@/renderer/api';
import {
crossfadeHandler,
gaplessHandler,
} from '/@/renderer/features/player/audio-player/utils/list-handlers';
} from '/@/renderer/components/audio-player/utils/list-handlers';
import { useWebAudio } from '/@/renderer/features/player/hooks/use-webaudio';
import {
TranscodingConfig,
usePlaybackSettings,
usePlayerSpeed,
useSettingsStore,
useSettingsStoreActions,
} from '/@/renderer/store';
import { TranscodingConfig, usePlaybackSettings, useSpeed } from '/@/renderer/store';
import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store/settings.store';
import { toast } from '/@/shared/components/toast/toast';
import { PlayerStatus, PlayerStyle } from '/@/shared/types/types';
import { PlaybackStyle, PlayerStatus } from '/@/shared/types/types';
export type AudioPlayerProgress = {
loaded: number;
@@ -43,7 +38,7 @@ interface AudioPlayerProps extends ReactPlayerProps {
crossfadeStyle: CrossfadeStyle;
currentPlayer: 1 | 2;
muted: boolean;
playbackStyle: PlayerStyle;
playbackStyle: PlaybackStyle;
player1?: Song;
player2?: Song;
status: PlayerStatus;
@@ -62,30 +57,26 @@ const getDuration = (ref: any) => {
const EMPTY_SOURCE =
'data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU2LjM2LjEwMAAAAAAAAAAAAAAA//OEAAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAAAEAAABIADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV6urq6urq6urq6urq6urq6urq6urq6urq6v////////////////////////////////8AAAAATGF2YzU2LjQxAAAAAAAAAAAAAAAAJAAAAAAAAAAAASDs90hvAAAAAAAAAAAAAAAAAAAA//MUZAAAAAGkAAAAAAAAA0gAAAAATEFN//MUZAMAAAGkAAAAAAAAA0gAAAAARTMu//MUZAYAAAGkAAAAAAAAA0gAAAAAOTku//MUZAkAAAGkAAAAAAAAA0gAAAAANVVV';
const useSongUrl = (
transcode: TranscodingConfig,
current: boolean,
song?: QueueSong,
): null | string => {
const useSongUrl = (transcode: TranscodingConfig, current: boolean, song?: Song): null | string => {
const prior = useRef(['', '']);
return useMemo(() => {
if (song?._serverId) {
if (song?.serverId) {
// If we are the current track, we do not want a transcoding
// reconfiguration to force a restart.
if (current && prior.current[0] === song._uniqueId) {
if (current && prior.current[0] === song.uniqueId) {
return prior.current[1];
}
if (!transcode.enabled) {
// transcoding disabled; save the result
prior.current = [song._uniqueId, song.streamUrl];
prior.current = [song.uniqueId, song.streamUrl];
return song.streamUrl;
}
const result = api.controller.getTranscodingUrl({
apiClientProps: {
serverId: song._serverId,
serverId: song.serverId,
},
query: {
base: song.streamUrl,
@@ -94,14 +85,14 @@ const useSongUrl = (
})!;
// transcoding enabled; save the updated result
prior.current = [song._uniqueId, result];
prior.current = [song.uniqueId, result];
return result;
}
// no track; clear result
prior.current = ['', ''];
return null;
}, [current, song?._uniqueId, song?._serverId, song?.streamUrl, transcode]);
}, [current, song?.uniqueId, song?.serverId, song?.streamUrl, transcode]);
};
export interface AudioPlayerRef {
@@ -131,7 +122,7 @@ export const AudioPlayer = forwardRef<AudioPlayerRef, AudioPlayerProps>((props,
const shouldUseWebAudio = useSettingsStore((state) => state.playback.webAudio);
const preservesPitch = useSettingsStore((state) => state.playback.preservePitch);
const { resetSampleRate } = useSettingsStoreActions();
const playbackSpeed = usePlayerSpeed();
const playbackSpeed = useSpeed();
const { transcode } = usePlaybackSettings();
const stream1 = useSongUrl(transcode, currentPlayer === 1, player1);
@@ -457,7 +448,7 @@ export const AudioPlayer = forwardRef<AudioPlayerRef, AudioPlayerProps>((props,
onEnded={stream1 ? handleOnEnded : undefined}
onError={handleOnError(player1Ref)}
onProgress={
playbackStyle === PlayerStyle.GAPLESS ? handleGapless1 : handleCrossfade1
playbackStyle === PlaybackStyle.GAPLESS ? handleGapless1 : handleCrossfade1
}
onReady={handlePlayer1Start}
playbackRate={playbackSpeed}
@@ -477,7 +468,7 @@ export const AudioPlayer = forwardRef<AudioPlayerRef, AudioPlayerProps>((props,
onEnded={stream2 ? handleOnEnded : undefined}
onError={handleOnError(player2Ref)}
onProgress={
playbackStyle === PlayerStyle.GAPLESS ? handleGapless2 : handleCrossfade2
playbackStyle === PlaybackStyle.GAPLESS ? handleGapless2 : handleCrossfade2
}
onReady={handlePlayer2Start}
playbackRate={playbackSpeed}
@@ -59,6 +59,7 @@ export const PosterCard = ({
<Image className={styles.image} src={data?.imageUrl} />
<GridCardControls
handleFavorite={controls.handleFavorite}
handlePlayQueueAdd={controls.handlePlayQueueAdd}
isHovered={isHovered}
itemData={data}
itemType={controls.itemType}
@@ -9,16 +9,16 @@ import {
PLAYLIST_CONTEXT_MENU_ITEMS,
} from '/@/renderer/features/context-menu/context-menu-items';
import { useHandleGridContextMenu } from '/@/renderer/features/context-menu/hooks/use-handle-context-menu';
import { usePlayerContext } from '/@/renderer/features/player/context/player-context';
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Button } from '/@/shared/components/button/button';
import { Icon } from '/@/shared/components/icon/icon';
import { LibraryItem } from '/@/shared/types/domain-types';
import { Play } from '/@/shared/types/types';
import { Play, PlayQueueAddOptions } from '/@/shared/types/types';
export const GridCardControls = ({
handleFavorite,
handlePlayQueueAdd,
isHovered,
itemData,
itemType,
@@ -30,6 +30,7 @@ export const GridCardControls = ({
itemType: LibraryItem;
serverId: string;
}) => void;
handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
isHovered?: boolean;
itemData: any;
itemType: LibraryItem;
@@ -38,18 +39,17 @@ export const GridCardControls = ({
const [isFavorite, setIsFavorite] = useState(itemData?.userFavorite);
const playButtonBehavior = usePlayButtonBehavior();
const player = usePlayerContext();
const handlePlay = async (e: MouseEvent<HTMLButtonElement>, playType?: Play) => {
e.preventDefault();
e.stopPropagation();
player.addToQueueByFetch(
itemData._serverId,
[itemData.id],
itemType,
playType || playButtonBehavior,
);
handlePlayQueueAdd?.({
byItemType: {
id: [itemData.id],
type: itemType,
},
playType: playType || playButtonBehavior,
});
};
const handleFavorites = async (e: MouseEvent<HTMLButtonElement>, serverId: string) => {
@@ -3,17 +3,17 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/li
import { RowClassRules, RowNode } from '@ag-grid-community/core';
import { MutableRefObject, useEffect, useMemo, useRef } from 'react';
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
import { useAppFocus } from '/@/renderer/hooks';
import { usePlayerSong } from '/@/renderer/store';
import { useCurrentSong, usePlayerStore } from '/@/renderer/store';
import { Song } from '/@/shared/types/domain-types';
import { PlayerStatus } from '/@/shared/types/types';
interface UseCurrentSongRowStylesProps {
tableRef: MutableRefObject<AgGridReactType | null>;
}
export const useCurrentSongRowStyles = ({ tableRef }: UseCurrentSongRowStylesProps) => {
const currentSong = usePlayerSong();
const currentSong = useCurrentSong();
const isFocused = useAppFocus();
const isFocusedRef = useRef<boolean>(isFocused);
@@ -50,12 +50,11 @@ export const useCurrentSongRowStyles = ({ tableRef }: UseCurrentSongRowStylesPro
};
}, [currentSong?.albumId, currentSong?.id]);
usePlayerEvents(
{
onCurrentSongChange: (properties, prev) => {
const song = properties.song;
const previousSong = prev.song;
useEffect(() => {
// Redraw song rows when current song changes
const unsubSongChange = usePlayerStore.subscribe(
(state) => state.current.song,
(song, previousSong) => {
if (tableRef?.current) {
const { api, columnApi } = tableRef?.current || {};
if (api == null || columnApi == null) {
@@ -75,8 +74,14 @@ export const useCurrentSongRowStyles = ({ tableRef }: UseCurrentSongRowStylesPro
api.redrawRows({ rowNodes });
}
},
onPlayerStatus: () => {
const song = currentSong;
{ equalityFn: (a, b) => a?.id === b?.id },
);
// Redraw song rows when the status changes
const unsubStatusChange = usePlayerStore.subscribe(
(state) => [state.current.status, state.current.song],
(current: (PlayerStatus | Song | undefined)[]) => {
const song = current[1] as Song;
if (tableRef?.current) {
const { api, columnApi } = tableRef?.current || {};
@@ -90,9 +95,16 @@ export const useCurrentSongRowStyles = ({ tableRef }: UseCurrentSongRowStylesPro
api.redrawRows({ rowNodes });
}
},
},
[tableRef],
);
{
equalityFn: (a, b) => (a[0] as PlayerStatus) === (b[0] as PlayerStatus),
},
);
return () => {
unsubSongChange();
unsubStatusChange();
};
}, [tableRef]);
return {
rowClassRules,
@@ -36,7 +36,7 @@ import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-
import { useAppFocus, useContainerQuery } from '/@/renderer/hooks';
import { useGenreRoute } from '/@/renderer/hooks/use-genre-route';
import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer, usePlayerSong, usePlayerStatus } from '/@/renderer/store';
import { useCurrentServer, useCurrentSong, useCurrentStatus } from '/@/renderer/store';
import {
PersistedTableColumn,
useGeneralSettings,
@@ -85,9 +85,9 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
const handlePlayQueueAdd = usePlayQueueAdd();
const tableConfig = useTableSettings('albumDetail');
const { setTable } = useSettingsStoreActions();
const status = usePlayerStatus();
const status = useCurrentStatus();
const isFocused = useAppFocus();
const currentSong = usePlayerSong();
const currentSong = useCurrentSong();
const { externalLinks, lastFM, musicBrainz } = useGeneralSettings();
const genreRoute = useGenreRoute();
@@ -290,7 +290,7 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
if (detailQuery.data.userFavorite) {
deleteFavoriteMutation.mutate({
apiClientProps: { serverId: detailQuery.data._serverId },
apiClientProps: { serverId: detailQuery.data.serverId },
query: {
id: [detailQuery.data.id],
type: LibraryItem.ALBUM,
@@ -298,7 +298,7 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
});
} else {
createFavoriteMutation.mutate({
apiClientProps: { serverId: detailQuery.data._serverId },
apiClientProps: { serverId: detailQuery.data.serverId },
query: {
id: [detailQuery.data.id],
type: LibraryItem.ALBUM,
@@ -41,8 +41,8 @@ export const AlbumDetailHeader = forwardRef(
const { t } = useTranslation();
const showRating =
detailQuery?.data?._serverType === ServerType.NAVIDROME ||
detailQuery?.data?._serverType === ServerType.SUBSONIC;
detailQuery?.data?.serverType === ServerType.NAVIDROME ||
detailQuery?.data?.serverType === ServerType.SUBSONIC;
const originalDifferentFromRelease =
detailQuery.data?.originalDate &&
@@ -125,7 +125,7 @@ export const AlbumDetailHeader = forwardRef(
if (originalDifferentFromRelease) {
const formatted = `${formatDateAbsoluteUTC(detailQuery!.data!.originalDate)}`;
metadataItems.splice(0, 0, {
metadataItems.splice(releaseTypes.length, 0, {
id: 'originalDate',
value: formatted,
});
@@ -137,7 +137,7 @@ export const AlbumDetailHeader = forwardRef(
if (!detailQuery?.data) return;
updateRatingMutation.mutate({
apiClientProps: { serverId: detailQuery.data._serverId },
apiClientProps: { serverId: detailQuery.data.serverId },
query: {
item: [detailQuery.data],
rating,
@@ -69,7 +69,7 @@ const DummyAlbumDetailRoute = () => {
try {
if (wasFavorite) {
await deleteFavoriteMutation.mutateAsync({
apiClientProps: { serverId: detailQuery.data._serverId },
apiClientProps: { serverId: detailQuery.data.serverId },
query: {
id: [detailQuery.data.id],
type: LibraryItem.SONG,
@@ -77,7 +77,7 @@ const DummyAlbumDetailRoute = () => {
});
} else {
await createFavoriteMutation.mutateAsync({
apiClientProps: { serverId: detailQuery.data._serverId },
apiClientProps: { serverId: detailQuery.data.serverId },
query: {
id: [detailQuery.data.id],
type: LibraryItem.SONG,
@@ -79,7 +79,7 @@ export const AlbumArtistDetailHeader = forwardRef(
});
};
const showRating = detailQuery?.data?._serverType === ServerType.NAVIDROME;
const showRating = detailQuery?.data?.serverType === ServerType.NAVIDROME;
return (
<LibraryHeader
File diff suppressed because it is too large Load Diff
@@ -18,6 +18,11 @@ import { PlayerStatus } from '/@/shared/types/types';
const discordRpc = isElectron() ? window.api.discordRpc : null;
type ActivityState = [QueueSong | undefined, number, PlayerStatus];
const MAX_FIELD_LENGTH = 127;
const truncate = (field: string) =>
field.length <= MAX_FIELD_LENGTH ? field : field.substring(0, MAX_FIELD_LENGTH - 1) + '…';
export const useDiscordRpc = () => {
const discordSettings = useDiscordSettings();
const generalSettings = useGeneralSettings();
@@ -36,7 +41,7 @@ export const useDiscordRpc = () => {
// Handle change detection
const song = current[0];
const trackChanged = lastUniqueId !== song._uniqueId;
const trackChanged = lastUniqueId !== song.uniqueId;
/*
1. If the song has just started, update status
@@ -51,7 +56,7 @@ export const useDiscordRpc = () => {
current[2] !== previous[2]
) {
if (trackChanged) {
setlastUniqueId(song._uniqueId);
setlastUniqueId(song.uniqueId);
}
const start = Math.round(Date.now() - current[1] * 1000);
@@ -66,13 +71,15 @@ export const useDiscordRpc = () => {
};
const activity: SetActivity = {
details: (song?.name && song.name.padEnd(2, ' ')) || 'Idle',
details: truncate((song?.name && song.name.padEnd(2, ' ')) || 'Idle'),
instance: false,
largeImageKey: undefined,
largeImageText: (song?.album && song.album.padEnd(2, ' ')) || 'Unknown album',
largeImageText: truncate(
(song?.album && song.album.padEnd(2, ' ')) || 'Unknown album',
),
smallImageKey: undefined,
smallImageText: sentenceCase(current[2]),
state: (artists && artists.padEnd(2, ' ')) || 'Unknown artist',
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,
// but manually installing the discord-types package appears to break things
@@ -119,12 +126,12 @@ export const useDiscordRpc = () => {
}
if (discordSettings.showServerImage && song) {
if (song._serverType === ServerType.JELLYFIN && song.imageUrl) {
if (song.serverType === ServerType.JELLYFIN && song.imageUrl) {
activity.largeImageKey = song.imageUrl;
} else if (song._serverType === ServerType.NAVIDROME) {
} else if (song.serverType === ServerType.NAVIDROME) {
try {
const info = await controller.getAlbumInfo({
apiClientProps: { serverId: song._serverId },
apiClientProps: { serverId: song.serverId },
query: { id: song.albumId },
});
@@ -99,7 +99,7 @@ export const lyricsQueries = {
return queryOptions({
gcTime: Infinity,
queryFn: async ({ signal }): Promise<FullLyricsMetadata | null | StructuredLyric[]> => {
const server = getServerById(song?._serverId);
const server = getServerById(song?.serverId);
if (!server) throw new Error('Server not found');
if (!song) return null;
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import { openLyricSearchModal } from '/@/renderer/features/lyrics/components/lyrics-search-form';
import {
usePlayerSong,
useCurrentSong,
useLyricsSettings,
useSettingsStore,
useSettingsStoreActions,
@@ -38,7 +38,7 @@ export const LyricsActions = ({
setIndex,
}: LyricsActionsProps) => {
const { t } = useTranslation();
const currentSong = usePlayerSong();
const currentSong = useCurrentSong();
const { setSettings } = useSettingsStoreActions();
const { delayMs, sources } = useLyricsSettings();
+8 -8
View File
@@ -20,7 +20,7 @@ import {
UnsynchronizedLyricsProps,
} from '/@/renderer/features/lyrics/unsynchronized-lyrics';
import { queryClient } from '/@/renderer/lib/react-query';
import { usePlayerSong, useLyricsSettings, usePlayerStore } from '/@/renderer/store';
import { useCurrentSong, useLyricsSettings, usePlayerStore } from '/@/renderer/store';
import { Center } from '/@/shared/components/center/center';
import { Group } from '/@/shared/components/group/group';
import { Icon } from '/@/shared/components/icon/icon';
@@ -29,7 +29,7 @@ import { Text } from '/@/shared/components/text/text';
import { FullLyricsMetadata, LyricSource, LyricsOverride } from '/@/shared/types/domain-types';
export const Lyrics = () => {
const currentSong = usePlayerSong();
const currentSong = useCurrentSong();
const {
enableAutoTranslation,
translationApiKey,
@@ -45,7 +45,7 @@ export const Lyrics = () => {
lyricsQueries.songLyrics(
{
query: { songId: currentSong?.id || '' },
serverId: currentSong?._serverId || '',
serverId: currentSong?.serverId || '',
},
currentSong,
),
@@ -73,13 +73,13 @@ export const Lyrics = () => {
const handleOnResetLyric = useCallback(() => {
queryClient.invalidateQueries({
exact: true,
queryKey: queryKeys.songs.lyrics(currentSong?._serverId, { songId: currentSong?.id }),
queryKey: queryKeys.songs.lyrics(currentSong?.serverId, { songId: currentSong?.id }),
});
}, [currentSong?.id, currentSong?._serverId]);
}, [currentSong?.id, currentSong?.serverId]);
const handleOnRemoveLyric = useCallback(() => {
queryClient.setQueryData(
queryKeys.songs.lyrics(currentSong?._serverId, { songId: currentSong?.id }),
queryKeys.songs.lyrics(currentSong?.serverId, { songId: currentSong?.id }),
(prev: FullLyricsMetadata | undefined) => {
if (!prev) {
return undefined;
@@ -91,7 +91,7 @@ export const Lyrics = () => {
};
},
);
}, [currentSong?.id, currentSong?._serverId]);
}, [currentSong?.id, currentSong?.serverId]);
const fetchTranslation = useCallback(async () => {
if (!lyrics) return;
@@ -126,7 +126,7 @@ export const Lyrics = () => {
remoteSource: override?.source as LyricSource | undefined,
song: currentSong,
},
serverId: currentSong?._serverId || '',
serverId: currentSong?.serverId || '',
}),
);
@@ -8,16 +8,17 @@ import { LyricLine } from '/@/renderer/features/lyrics/lyric-line';
import { useScrobble } from '/@/renderer/features/player/hooks/use-scrobble';
import { PlayersRef } from '/@/renderer/features/player/ref/players-ref';
import {
useCurrentPlayer,
useCurrentStatus,
useCurrentTime,
useLyricsSettings,
usePlaybackType,
usePlayerActions,
usePlayerData,
usePlayerNum,
usePlayerStatus,
usePlayerTimestamp,
useSeeked,
useSetCurrentTime,
} from '/@/renderer/store';
import { FullLyricsMetadata, SynchronizedLyricsArray } from '/@/shared/types/domain-types';
import { PlayerStatus, PlayerType } from '/@/shared/types/types';
import { PlaybackType, PlayerStatus } from '/@/shared/types/types';
const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
const utils = isElectron() ? window.api.utils : null;
@@ -37,33 +38,33 @@ export const SynchronizedLyrics = ({
translatedLyrics,
}: SynchronizedLyricsProps) => {
const playersRef = PlayersRef;
const status = usePlayerStatus();
const status = useCurrentStatus();
const playbackType = usePlaybackType();
const playerData = usePlayerData();
const now = usePlayerTimestamp();
const now = useCurrentTime();
const settings = useLyricsSettings();
const currentPlayer = usePlayerNum();
const currentPlayer = useCurrentPlayer();
const currentPlayerRef =
currentPlayer === 1 ? playersRef.current?.player1 : playersRef.current?.player2;
const setCurrentTime = useSetCurrentTime();
const { handleScrobbleFromSeek } = useScrobble();
const handleSeek = useCallback(
(time: number) => {
if (playbackType === PlayerType.LOCAL && mpvPlayer) {
if (playbackType === PlaybackType.LOCAL && mpvPlayer) {
mpvPlayer.seekTo(time);
// setCurrentTime(time, true);
setCurrentTime(time, true);
} else {
// setCurrentTime(time, true);
setCurrentTime(time, true);
handleScrobbleFromSeek(time);
mpris?.updateSeek(time);
currentPlayerRef?.seekTo(time);
}
},
[currentPlayerRef, handleScrobbleFromSeek, playbackType],
[currentPlayerRef, handleScrobbleFromSeek, playbackType, setCurrentTime],
);
// const seeked = useSeeked();
const seeked = useSeeked();
// A reference to the timeout handler
const lyricTimer = useRef<null | ReturnType<typeof setTimeout>>(null);
@@ -96,7 +97,7 @@ export const SynchronizedLyrics = ({
};
const getCurrentTime = useCallback(async () => {
if (isElectron() && playbackType !== PlayerType.WEB) {
if (isElectron() && playbackType !== PlaybackType.WEB) {
if (mpvPlayer) {
return mpvPlayer.getCurrentTime();
}
@@ -108,7 +109,7 @@ export const SynchronizedLyrics = ({
}
const player =
playerData.player.playerNum === 1
playerData.current.player === 1
? playersRef.current.player1
: playersRef.current.player2;
const underlying = player?.getInternalPlayer();
@@ -281,16 +282,16 @@ export const SynchronizedLyrics = ({
// we may be playing the same track (repeat one). In this case, we also
// need to restart playback
const restarted = status === PlayerStatus.PLAYING && now === 0;
// if (!seeked && !restarted) {
// return;
// }
if (!seeked && !restarted) {
return;
}
if (lyricTimer.current) {
clearTimeout(lyricTimer.current);
}
setCurrentLyric(now * 1000 - delayMsRef.current);
}, [now, setCurrentLyric, status]);
}, [now, seeked, setCurrentLyric, status]);
useEffect(() => {
// Guaranteed cleanup; stop the timer, and just in case also increment
@@ -7,12 +7,15 @@ import { useTranslation } from 'react-i18next';
import { TableConfigDropdown } from '/@/renderer/components/virtual-table';
import { updateSong } from '/@/renderer/features/player/update-remote-song';
import { SearchInput } from '/@/renderer/features/shared/components/search-input';
import { usePlayerControls, useQueueControls } from '/@/renderer/store';
import { usePlayerStore, useSetCurrentTime } from '/@/renderer/store/player.store';
import { usePlaybackType } from '/@/renderer/store/settings.store';
import { setQueue, setQueueNext } from '/@/renderer/utils/set-transcoded-queue-data';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Group } from '/@/shared/components/group/group';
import { Popover } from '/@/shared/components/popover/popover';
import { Song } from '/@/shared/types/domain-types';
import { TableType } from '/@/shared/types/types';
import { PlaybackType, TableType } from '/@/shared/types/types';
const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
@@ -30,30 +33,30 @@ export const PlayQueueListControls = ({
type,
}: PlayQueueListOptionsProps) => {
const { t } = useTranslation();
// const {
// clearQueue,
// moveToBottomOfQueue,
// moveToNextOfQueue,
// moveToTopOfQueue,
// removeFromQueue,
// shuffleQueue,
// } = useQueueControls();
const {
clearQueue,
moveToBottomOfQueue,
moveToNextOfQueue,
moveToTopOfQueue,
removeFromQueue,
shuffleQueue,
} = useQueueControls();
// const { pause } = usePlayerControls();
const { pause } = usePlayerControls();
const playbackType = usePlaybackType();
// const setCurrentTime = useSetCurrentTime();
const setCurrentTime = useSetCurrentTime();
const handleMoveToNext = () => {
const selectedRows = tableRef?.current?.grid.api.getSelectedRows();
const uniqueIds = selectedRows?.map((row) => row.uniqueId);
if (!uniqueIds?.length) return;
// const playerData = moveToNextOfQueue(uniqueIds);
const playerData = moveToNextOfQueue(uniqueIds);
// if (playbackType === PlaybackType.LOCAL) {
// setQueueNext(playerData);
// }
if (playbackType === PlaybackType.LOCAL) {
setQueueNext(playerData);
}
};
const handleMoveToBottom = () => {
@@ -61,11 +64,11 @@ export const PlayQueueListControls = ({
const uniqueIds = selectedRows?.map((row) => row.uniqueId);
if (!uniqueIds?.length) return;
// const playerData = moveToBottomOfQueue(uniqueIds);
const playerData = moveToBottomOfQueue(uniqueIds);
// if (playbackType === PlaybackType.LOCAL) {
// setQueueNext(playerData);
// }
if (playbackType === PlaybackType.LOCAL) {
setQueueNext(playerData);
}
};
const handleMoveToTop = () => {
@@ -73,11 +76,11 @@ export const PlayQueueListControls = ({
const uniqueIds = selectedRows?.map((row) => row.uniqueId);
if (!uniqueIds?.length) return;
// const playerData = moveToTopOfQueue(uniqueIds);
const playerData = moveToTopOfQueue(uniqueIds);
// if (playbackType === PlaybackType.LOCAL) {
// setQueueNext(playerData);
// }
if (playbackType === PlaybackType.LOCAL) {
setQueueNext(playerData);
}
};
const handleRemoveSelected = () => {
@@ -85,42 +88,43 @@ export const PlayQueueListControls = ({
const uniqueIds = selectedRows?.map((row) => row.uniqueId);
if (!uniqueIds?.length) return;
// const currentSong = usePlayerStore.getState().current.song;
// const playerData = removeFromQueue(uniqueIds);
// const isCurrentSongRemoved = currentSong && uniqueIds.includes(currentSong.uniqueId);
const currentSong = usePlayerStore.getState().current.song;
const playerData = removeFromQueue(uniqueIds);
const isCurrentSongRemoved = currentSong && uniqueIds.includes(currentSong.uniqueId);
// if (playbackType === PlaybackType.LOCAL) {
// if (isCurrentSongRemoved) {
// setQueue(playerData);
// } else {
// setQueueNext(playerData);
// }
// }
if (playbackType === PlaybackType.LOCAL) {
if (isCurrentSongRemoved) {
setQueue(playerData);
} else {
setQueueNext(playerData);
}
}
// if (isCurrentSongRemoved) {
// updateSong(playerData.current.song);
// }
if (isCurrentSongRemoved) {
updateSong(playerData.current.song);
}
};
const handleClearQueue = () => {
// const playerData = clearQueue();
const playerData = clearQueue();
// if (playbackType === PlaybackType.LOCAL) {
// setQueue(playerData);
// mpvPlayer!.pause();
// }
if (playbackType === PlaybackType.LOCAL) {
setQueue(playerData);
mpvPlayer!.pause();
}
updateSong(undefined);
// setCurrentTime(0);
// pause();
setCurrentTime(0);
pause();
};
const handleShuffleQueue = () => {
// const playerData = shuffleQueue();
// if (playbackType === PlaybackType.LOCAL) {
// setQueueNext(playerData);
// }
const playerData = shuffleQueue();
if (playbackType === PlaybackType.LOCAL) {
setQueueNext(playerData);
}
};
const handleSearchTerm = useCallback(
@@ -1,9 +1,15 @@
import type { RowClassRules, RowDragEvent, RowNode } from '@ag-grid-community/core';
import type {
CellDoubleClickedEvent,
RowClassRules,
RowDragEvent,
RowNode,
} from '@ag-grid-community/core';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import type { Ref } from 'react';
import '@ag-grid-community/styles/ag-theme-alpine.css';
import { useMergedRef } from '@mantine/hooks';
import '@ag-grid-community/styles/ag-theme-alpine.css';
import isElectron from 'is-electron';
import debounce from 'lodash/debounce';
import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
@@ -13,22 +19,32 @@ import { getColumnDefs, VirtualTable } from '/@/renderer/components/virtual-tabl
import { ErrorFallback } from '/@/renderer/features/action-required/components/error-fallback';
import { QUEUE_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
import { useHandleTableContextMenu } from '/@/renderer/features/context-menu/hooks/use-handle-context-menu';
import { PlayersRef } from '/@/renderer/features/player/ref/players-ref';
import { updateSong } from '/@/renderer/features/player/update-remote-song';
import { useAppFocus } from '/@/renderer/hooks';
import {
useAppStoreActions,
usePlayerQueue,
usePlayerSong,
usePlayerStatus,
useCurrentSong,
useCurrentStatus,
useDefaultQueue,
usePlayerControls,
usePreviousSong,
useQueueControls,
useVolume,
} from '/@/renderer/store';
import {
PersistedTableColumn,
usePlaybackType,
useSettingsStore,
useSettingsStoreActions,
useTableSettings,
} from '/@/renderer/store/settings.store';
import { searchSongs } from '/@/renderer/utils/search-songs';
import { setQueue, setQueueNext } from '/@/renderer/utils/set-transcoded-queue-data';
import { LibraryItem, QueueSong } from '/@/shared/types/domain-types';
import { TableType } from '/@/shared/types/types';
import { PlaybackType, TableType } from '/@/shared/types/types';
const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
type QueueProps = {
searchTerm?: string;
@@ -38,13 +54,18 @@ type QueueProps = {
export const PlayQueue = forwardRef(({ searchTerm, type }: QueueProps, ref: Ref<any>) => {
const tableRef = useRef<AgGridReactType | null>(null);
const mergedRef = useMergedRef(ref, tableRef);
const queue = usePlayerQueue();
const currentSong = usePlayerSong();
const status = usePlayerStatus();
const queue = useDefaultQueue();
const { reorderQueue, setCurrentTrack } = useQueueControls();
const currentSong = useCurrentSong();
const previousSong = usePreviousSong();
const status = useCurrentStatus();
const { setSettings } = useSettingsStoreActions();
const { setAppStore } = useAppStoreActions();
const tableConfig = useTableSettings(type);
const [gridApi, setGridApi] = useState<AgGridReactType | undefined>();
const playbackType = usePlaybackType();
const { play } = usePlayerControls();
const volume = useVolume();
const isFocused = useAppFocus();
const isFocusedRef = useRef<boolean>(isFocused);
@@ -73,26 +94,26 @@ export const PlayQueue = forwardRef(({ searchTerm, type }: QueueProps, ref: Ref<
[tableConfig.columns],
);
// const handleDoubleClick = (e: CellDoubleClickedEvent) => {
// const playerData = setCurrentTrack(e.data.uniqueId);
// updateSong(playerData.current.song);
const handleDoubleClick = (e: CellDoubleClickedEvent) => {
const playerData = setCurrentTrack(e.data.uniqueId);
updateSong(playerData.current.song);
// if (playbackType === PlaybackType.LOCAL) {
// mpvPlayer!.volume(volume);
// setQueue(playerData, false);
// } else {
// const player =
// playerData.current.player === 1
// ? PlayersRef.current?.player1
// : PlayersRef.current?.player2;
// const underlying = player?.getInternalPlayer();
// if (underlying) {
// underlying.currentTime = 0;
// }
// }
if (playbackType === PlaybackType.LOCAL) {
mpvPlayer!.volume(volume);
setQueue(playerData, false);
} else {
const player =
playerData.current.player === 1
? PlayersRef.current?.player1
: PlayersRef.current?.player2;
const underlying = player?.getInternalPlayer();
if (underlying) {
underlying.currentTime = 0;
}
}
// play();
// };
play();
};
const handleDragStart = () => {
if (type === 'sideDrawerQueue') {
@@ -104,14 +125,14 @@ export const PlayQueue = forwardRef(({ searchTerm, type }: QueueProps, ref: Ref<
const handleDragEnd = (e: RowDragEvent<QueueSong>) => {
if (!e.nodes.length) return;
const selectedUniqueIds = e.nodes
.map((node) => node.data?._uniqueId)
.map((node) => node.data?.uniqueId)
.filter((e) => e !== undefined);
// const playerData = reorderQueue(selectedUniqueIds as string[], e.overNode?.data?.uniqueId);
const playerData = reorderQueue(selectedUniqueIds as string[], e.overNode?.data?.uniqueId);
// if (playbackType === PlaybackType.LOCAL) {
// setQueueNext(playerData);
// }
if (playbackType === PlaybackType.LOCAL) {
setQueueNext(playerData);
}
if (type === 'sideDrawerQueue') {
setAppStore({ isReorderingQueue: false });
@@ -125,8 +146,8 @@ export const PlayQueue = forwardRef(({ searchTerm, type }: QueueProps, ref: Ref<
const handleGridReady = () => {
const { api } = tableRef?.current || {};
if (currentSong?._uniqueId) {
const currentNode = api?.getRowNode(currentSong?._uniqueId);
if (currentSong?.uniqueId) {
const currentNode = api?.getRowNode(currentSong?.uniqueId);
if (!currentNode) return;
api?.ensureNodeVisible(currentNode, 'middle');
@@ -178,18 +199,10 @@ export const PlayQueue = forwardRef(({ searchTerm, type }: QueueProps, ref: Ref<
const rowClassRules = useMemo<RowClassRules | undefined>(() => {
return {
'current-song': (params) => {
return params.data.uniqueId === currentSong?._uniqueId;
return params.data.uniqueId === currentSong?.uniqueId;
},
};
}, [currentSong?._uniqueId]);
const previousSongRef = useRef<QueueSong | undefined>(undefined);
useEffect(() => {
if (currentSong) {
previousSongRef.current = currentSong;
}
}, [currentSong]);
}, [currentSong?.uniqueId]);
// Redraw the current song row when the previous song changes
useEffect(() => {
@@ -199,11 +212,11 @@ export const PlayQueue = forwardRef(({ searchTerm, type }: QueueProps, ref: Ref<
return;
}
const currentNode = currentSong?._uniqueId
? api.getRowNode(currentSong._uniqueId)
const currentNode = currentSong?.uniqueId
? api.getRowNode(currentSong.uniqueId)
: undefined;
const previousNode = previousSongRef.current?._uniqueId
? api.getRowNode(previousSongRef.current?._uniqueId)
const previousNode = previousSong?.uniqueId
? api.getRowNode(previousSong?.uniqueId)
: undefined;
const rowNodes = [currentNode, previousNode].filter(
@@ -218,7 +231,7 @@ export const PlayQueue = forwardRef(({ searchTerm, type }: QueueProps, ref: Ref<
}
}
}
}, [currentSong, previousSongRef, tableConfig.followCurrentSong, status]);
}, [currentSong, previousSong, tableConfig.followCurrentSong, status]);
// As a separate rule, update the current row when focus changes. This is
// to prevent queue scrolling when the application loses and then gains focus.
@@ -230,8 +243,8 @@ export const PlayQueue = forwardRef(({ searchTerm, type }: QueueProps, ref: Ref<
return;
}
const currentNode = currentSong?._uniqueId
? api.getRowNode(currentSong._uniqueId)
const currentNode = currentSong?.uniqueId
? api.getRowNode(currentSong.uniqueId)
: undefined;
if (currentNode) {
@@ -253,7 +266,7 @@ export const PlayQueue = forwardRef(({ searchTerm, type }: QueueProps, ref: Ref<
columnDefs={columnDefs}
context={{
currentSong,
// handleDoubleClick,
handleDoubleClick,
isFocused,
isQueue: true,
itemType: LibraryItem.SONG,
@@ -263,7 +276,7 @@ export const PlayQueue = forwardRef(({ searchTerm, type }: QueueProps, ref: Ref<
deselectOnClickOutside={type === 'fullScreen'}
getRowId={(data) => data.data.uniqueId}
onCellContextMenu={onCellContextMenu}
// onCellDoubleClicked={handleDoubleClick}
onCellDoubleClicked={handleDoubleClick}
onColumnMoved={handleColumnChange}
onColumnResized={debouncedColumnChange}
onDragStarted={handleDragStart}
@@ -1,259 +0,0 @@
import type { RefObject } from 'react';
import isElectron from 'is-electron';
import { useEffect, useImperativeHandle, useRef, useState } from 'react';
import { AudioPlayer, PlayerOnProgressProps } from '/@/renderer/features/player/audio-player/types';
import { getMpvProperties } from '/@/renderer/features/settings/components/playback/mpv-settings';
import { useSettingsStore } from '/@/renderer/store';
import { PlayerStatus } from '/@/shared/types/types';
export interface MpvPlayerEngineHandle extends AudioPlayer {}
interface MpvPlayerEngineProps {
currentSrc: string | undefined;
isMuted: boolean;
isTransitioning: boolean;
nextSrc: string | undefined;
onEnded: () => void;
onProgress: (e: PlayerOnProgressProps) => void;
playerRef: RefObject<MpvPlayerEngineHandle>;
playerStatus: PlayerStatus;
speed?: number;
volume: number;
}
const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
const mpvPlayerListener = isElectron() ? window.api.mpvPlayerListener : null;
const ipc = isElectron() ? window.api.ipc : null;
const utils = isElectron() ? window.api.utils : null;
const PROGRESS_UPDATE_INTERVAL = 250;
const TRANSITION_PROGRESS_INTERVAL = 10;
export const MpvPlayerEngine = (props: MpvPlayerEngineProps) => {
const {
currentSrc,
isMuted,
isTransitioning,
nextSrc,
onEnded,
onProgress,
playerRef,
playerStatus,
speed,
volume,
} = props;
const [internalVolume, setInternalVolume] = useState(volume / 100 || 0);
const [duration] = useState(0);
const [previousCurrentSrc, setPreviousCurrentSrc] = useState<string | undefined>(currentSrc);
const progressIntervalRef = useRef<NodeJS.Timeout | null>(null);
const mpvExtraParameters = useSettingsStore((store) => store.playback.mpvExtraParameters);
const mpvProperties = useSettingsStore((store) => store.playback.mpvProperties);
// Start the mpv instance on startup
useEffect(() => {
const initializeMpv = async () => {
const isRunning: boolean | undefined = await mpvPlayer?.isRunning();
mpvPlayer?.stop();
if (!isRunning) {
const properties: Record<string, any> = {
// speed: usePlayerStore.getState().speed,
...getMpvProperties(mpvProperties),
};
await mpvPlayer?.initialize({
extraParameters: mpvExtraParameters,
properties,
});
mpvPlayer?.volume(properties.volume);
}
utils?.restoreQueue();
};
initializeMpv();
return () => {
mpvPlayer?.stop();
mpvPlayer?.cleanup();
};
}, [mpvExtraParameters, mpvProperties]);
// Update volume
useEffect(() => {
if (!mpvPlayer) {
return;
}
const vol = volume / 100 || 0;
setInternalVolume(vol);
mpvPlayer.volume(volume);
}, [volume]);
// Update mute status
useEffect(() => {
if (!mpvPlayer) {
return;
}
mpvPlayer.mute(isMuted);
}, [isMuted]);
// Update speed/playback rate
useEffect(() => {
if (!mpvPlayer) {
return;
}
if (!speed) {
return;
}
mpvPlayer.setProperties({ speed });
}, [speed]);
// Handle current song changes - update queue position 0
// When currentSrc changes, we need to update the queue
useEffect(() => {
if (!mpvPlayer) {
return;
}
// If currentSrc changed, update the queue
if (currentSrc !== previousCurrentSrc) {
if (currentSrc) {
// Set current song at position 0 and next song at position 1
mpvPlayer.setQueue(currentSrc, nextSrc, playerStatus !== PlayerStatus.PLAYING);
setPreviousCurrentSrc(currentSrc);
} else {
// Clear queue if no current song
mpvPlayer.setQueue(undefined, undefined, true);
setPreviousCurrentSrc(undefined);
}
} else {
// If currentSrc hasn't changed but nextSrc has, update position 1
// This happens when the next song changes but current song stays the same
if (currentSrc) {
mpvPlayer.setQueueNext(nextSrc);
}
}
}, [currentSrc, previousCurrentSrc, nextSrc, playerStatus]);
// Handle play/pause status
useEffect(() => {
if (!mpvPlayer) {
return;
}
if (playerStatus === PlayerStatus.PLAYING) {
mpvPlayer.play();
} else if (playerStatus === PlayerStatus.PAUSED) {
mpvPlayer.pause();
}
}, [playerStatus]);
// Set up progress tracking
useEffect(() => {
if (progressIntervalRef.current) {
clearInterval(progressIntervalRef.current);
}
const updateProgress = async () => {
if (!mpvPlayer) {
return;
}
try {
const time = await mpvPlayer.getCurrentTime();
if (time !== undefined) {
onProgress({
played: time / (duration || time + 10),
playedSeconds: time,
});
}
} catch {
// Handle error silently
}
};
if (currentSrc) {
const interval = isTransitioning
? TRANSITION_PROGRESS_INTERVAL
: PROGRESS_UPDATE_INTERVAL;
progressIntervalRef.current = setInterval(updateProgress, interval);
updateProgress();
}
return () => {
if (progressIntervalRef.current) {
clearInterval(progressIntervalRef.current);
}
};
}, [currentSrc, isTransitioning, duration, onProgress]);
useEffect(() => {
if (!mpvPlayerListener) {
return;
}
const handleOnEnded = () => {
onEnded();
};
mpvPlayerListener.rendererAutoNext(handleOnEnded);
return () => {
ipc?.removeAllListeners('renderer-player-auto-next');
};
}, [nextSrc, onEnded]);
useImperativeHandle<MpvPlayerEngineHandle, MpvPlayerEngineHandle>(playerRef, () => ({
decreaseVolume(by: number) {
const newVol = Math.max(0, internalVolume - by / 100);
setInternalVolume(newVol);
if (mpvPlayer) {
mpvPlayer.volume(newVol * 100);
}
},
increaseVolume(by: number) {
const newVol = Math.min(1, internalVolume + by / 100);
setInternalVolume(newVol);
if (mpvPlayer) {
mpvPlayer.volume(newVol * 100);
}
},
pause() {
if (mpvPlayer) {
mpvPlayer.pause();
}
},
play() {
if (mpvPlayer) {
mpvPlayer.play();
}
},
seekTo(seekTo: number) {
if (mpvPlayer) {
mpvPlayer.seekTo(seekTo);
}
},
setVolume(vol: number) {
const volDecimal = vol / 100 || 0;
setInternalVolume(volDecimal);
if (mpvPlayer) {
mpvPlayer.volume(vol);
}
},
}));
return <div id="mpv-player-engine" style={{ display: 'none' }} />;
};
MpvPlayerEngine.displayName = 'MpvPlayerEngine';
@@ -1,166 +0,0 @@
import type { RefObject } from 'react';
import { useImperativeHandle, useRef, useState } from 'react';
import ReactPlayer from 'react-player';
import { AudioPlayer, PlayerOnProgressProps } from '/@/renderer/features/player/audio-player/types';
import { convertToLogVolume } from '/@/renderer/features/player/audio-player/utils/player-utils';
import { PlayerStatus } from '/@/shared/types/types';
export interface WebPlayerEngineHandle extends AudioPlayer {
player1(): {
ref: null | ReactPlayer;
setVolume: (volume: number) => void;
};
player2(): {
ref: null | ReactPlayer;
setVolume: (volume: number) => void;
};
}
interface WebPlayerEngineProps {
isMuted: boolean;
isTransitioning: boolean;
onEndedPlayer1: () => void;
onEndedPlayer2: () => void;
onProgressPlayer1: (e: PlayerOnProgressProps) => void;
onProgressPlayer2: (e: PlayerOnProgressProps) => void;
playerNum: number;
playerRef: RefObject<WebPlayerEngineHandle>;
playerStatus: PlayerStatus;
speed?: number;
src1: string | undefined;
src2: string | undefined;
volume: number;
}
// Credits: https://gist.github.com/novwhisky/8a1a0168b94f3b6abfaa?permalink_comment_id=1551393#gistcomment-1551393
// This is used so that the player will always have an <audio> element. This means that
// player1Source and player2Source are connected BEFORE the user presses play for
// the first time. This workaround is important for Safari, which seems to require the
// source to be connected PRIOR to resuming audio context
const EMPTY_SOURCE =
'data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU2LjM2LjEwMAAAAAAAAAAAAAAA//OEAAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAAAEAAABIADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV6urq6urq6urq6urq6urq6urq6urq6urq6v////////////////////////////////8AAAAATGF2YzU2LjQxAAAAAAAAAAAAAAAAJAAAAAAAAAAAASDs90hvAAAAAAAAAAAAAAAAAAAA//MUZAAAAAGkAAAAAAAAA0gAAAAATEFN//MUZAMAAAGkAAAAAAAAA0gAAAAARTMu//MUZAYAAAGkAAAAAAAAA0gAAAAAOTku//MUZAkAAAGkAAAAAAAAA0gAAAAANVVV';
export const WebPlayerEngine = (props: WebPlayerEngineProps) => {
const {
isMuted,
isTransitioning,
onEndedPlayer1,
onEndedPlayer2,
onProgressPlayer1,
onProgressPlayer2,
playerNum,
playerRef,
playerStatus,
speed,
src1,
src2,
volume,
} = props;
const player1Ref = useRef<null | ReactPlayer>(null);
const player2Ref = useRef<null | ReactPlayer>(null);
const [internalVolume1, setInternalVolume1] = useState(volume / 100 || 0);
const [internalVolume2, setInternalVolume2] = useState(volume / 100 || 0);
useImperativeHandle<WebPlayerEngineHandle, WebPlayerEngineHandle>(playerRef, () => ({
decreaseVolume(by: number) {
setInternalVolume1(Math.max(0, internalVolume1 - by / 100));
setInternalVolume2(Math.max(0, internalVolume2 - by / 100));
},
increaseVolume(by: number) {
setInternalVolume1(Math.min(1, internalVolume1 + by / 100));
setInternalVolume2(Math.min(1, internalVolume2 + by / 100));
},
pause() {
player1Ref.current?.getInternalPlayer()?.pause();
player2Ref.current?.getInternalPlayer()?.pause();
},
play() {
if (playerNum === 1) {
player1Ref.current?.getInternalPlayer()?.play();
} else {
player2Ref.current?.getInternalPlayer()?.play();
}
},
player1() {
return {
ref: player1Ref?.current,
setVolume: (volume: number) => setInternalVolume1(volume / 100 || 0),
};
},
player2() {
return {
ref: player2Ref?.current,
setVolume: (volume: number) => setInternalVolume2(volume / 100 || 0),
};
},
seekTo(seekTo: number) {
playerNum === 1
? player1Ref.current?.seekTo(seekTo)
: player2Ref.current?.seekTo(seekTo);
},
setVolume(volume: number) {
setInternalVolume1(volume / 100 || 0);
setInternalVolume2(volume / 100 || 0);
},
setVolume1(volume: number) {
setInternalVolume1(volume / 100 || 0);
},
setVolume2(volume: number) {
setInternalVolume2(volume / 100 || 0);
},
}));
const volume1 = convertToLogVolume(internalVolume1);
const volume2 = convertToLogVolume(internalVolume2);
return (
<div id="web-player-engine" style={{ display: 'none' }}>
{Boolean(src1) && (
<ReactPlayer
config={{
file: { attributes: { crossOrigin: 'anonymous' }, forceAudio: true },
}}
controls={false}
height={0}
id="web-player-1"
muted={isMuted}
onEnded={src1 ? () => onEndedPlayer1() : undefined}
onProgress={onProgressPlayer1}
playbackRate={speed || 1}
playing={playerNum === 1 && playerStatus === PlayerStatus.PLAYING}
progressInterval={isTransitioning ? 10 : 250}
ref={player1Ref}
url={src1 || EMPTY_SOURCE}
volume={volume1}
width={0}
/>
)}
{Boolean(src2) && (
<ReactPlayer
config={{
file: { attributes: { crossOrigin: 'anonymous' }, forceAudio: true },
}}
controls={false}
height={0}
id="web-player-2"
muted={isMuted}
onEnded={src2 ? () => onEndedPlayer2() : undefined}
onProgress={onProgressPlayer2}
playbackRate={speed || 1}
playing={playerNum === 2 && playerStatus === PlayerStatus.PLAYING}
progressInterval={isTransitioning ? 10 : 250}
ref={player2Ref}
url={src2 || EMPTY_SOURCE}
volume={volume2}
width={0}
/>
)}
</div>
);
};
WebPlayerEngine.displayName = 'WebPlayerEngine';
@@ -1,147 +0,0 @@
import { t } from 'i18next';
import isElectron from 'is-electron';
import { useCallback, useEffect } from 'react';
import { usePlayerActions } from '/@/renderer/store';
import { toast } from '/@/shared/components/toast/toast';
const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
const mpvPlayerListener = isElectron() ? window.api.mpvPlayerListener : null;
const ipc = isElectron() ? window.api.ipc : null;
export const useMainPlayerListener = () => {
const {
decreaseVolume,
increaseVolume,
mediaAutoNext,
mediaNext,
mediaPause,
mediaPlay,
mediaPrevious,
mediaSkipBackward,
mediaSkipForward,
mediaStop,
mediaToggleMute,
mediaTogglePlayPause,
toggleRepeat,
toggleShuffle,
} = usePlayerActions();
const handleMpvError = useCallback(
(message: string) => {
toast.error({
id: 'mpv-error',
message,
title: t('error.playbackError', { postProcess: 'sentenceCase' }) as string,
});
mediaPause();
mpvPlayer!.pause();
},
[mediaPause],
);
useEffect(() => {
if (!mpvPlayerListener) {
return;
}
mpvPlayerListener.rendererPlayPause(() => {
mediaTogglePlayPause();
});
mpvPlayerListener.rendererNext(() => {
mediaNext();
});
mpvPlayerListener.rendererPrevious(() => {
mediaPrevious();
});
mpvPlayerListener.rendererPlayPause(() => {
mediaTogglePlayPause();
});
mpvPlayerListener.rendererPlay(() => {
mediaPlay();
});
mpvPlayerListener.rendererPause(() => {
mediaPause();
});
mpvPlayerListener.rendererStop(() => {
mediaStop();
});
mpvPlayerListener.rendererSkipForward(() => {
mediaSkipForward();
});
mpvPlayerListener.rendererSkipBackward(() => {
mediaSkipBackward();
});
mpvPlayerListener.rendererAutoNext(() => {
mediaAutoNext();
});
mpvPlayerListener.rendererToggleShuffle(() => {
toggleShuffle();
});
mpvPlayerListener.rendererToggleRepeat(() => {
toggleRepeat();
});
mpvPlayerListener.rendererVolumeMute(() => {
mediaToggleMute();
});
mpvPlayerListener.rendererVolumeUp(() => {
increaseVolume(1);
});
mpvPlayerListener.rendererVolumeDown(() => {
decreaseVolume(1);
});
mpvPlayerListener.rendererError((_event: any, message: string) => {
handleMpvError(message);
});
return () => {
ipc?.removeAllListeners('renderer-player-play-pause');
ipc?.removeAllListeners('renderer-player-next');
ipc?.removeAllListeners('renderer-player-previous');
ipc?.removeAllListeners('renderer-player-play-pause');
ipc?.removeAllListeners('renderer-player-play');
ipc?.removeAllListeners('renderer-player-pause');
ipc?.removeAllListeners('renderer-player-stop');
ipc?.removeAllListeners('renderer-player-skip-forward');
ipc?.removeAllListeners('renderer-player-skip-backward');
ipc?.removeAllListeners('renderer-player-auto-next');
ipc?.removeAllListeners('renderer-player-toggle-shuffle');
ipc?.removeAllListeners('renderer-player-toggle-repeat');
ipc?.removeAllListeners('renderer-player-volume-mute');
ipc?.removeAllListeners('renderer-player-volume-up');
ipc?.removeAllListeners('renderer-player-volume-down');
ipc?.removeAllListeners('renderer-player-error');
};
}, [
decreaseVolume,
handleMpvError,
increaseVolume,
mediaAutoNext,
mediaNext,
mediaPause,
mediaPlay,
mediaPrevious,
mediaSkipForward,
mediaSkipBackward,
mediaStop,
mediaToggleMute,
mediaTogglePlayPause,
toggleRepeat,
toggleShuffle,
]);
};
@@ -1,124 +0,0 @@
import { useEffect } from 'react';
import {
subscribeCurrentTrack,
subscribePlayerMute,
subscribePlayerProgress,
subscribePlayerQueue,
subscribePlayerRepeat,
subscribePlayerSeekToTimestamp,
subscribePlayerShuffle,
subscribePlayerSpeed,
subscribePlayerStatus,
subscribePlayerVolume,
} from '/@/renderer/store';
import { QueueData, QueueSong } from '/@/shared/types/domain-types';
import { PlayerRepeat, PlayerShuffle, PlayerStatus } from '/@/shared/types/types';
interface PlayerEvents {
cleanup: () => void;
}
interface PlayerEventsCallbacks {
onCurrentSongChange?: (
properties: { index: number; song: QueueSong | undefined },
prev: { index: number; song: QueueSong | undefined },
) => void;
onPlayerMute?: (properties: { muted: boolean }, prev: { muted: boolean }) => void;
onPlayerProgress?: (properties: { timestamp: number }, prev: { timestamp: number }) => void;
onPlayerQueueChange?: (queue: QueueData, prev: QueueData) => void;
onPlayerRepeat?: (properties: { repeat: PlayerRepeat }, prev: { repeat: PlayerRepeat }) => void;
onPlayerSeek?: (properties: { seconds: number }, prev: { seconds: number }) => void;
onPlayerSeekToTimestamp?: (
properties: { timestamp: number },
prev: { timestamp: number },
) => void;
onPlayerShuffle?: (
properties: { shuffle: PlayerShuffle },
prev: { shuffle: PlayerShuffle },
) => void;
onPlayerSpeed?: (properties: { speed: number }, prev: { speed: number }) => void;
onPlayerStatus?: (properties: { status: PlayerStatus }, prev: { status: PlayerStatus }) => void;
onPlayerVolume?: (properties: { volume: number }, prev: { volume: number }) => void;
}
export function usePlayerEvents(callbacks: PlayerEventsCallbacks, deps: React.DependencyList) {
useEffect(() => {
const engine = createPlayerEvents(callbacks);
return () => {
engine.cleanup();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [...deps]);
}
function createPlayerEvents(callbacks: PlayerEventsCallbacks): PlayerEvents {
const unsubscribers: (() => void)[] = [];
// Subscribe to current track changes
if (callbacks.onCurrentSongChange) {
const unsubscribe = subscribeCurrentTrack(callbacks.onCurrentSongChange);
unsubscribers.push(unsubscribe);
}
// Subscribe to player progress
if (callbacks.onPlayerProgress) {
const unsubscribe = subscribePlayerProgress(callbacks.onPlayerProgress);
unsubscribers.push(unsubscribe);
}
// Subscribe to queue changes
if (callbacks.onPlayerQueueChange) {
const unsubscribe = subscribePlayerQueue(callbacks.onPlayerQueueChange);
unsubscribers.push(unsubscribe);
}
// Subscribe to seek events
if (callbacks.onPlayerSeekToTimestamp) {
const unsubscribe = subscribePlayerSeekToTimestamp(callbacks.onPlayerSeekToTimestamp);
unsubscribers.push(unsubscribe);
}
// Subscribe to player status changes
if (callbacks.onPlayerStatus) {
const unsubscribe = subscribePlayerStatus(callbacks.onPlayerStatus);
unsubscribers.push(unsubscribe);
}
// Subscribe to volume changes
if (callbacks.onPlayerVolume) {
const unsubscribe = subscribePlayerVolume(callbacks.onPlayerVolume);
unsubscribers.push(unsubscribe);
}
// Subscribe to mute changes
if (callbacks.onPlayerMute) {
const unsubscribe = subscribePlayerMute(callbacks.onPlayerMute);
unsubscribers.push(unsubscribe);
}
// Subscribe to speed changes
if (callbacks.onPlayerSpeed) {
const unsubscribe = subscribePlayerSpeed(callbacks.onPlayerSpeed);
unsubscribers.push(unsubscribe);
}
// Subscribe to repeat changes
if (callbacks.onPlayerRepeat) {
const unsubscribe = subscribePlayerRepeat(callbacks.onPlayerRepeat);
unsubscribers.push(unsubscribe);
}
// Subscribe to shuffle changes
if (callbacks.onPlayerShuffle) {
const unsubscribe = subscribePlayerShuffle(callbacks.onPlayerShuffle);
unsubscribers.push(unsubscribe);
}
return {
cleanup: () => {
unsubscribers.forEach((unsubscribe) => unsubscribe());
},
};
}
@@ -1,126 +0,0 @@
import { useCallback, useRef, useState } from 'react';
import { MpvPlayerEngine, MpvPlayerEngineHandle } from './engine/mpv-player-engine';
import { useMainPlayerListener } from '/@/renderer/features/player/audio-player/hooks/use-main-player-listener';
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
import { PlayerOnProgressProps } from '/@/renderer/features/player/audio-player/types';
import {
usePlayerActions,
usePlayerData,
usePlayerMuted,
usePlayerProperties,
usePlayerVolume,
} from '/@/renderer/store';
import { PlayerStatus } from '/@/shared/types/types';
const PLAY_PAUSE_FADE_DURATION = 300;
const PLAY_PAUSE_FADE_INTERVAL = 10;
export function MpvPlayer() {
const playerRef = useRef<MpvPlayerEngineHandle>(null);
const { currentSong, nextSong, status } = usePlayerData();
const { mediaAutoNext, setTimestamp } = usePlayerActions();
const { speed } = usePlayerProperties();
const isMuted = usePlayerMuted();
const volume = usePlayerVolume();
const [localPlayerStatus, setLocalPlayerStatus] = useState<PlayerStatus>(status);
const [isTransitioning, setIsTransitioning] = useState(false);
const fadeAndSetStatus = useCallback(
async (startVolume: number, endVolume: number, duration: number, status: PlayerStatus) => {
if (isTransitioning) {
return setLocalPlayerStatus(status);
}
const steps = duration / PLAY_PAUSE_FADE_INTERVAL;
const volumeStep = (endVolume - startVolume) / steps;
let currentStep = 0;
const promise = new Promise((resolve) => {
const interval = setInterval(() => {
currentStep++;
const newVolume = startVolume + volumeStep * currentStep;
playerRef.current?.setVolume(newVolume);
if (currentStep >= steps) {
clearInterval(interval);
setIsTransitioning(false);
resolve(true);
}
}, PLAY_PAUSE_FADE_INTERVAL);
});
if (status === PlayerStatus.PAUSED) {
await promise;
setLocalPlayerStatus(status);
} else if (status === PlayerStatus.PLAYING) {
setLocalPlayerStatus(status);
await promise;
}
},
[isTransitioning],
);
const onProgress = useCallback(
(e: PlayerOnProgressProps) => {
setTimestamp(Number(e.playedSeconds.toFixed(0)));
},
[setTimestamp],
);
const handleOnEnded = useCallback(() => {
// When mpv auto-advances to the next song (position 1 becomes position 0),
// we need to update the player store first, then update the mpv queue with the new next song
// This follows the same pattern as the old useCenterControls implementation
const playerData = mediaAutoNext();
// Update the mpv queue with the new next song
// The engine will handle the queue update through the useEffect when nextSong changes
playerRef.current?.setVolume(volume);
setIsTransitioning(false);
return playerData;
}, [mediaAutoNext, volume, setIsTransitioning]);
usePlayerEvents(
{
onPlayerSeekToTimestamp: (properties) => {
const timestamp = properties.timestamp;
playerRef.current?.seekTo(timestamp);
},
onPlayerStatus: async (properties) => {
const status = properties.status;
if (status === PlayerStatus.PAUSED) {
fadeAndSetStatus(volume, 0, PLAY_PAUSE_FADE_DURATION, PlayerStatus.PAUSED);
} else if (status === PlayerStatus.PLAYING) {
fadeAndSetStatus(0, volume, PLAY_PAUSE_FADE_DURATION, PlayerStatus.PLAYING);
}
},
onPlayerVolume: (properties) => {
const volume = properties.volume;
playerRef.current?.setVolume(volume);
},
},
[volume, isTransitioning, fadeAndSetStatus],
);
useMainPlayerListener();
return (
<MpvPlayerEngine
currentSrc={currentSong?.streamUrl}
isMuted={isMuted}
isTransitioning={isTransitioning}
nextSrc={nextSong?.streamUrl}
onEnded={handleOnEnded}
onProgress={onProgress}
playerRef={playerRef}
playerStatus={localPlayerStatus}
speed={speed}
volume={volume}
/>
);
}
@@ -1,13 +0,0 @@
export interface AudioPlayer {
decreaseVolume(by: number): void;
increaseVolume(by: number): void;
pause(): void;
play(): void;
seekTo(seekTo: number): void;
setVolume(volume: number): void;
}
export interface PlayerOnProgressProps {
played: number;
playedSeconds: number;
}
@@ -1,3 +0,0 @@
export const convertToLogVolume = (linearVolume: number) => {
return Math.pow(linearVolume, 2.0);
};
@@ -1,330 +0,0 @@
import type { Dispatch } from 'react';
import type ReactPlayer from 'react-player';
import { useCallback, useRef, useState } from 'react';
import {
WebPlayerEngine,
WebPlayerEngineHandle,
} from '/@/renderer/features/player/audio-player/engine/web-player-engine';
import { useMainPlayerListener } from '/@/renderer/features/player/audio-player/hooks/use-main-player-listener';
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
import { PlayerOnProgressProps } from '/@/renderer/features/player/audio-player/types';
import {
usePlayerActions,
usePlayerData,
usePlayerMuted,
usePlayerProperties,
usePlayerVolume,
} from '/@/renderer/store';
import { PlayerStatus, PlayerStyle } from '/@/shared/types/types';
const PLAY_PAUSE_FADE_DURATION = 300;
const PLAY_PAUSE_FADE_INTERVAL = 10;
export function WebPlayer() {
const playerRef = useRef<WebPlayerEngineHandle>(null);
const { num, player1, player2, status } = usePlayerData();
const { mediaAutoNext, setTimestamp } = usePlayerActions();
const { crossfadeDuration, speed, transitionType } = usePlayerProperties();
const isMuted = usePlayerMuted();
const volume = usePlayerVolume();
const [localPlayerStatus, setLocalPlayerStatus] = useState<PlayerStatus>(status);
const [isTransitioning, setIsTransitioning] = useState<boolean | string>(false);
const fadeAndSetStatus = useCallback(
async (startVolume: number, endVolume: number, duration: number, status: PlayerStatus) => {
if (isTransitioning) {
return setLocalPlayerStatus(status);
}
const steps = duration / PLAY_PAUSE_FADE_INTERVAL;
const volumeStep = (endVolume - startVolume) / steps;
let currentStep = 0;
const promise = new Promise((resolve) => {
const interval = setInterval(() => {
currentStep++;
const newVolume = startVolume + volumeStep * currentStep;
playerRef.current?.setVolume(newVolume);
if (currentStep >= steps) {
clearInterval(interval);
setIsTransitioning(false);
resolve(true);
}
}, PLAY_PAUSE_FADE_INTERVAL);
});
if (status === PlayerStatus.PAUSED) {
await promise;
setLocalPlayerStatus(status);
} else if (status === PlayerStatus.PLAYING) {
setLocalPlayerStatus(status);
await promise;
}
},
[isTransitioning],
);
const onProgressPlayer1 = useCallback(
(e: PlayerOnProgressProps) => {
if (transitionType === 'crossfade' && num === 1) {
setTimestamp(Number(e.playedSeconds.toFixed(0)));
} else if (transitionType === 'gapless') {
setTimestamp(Number(e.playedSeconds.toFixed(0)));
}
if (!playerRef.current?.player1()) {
return;
}
switch (transitionType) {
case PlayerStyle.CROSSFADE:
crossfadeHandler({
crossfadeDuration: crossfadeDuration,
currentPlayer: playerRef.current.player1(),
currentPlayerNum: num,
currentTime: e.playedSeconds,
duration: getDuration(playerRef.current.player1().ref),
isTransitioning,
nextPlayer: playerRef.current.player2(),
playerNum: 1,
setIsTransitioning,
volume,
});
break;
case PlayerStyle.GAPLESS:
gaplessHandler({
currentTime: e.playedSeconds,
duration: getDuration(playerRef.current.player1().ref),
isFlac: false,
isTransitioning,
nextPlayer: playerRef.current.player2(),
setIsTransitioning,
});
break;
}
},
[crossfadeDuration, isTransitioning, num, setTimestamp, transitionType, volume],
);
const onProgressPlayer2 = useCallback(
(e: PlayerOnProgressProps) => {
if (transitionType === PlayerStyle.CROSSFADE && num === 2) {
setTimestamp(Number(e.playedSeconds.toFixed(0)));
} else if (transitionType === PlayerStyle.GAPLESS) {
setTimestamp(Number(e.playedSeconds.toFixed(0)));
}
if (!playerRef.current?.player2()) {
return;
}
switch (transitionType) {
case PlayerStyle.CROSSFADE:
crossfadeHandler({
crossfadeDuration: crossfadeDuration,
currentPlayer: playerRef.current.player2(),
currentPlayerNum: num,
currentTime: e.playedSeconds,
duration: getDuration(playerRef.current.player2().ref),
isTransitioning,
nextPlayer: playerRef.current.player1(),
playerNum: 2,
setIsTransitioning,
volume,
});
break;
case PlayerStyle.GAPLESS:
gaplessHandler({
currentTime: e.playedSeconds,
duration: getDuration(playerRef.current.player2().ref),
isFlac: false,
isTransitioning,
nextPlayer: playerRef.current.player1(),
setIsTransitioning,
});
break;
}
},
[crossfadeDuration, isTransitioning, num, setTimestamp, transitionType, volume],
);
const handleOnEndedPlayer1 = useCallback(() => {
const promise = new Promise((resolve) => {
mediaAutoNext();
resolve(true);
});
promise.then(() => {
playerRef.current?.player1()?.ref?.getInternalPlayer().pause();
playerRef.current?.setVolume(volume);
setIsTransitioning(false);
});
}, [mediaAutoNext, volume]);
const handleOnEndedPlayer2 = useCallback(() => {
const promise = new Promise((resolve) => {
mediaAutoNext();
resolve(true);
});
promise.then(() => {
playerRef.current?.player2()?.ref?.getInternalPlayer().pause();
playerRef.current?.setVolume(volume);
setIsTransitioning(false);
});
}, [mediaAutoNext, volume]);
usePlayerEvents(
{
onPlayerSeekToTimestamp: (properties) => {
const timestamp = properties.timestamp;
if (num === 1) {
playerRef.current?.player1()?.ref?.seekTo(timestamp);
} else {
playerRef.current?.player2()?.ref?.seekTo(timestamp);
}
},
onPlayerStatus: async (properties) => {
const status = properties.status;
if (status === PlayerStatus.PAUSED) {
fadeAndSetStatus(volume, 0, PLAY_PAUSE_FADE_DURATION, PlayerStatus.PAUSED);
} else if (status === PlayerStatus.PLAYING) {
fadeAndSetStatus(0, volume, PLAY_PAUSE_FADE_DURATION, PlayerStatus.PLAYING);
}
},
onPlayerVolume: (properties) => {
const volume = properties.volume;
playerRef.current?.setVolume(volume);
},
},
[volume, num, isTransitioning],
);
useMainPlayerListener();
return (
<WebPlayerEngine
isMuted={isMuted}
isTransitioning={Boolean(isTransitioning)}
onEndedPlayer1={handleOnEndedPlayer1}
onEndedPlayer2={handleOnEndedPlayer2}
onProgressPlayer1={onProgressPlayer1}
onProgressPlayer2={onProgressPlayer2}
playerNum={num}
playerRef={playerRef}
playerStatus={localPlayerStatus}
speed={speed}
src1={player1?.streamUrl}
src2={player2?.streamUrl}
volume={volume}
/>
);
}
function crossfadeHandler(args: {
crossfadeDuration: number;
currentPlayer: {
ref: null | ReactPlayer;
setVolume: (volume: number) => void;
};
currentPlayerNum: number;
currentTime: number;
duration: number;
isTransitioning: boolean | string;
nextPlayer: {
ref: null | ReactPlayer;
setVolume: (volume: number) => void;
};
playerNum: number;
setIsTransitioning: Dispatch<boolean | string>;
volume: number;
}) {
const {
crossfadeDuration,
currentPlayer,
currentPlayerNum,
currentTime,
duration,
isTransitioning,
nextPlayer,
playerNum,
setIsTransitioning,
volume,
} = args;
const player = `player${playerNum}`;
if (!isTransitioning) {
if (currentTime > duration - crossfadeDuration) {
nextPlayer.setVolume(0);
nextPlayer.ref?.getInternalPlayer().play();
return setIsTransitioning(player);
}
return;
}
if (isTransitioning !== player && currentPlayerNum !== playerNum) {
return;
}
const timeLeft = duration - currentTime;
// Calculate the volume levels based on time remaining
const currentPlayerVolume = (timeLeft / crossfadeDuration) * volume;
const nextPlayerVolume = ((crossfadeDuration - timeLeft) / crossfadeDuration) * volume;
// Set volumes for both players
currentPlayer.setVolume(currentPlayerVolume);
nextPlayer.setVolume(nextPlayerVolume);
}
function gaplessHandler(args: {
currentTime: number;
duration: number;
isFlac: boolean;
isTransitioning: boolean | string;
nextPlayer: {
ref: null | ReactPlayer;
setVolume: (volume: number) => void;
};
setIsTransitioning: Dispatch<boolean | string>;
}) {
const { currentTime, duration, isFlac, isTransitioning, nextPlayer, setIsTransitioning } = args;
if (!isTransitioning) {
if (currentTime > duration - 2) {
return setIsTransitioning(true);
}
return null;
}
const durationPadding = getDurationPadding(isFlac);
if (currentTime + durationPadding >= duration) {
return nextPlayer.ref
?.getInternalPlayer()
?.play()
.catch(() => {});
}
return null;
}
function getDuration(ref: null | ReactPlayer | undefined) {
return ref?.getInternalPlayer()?.duration || 0;
}
function getDurationPadding(isFlac: boolean) {
switch (isFlac) {
case false:
return 0.116;
case true:
return 0.065;
}
}
@@ -4,6 +4,31 @@
align-items: center;
}
.slider-container {
display: flex;
width: 95%;
height: 20px;
}
.slider-value-wrapper {
display: flex;
flex: 1;
align-self: center;
justify-content: center;
max-width: 50px;
@media (width < 768px) {
display: none;
}
}
.slider-wrapper {
display: flex;
flex: 6;
align-items: center;
height: 100%;
}
.controls-container {
display: flex;
align-items: center;
@@ -1,4 +1,8 @@
import { useHotkeys } from '@mantine/hooks';
import { useQueryClient } from '@tanstack/react-query';
import formatDuration from 'format-duration';
import isElectron from 'is-electron';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import styles from './center-controls.module.css';
@@ -6,48 +10,129 @@ import styles from './center-controls.module.css';
import { PlayButton, PlayerButton } from '/@/renderer/features/player/components/player-button';
import { PlayerbarSlider } from '/@/renderer/features/player/components/playerbar-slider';
import { openShuffleAllModal } from '/@/renderer/features/player/components/shuffle-all-modal';
import { usePlayerContext } from '/@/renderer/features/player/context/player-context';
import { useCenterControls } from '/@/renderer/features/player/hooks/use-center-controls';
import { useMediaSession } from '/@/renderer/features/player/hooks/use-media-session';
import { usePlayQueueAdd } from '/@/renderer/features/player/hooks/use-playqueue-add';
import {
usePlayerRepeat,
usePlayerShuffle,
usePlayerSong,
usePlayerStatus,
useAppStore,
useAppStoreActions,
useCurrentPlayer,
useCurrentSong,
useCurrentStatus,
useCurrentTime,
useHotkeySettings,
usePlaybackType,
useRepeatStatus,
useSetCurrentTime,
useSettingsStore,
useShuffleStatus,
} from '/@/renderer/store';
import { Icon } from '/@/shared/components/icon/icon';
import { PlayerRepeat, PlayerShuffle, PlayerStatus } from '/@/shared/types/types';
import { Text } from '/@/shared/components/text/text';
import { PlaybackSelectors } from '/@/shared/constants/playback-selectors';
import { PlaybackType, PlayerRepeat, PlayerShuffle, PlayerStatus } from '/@/shared/types/types';
export const CenterControls = () => {
interface CenterControlsProps {
playersRef: any;
}
export const CenterControls = ({ playersRef }: CenterControlsProps) => {
const { t } = useTranslation();
const queryClient = useQueryClient();
const currentSong = usePlayerSong();
const [isSeeking, setIsSeeking] = useState(false);
const currentSong = useCurrentSong();
const skip = useSettingsStore((state) => state.general.skipButtons);
const buttonSize = useSettingsStore((state) => state.general.buttonSize);
const status = usePlayerStatus();
const repeat = usePlayerRepeat();
const shuffle = usePlayerShuffle();
const playbackType = usePlaybackType();
const player1 = playersRef?.current?.player1;
const player2 = playersRef?.current?.player2;
const status = useCurrentStatus();
const player = useCurrentPlayer();
const setCurrentTime = useSetCurrentTime();
const repeat = useRepeatStatus();
const shuffle = useShuffleStatus();
const { bindings } = useHotkeySettings();
const { showTimeRemaining } = useAppStore();
const { setShowTimeRemaining } = useAppStoreActions();
const {
mediaNext,
mediaPrevious,
mediaSkipBackward,
mediaSkipForward,
mediaStop,
mediaTogglePlayPause,
toggleRepeat,
toggleShuffle,
} = usePlayerContext();
handleNextTrack,
handlePause,
handlePlay,
handlePlayPause,
handlePrevTrack,
handleSeekSlider,
handleSkipBackward,
handleSkipForward,
handleStop,
handleToggleRepeat,
handleToggleShuffle,
} = useCenterControls({ playersRef });
const handlePlayQueueAdd = usePlayQueueAdd();
const songDuration = currentSong?.duration ? currentSong.duration / 1000 : 0;
const currentTime = useCurrentTime();
const currentPlayerRef = player === 1 ? player1 : player2;
const formattedDuration = formatDuration(songDuration * 1000 || 0);
const formattedTimeRemaining = formatDuration((currentTime - songDuration) * 1000 || 0);
const formattedTime = formatDuration(currentTime * 1000 || 0);
useEffect(() => {
let interval: ReturnType<typeof setInterval>;
if (status === PlayerStatus.PLAYING && !isSeeking) {
if (!isElectron() || playbackType === PlaybackType.WEB) {
// Update twice a second for slightly better performance
interval = setInterval(() => {
if (currentPlayerRef) {
setCurrentTime(currentPlayerRef.getCurrentTime());
}
}, 500);
}
}
return () => clearInterval(interval);
}, [currentPlayerRef, isSeeking, setCurrentTime, playbackType, status]);
const [seekValue, setSeekValue] = useState(0);
useHotkeys([
[bindings.playPause.isGlobal ? '' : bindings.playPause.hotkey, handlePlayPause],
[bindings.play.isGlobal ? '' : bindings.play.hotkey, handlePlay],
[bindings.pause.isGlobal ? '' : bindings.pause.hotkey, handlePause],
[bindings.stop.isGlobal ? '' : bindings.stop.hotkey, handleStop],
[bindings.next.isGlobal ? '' : bindings.next.hotkey, handleNextTrack],
[bindings.previous.isGlobal ? '' : bindings.previous.hotkey, handlePrevTrack],
[bindings.toggleRepeat.isGlobal ? '' : bindings.toggleRepeat.hotkey, handleToggleRepeat],
[bindings.toggleShuffle.isGlobal ? '' : bindings.toggleShuffle.hotkey, handleToggleShuffle],
[
bindings.skipBackward.isGlobal ? '' : bindings.skipBackward.hotkey,
() => handleSkipBackward(skip?.skipBackwardSeconds || 5),
],
[
bindings.skipForward.isGlobal ? '' : bindings.skipForward.hotkey,
() => handleSkipForward(skip?.skipForwardSeconds || 5),
],
]);
useMediaSession({
handleNextTrack,
handlePause,
handlePlay,
handlePrevTrack,
handleSeekSlider,
handleSkipBackward,
handleSkipForward,
handleStop,
});
return (
<>
<div className={styles.controlsContainer}>
<div className={styles.buttonsContainer}>
<PlayerButton
icon={<Icon fill="default" icon="mediaStop" size={buttonSize - 2} />}
onClick={mediaStop}
onClick={handleStop}
tooltip={{
label: t('player.stop', { postProcess: 'sentenceCase' }),
openDelay: 0,
@@ -63,7 +148,7 @@ export const CenterControls = () => {
/>
}
isActive={shuffle !== PlayerShuffle.NONE}
onClick={toggleShuffle}
onClick={handleToggleShuffle}
tooltip={{
label:
shuffle === PlayerShuffle.NONE
@@ -78,7 +163,7 @@ export const CenterControls = () => {
/>
<PlayerButton
icon={<Icon fill="default" icon="mediaPrevious" size={buttonSize} />}
onClick={mediaPrevious}
onClick={handlePrevTrack}
tooltip={{
label: t('player.previous', { postProcess: 'sentenceCase' }),
openDelay: 0,
@@ -90,7 +175,7 @@ export const CenterControls = () => {
icon={
<Icon fill="default" icon="mediaStepBackward" size={buttonSize} />
}
onClick={mediaSkipBackward}
onClick={() => handleSkipBackward(skip?.skipBackwardSeconds)}
tooltip={{
label: t('player.skip', {
context: 'back',
@@ -105,12 +190,12 @@ export const CenterControls = () => {
<PlayButton
disabled={currentSong?.id === undefined}
isPaused={status === PlayerStatus.PAUSED}
onClick={mediaTogglePlayPause}
onClick={handlePlayPause}
/>
{skip?.enabled && (
<PlayerButton
icon={<Icon fill="default" icon="mediaStepForward" size={buttonSize} />}
onClick={mediaSkipForward}
onClick={() => handleSkipForward(skip?.skipForwardSeconds)}
tooltip={{
label: t('player.skip', {
context: 'forward',
@@ -124,7 +209,7 @@ export const CenterControls = () => {
)}
<PlayerButton
icon={<Icon fill="default" icon="mediaNext" size={buttonSize} />}
onClick={mediaNext}
onClick={handleNextTrack}
tooltip={{
label: t('player.next', { postProcess: 'sentenceCase' }),
openDelay: 0,
@@ -144,7 +229,7 @@ export const CenterControls = () => {
)
}
isActive={repeat !== PlayerRepeat.NONE}
onClick={toggleRepeat}
onClick={handleToggleRepeat}
tooltip={{
label: `${
repeat === PlayerRepeat.NONE
@@ -182,7 +267,57 @@ export const CenterControls = () => {
/>
</div>
</div>
<PlayerbarSlider />
<div className={styles.sliderContainer}>
<div className={styles.sliderValueWrapper}>
<Text
className={PlaybackSelectors.elapsedTime}
fw={600}
isMuted
isNoSelect
size="xs"
style={{ userSelect: 'none' }}
>
{formattedTime}
</Text>
</div>
<div className={styles.sliderWrapper}>
<PlayerbarSlider
label={(value) => formatDuration(value * 1000)}
max={songDuration}
min={0}
onChange={(e) => {
setIsSeeking(true);
setSeekValue(e);
}}
onChangeEnd={(e) => {
// There is a timing bug in Mantine in which the onChangeEnd
// event fires before onChange. Add a small delay to force
// onChangeEnd to happen after onCHange
setTimeout(() => {
handleSeekSlider(e);
setIsSeeking(false);
}, 50);
}}
size={6}
value={!isSeeking ? currentTime : seekValue}
w="100%"
/>
</div>
<div className={styles.sliderValueWrapper}>
<Text
className={PlaybackSelectors.totalDuration}
fw={600}
isMuted
isNoSelect
onClick={() => setShowTimeRemaining(!showTimeRemaining)}
role="button"
size="xs"
style={{ cursor: 'pointer', userSelect: 'none' }}
>
{showTimeRemaining ? formattedTimeRemaining : formattedDuration}
</Text>
</div>
</div>
</>
);
};
@@ -15,7 +15,7 @@ import {
} from '/@/renderer/store/full-screen-player.store';
import { Button } from '/@/shared/components/button/button';
import { Group } from '/@/shared/components/group/group';
import { PlayerType } from '/@/shared/types/types';
import { PlaybackType } from '/@/shared/types/types';
const Visualizer = lazy(() =>
import('/@/renderer/features/player/components/visualizer').then((module) => ({
@@ -48,7 +48,7 @@ export const FullScreenPlayerQueue = () => {
},
];
if (type === PlayerType.WEB && webAudio) {
if (type === PlaybackType.WEB && webAudio) {
items.push({
active: activeTab === 'visualizer',
label: t('page.fullscreenPlayer.visualizer', { postProcess: 'titleCase' }),
@@ -107,7 +107,7 @@ export const FullScreenPlayerQueue = () => {
</div>
) : activeTab === 'lyrics' ? (
<Lyrics />
) : activeTab === 'visualizer' && type === PlayerType.WEB && webAudio ? (
) : activeTab === 'visualizer' && type === PlaybackType.WEB && webAudio ? (
<Suspense fallback={<></>}>
<Visualizer />
</Suspense>
@@ -11,7 +11,7 @@ import { FullScreenPlayerImage } from '/@/renderer/features/player/components/fu
import { FullScreenPlayerQueue } from '/@/renderer/features/player/components/full-screen-player-queue';
import { useFastAverageColor } from '/@/renderer/hooks';
import {
usePlayerSong,
useCurrentSong,
useFullScreenPlayerStore,
useFullScreenPlayerStoreActions,
useLyricsSettings,
@@ -421,7 +421,7 @@ export const FullScreenPlayer = () => {
isOpenedRef.current = true;
}, [location, setStore]);
const currentSong = usePlayerSong();
const currentSong = useCurrentSong();
const { background } = useFastAverageColor({
algorithm: 'dominant',
src: currentSong?.imageUrl,
@@ -1,8 +1,8 @@
import { SimilarSongsList } from '/@/renderer/features/similar-songs/components/similar-songs-list';
import { usePlayerSong } from '/@/renderer/store';
import { useCurrentSong } from '/@/renderer/store';
export const FullScreenSimilarSongs = () => {
const currentSong = usePlayerSong();
const currentSong = useCurrentSong();
return currentSong?.id ? <SimilarSongsList fullScreen song={currentSong} /> : null;
};
@@ -12,9 +12,9 @@ import { useHandleGeneralContextMenu } from '/@/renderer/features/context-menu/h
import { AppRoute } from '/@/renderer/router/routes';
import {
useAppStoreActions,
useCurrentSong,
useFullScreenPlayerStore,
useHotkeySettings,
usePlayerSong,
useSetFullScreenPlayerStore,
useSidebarStore,
} from '/@/renderer/store';
@@ -34,7 +34,7 @@ export const LeftControls = () => {
const setFullScreenPlayerStore = useSetFullScreenPlayerStore();
const { collapsed, image } = useSidebarStore();
const hideImage = image && !collapsed;
const currentSong = usePlayerSong();
const currentSong = useCurrentSong();
const title = currentSong?.name;
const artists = currentSong?.artists;
const { bindings } = useHotkeySettings();
@@ -49,28 +49,3 @@
right: calc(0.1rem * -1);
}
}
.slider-container {
display: flex;
width: 95%;
height: 20px;
}
.slider-value-wrapper {
display: flex;
flex: 1;
align-self: center;
justify-content: center;
max-width: 50px;
@media (width < 768px) {
display: none;
}
}
.slider-wrapper {
display: flex;
flex: 6;
align-items: center;
height: 100%;
}
@@ -1,128 +1,8 @@
import formatDuration from 'format-duration';
import { useEffect, useRef, useState } from 'react';
import styles from './playerbar-slider.module.css';
import { MpvPlayer } from '/@/renderer/features/player/audio-player/mpv-player';
import { WebPlayer } from '/@/renderer/features/player/audio-player/web-player';
import { usePlayerContext } from '/@/renderer/features/player/context/player-context';
import { useRemote } from '/@/renderer/features/remote/hooks/use-remote';
import {
useAppStore,
useAppStoreActions,
usePlaybackType,
usePlayerSong,
usePlayerTimestamp,
} from '/@/renderer/store';
import { Slider, SliderProps } from '/@/shared/components/slider/slider';
import { Text } from '/@/shared/components/text/text';
import { PlaybackSelectors } from '/@/shared/constants/playback-selectors';
import { PlayerType } from '/@/shared/types/types';
export const PlayerbarSlider = ({ ...props }: SliderProps) => {
const playbackType = usePlaybackType();
const currentSong = usePlayerSong();
const songDuration = currentSong?.duration ? currentSong.duration / 1000 : 0;
const [isSeeking, setIsSeeking] = useState(false);
const [seekValue, setSeekValue] = useState(0);
const currentTime = usePlayerTimestamp();
const seekTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const formattedDuration = formatDuration(songDuration * 1000 || 0);
const formattedTimeRemaining = formatDuration((currentTime - songDuration) * 1000 || 0);
const formattedTime = formatDuration(currentTime * 1000 || 0);
const { showTimeRemaining } = useAppStore();
const { setShowTimeRemaining } = useAppStoreActions();
const { mediaSeekToTimestamp } = usePlayerContext();
const handleSeekToTimestamp = (timestamp: number) => {
mediaSeekToTimestamp(timestamp);
};
useEffect(() => {
return () => {
if (seekTimeoutRef.current) {
clearTimeout(seekTimeoutRef.current);
}
};
}, []);
useRemote();
return (
<>
<div className={styles.sliderContainer}>
<div className={styles.sliderValueWrapper}>
<Text
className={PlaybackSelectors.elapsedTime}
fw={600}
isMuted
isNoSelect
size="xs"
style={{ userSelect: 'none' }}
>
{formattedTime}
</Text>
</div>
<div className={styles.sliderWrapper}>
<CustomPlayerbarSlider
{...props}
label={(value) => formatDuration(value * 1000)}
max={songDuration}
min={0}
onChange={(e) => {
// Cancel any pending timeout if user starts seeking again
if (seekTimeoutRef.current) {
clearTimeout(seekTimeoutRef.current);
seekTimeoutRef.current = null;
}
setIsSeeking(true);
setSeekValue(e);
}}
onChangeEnd={(e) => {
setSeekValue(e);
handleSeekToTimestamp(e);
// Delay resetting isSeeking to allow currentTime to catch up
// This prevents the slider from flickering back and forth
seekTimeoutRef.current = setTimeout(() => {
setIsSeeking(false);
seekTimeoutRef.current = null;
}, 300);
}}
onClick={(e) => {
e?.stopPropagation();
}}
size={6}
value={!isSeeking ? currentTime : seekValue}
w="100%"
/>
</div>
<div className={styles.sliderValueWrapper}>
<Text
className={PlaybackSelectors.totalDuration}
fw={600}
isMuted
isNoSelect
onClick={() => setShowTimeRemaining(!showTimeRemaining)}
role="button"
size="xs"
style={{ cursor: 'pointer', userSelect: 'none' }}
>
{showTimeRemaining ? formattedTimeRemaining : formattedDuration}
</Text>
</div>
</div>
{playbackType === PlayerType.WEB && <WebPlayer />}
{playbackType === PlayerType.LOCAL && <MpvPlayer />}
</>
);
};
export const CustomPlayerbarSlider = ({ ...props }: SliderProps) => {
return (
<Slider
classNames={{
@@ -133,7 +13,9 @@ export const CustomPlayerbarSlider = ({ ...props }: SliderProps) => {
track: styles.track,
}}
{...props}
size={6}
onClick={(e) => {
e?.stopPropagation();
}}
/>
);
};
@@ -1,18 +1,46 @@
import clsx from 'clsx';
import { MouseEvent } from 'react';
import { MouseEvent, useCallback } from 'react';
import styles from './playerbar.module.css';
import { AudioPlayer } from '/@/renderer/components/audio-player';
import { CenterControls } from '/@/renderer/features/player/components/center-controls';
import { LeftControls } from '/@/renderer/features/player/components/left-controls';
import { RightControls } from '/@/renderer/features/player/components/right-controls';
import { usePowerSaveBlocker } from '/@/renderer/features/player/hooks/use-power-save-blocker';
import { useFullScreenPlayerStore, useSetFullScreenPlayerStore } from '/@/renderer/store';
import { useGeneralSettings } from '/@/renderer/store/settings.store';
import { PlayersRef } from '/@/renderer/features/player/ref/players-ref';
import { updateSong } from '/@/renderer/features/player/update-remote-song';
import {
useCurrentPlayer,
useCurrentStatus,
useFullScreenPlayerStore,
useMuted,
usePlayer1Data,
usePlayer2Data,
usePlayerControls,
useSetFullScreenPlayerStore,
useVolume,
} from '/@/renderer/store';
import {
useGeneralSettings,
usePlaybackType,
useSettingsStore,
} from '/@/renderer/store/settings.store';
import { PlaybackSelectors } from '/@/shared/constants/playback-selectors';
import { PlaybackType } from '/@/shared/types/types';
export const Playerbar = () => {
const playersRef = PlayersRef;
const settings = useSettingsStore((state) => state.playback);
const { playerbarOpenDrawer } = useGeneralSettings();
const playbackType = usePlaybackType();
const volume = useVolume();
const player1 = usePlayer1Data();
const player2 = usePlayer2Data();
const status = useCurrentStatus();
const player = useCurrentPlayer();
const muted = useMuted();
const { autoNext } = usePlayerControls();
const { expanded: isFullScreenPlayerExpanded } = useFullScreenPlayerStore();
const setFullScreenPlayerStore = useSetFullScreenPlayerStore();
@@ -23,6 +51,11 @@ export const Playerbar = () => {
setFullScreenPlayerStore({ expanded: !isFullScreenPlayerExpanded });
};
const autoNextFn = useCallback(() => {
const playerData = autoNext();
updateSong(playerData.current.song);
}, [autoNext]);
return (
<div
className={clsx(styles.container, PlaybackSelectors.mediaPlayer)}
@@ -33,15 +66,15 @@ export const Playerbar = () => {
<LeftControls />
</div>
<div className={styles.centerGridItem}>
<CenterControls />
<CenterControls playersRef={playersRef} />
</div>
<div className={styles.rightGridItem}>
<RightControls />
</div>
</div>
{/* {playbackType === PlayerType.WEB && (
{playbackType === PlaybackType.WEB && (
<AudioPlayer
// autoNext={autoNextFn}
autoNext={autoNextFn}
crossfadeDuration={settings.crossfadeDuration}
crossfadeStyle={settings.crossfadeStyle}
currentPlayer={player}
@@ -54,7 +87,7 @@ export const Playerbar = () => {
style={settings.style as any}
volume={(volume / 100) ** 2}
/>
)} */}
)}
</div>
);
};
@@ -1,27 +1,27 @@
import { useHotkeys, useMediaQuery } from '@mantine/hooks';
import isElectron from 'is-electron';
import { useCallback, useEffect, WheelEvent } from 'react';
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { CustomPlayerbarSlider } from '/@/renderer/features/player/components/playerbar-slider';
import { usePlayerContext } from '/@/renderer/features/player/context/player-context';
import { PlayerbarSlider } from '/@/renderer/features/player/components/playerbar-slider';
import { useRightControls } from '/@/renderer/features/player/hooks/use-right-controls';
import { useCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
import { useSetRating } from '/@/renderer/features/shared/mutations/set-rating-mutation';
import {
useAppStoreActions,
useCurrentServer,
useGeneralSettings,
useCurrentSong,
useHotkeySettings,
useMuted,
usePlaybackSettings,
usePlaybackType,
usePlayerData,
usePlayerMuted,
usePlayerSpeed,
usePlayerVolume,
usePreviousSong,
useSettingsStore,
useSettingsStoreActions,
useSidebarStore,
useSpeed,
useVolume,
} from '/@/renderer/store';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu';
@@ -32,53 +32,38 @@ import { Rating } from '/@/shared/components/rating/rating';
import { Slider } from '/@/shared/components/slider/slider';
import { Switch } from '/@/shared/components/switch/switch';
import { LibraryItem, QueueSong, ServerType, Song } from '/@/shared/types/domain-types';
import { PlayerType } from '/@/shared/types/types';
import { PlaybackType } from '/@/shared/types/types';
const ipc = isElectron() ? window.api.ipc : null;
const remote = isElectron() ? window.api.remote : null;
const calculateVolumeUp = (volume: number, volumeWheelStep: number) => {
let volumeToSet;
const newVolumeGreaterThanHundred = volume + volumeWheelStep > 100;
if (newVolumeGreaterThanHundred) {
volumeToSet = 100;
} else {
volumeToSet = volume + volumeWheelStep;
}
return volumeToSet;
};
const calculateVolumeDown = (volume: number, volumeWheelStep: number) => {
let volumeToSet;
const newVolumeLessThanZero = volume - volumeWheelStep < 0;
if (newVolumeLessThanZero) {
volumeToSet = 0;
} else {
volumeToSet = volume - volumeWheelStep;
}
return volumeToSet;
};
export const RightControls = () => {
const { t } = useTranslation();
const isMinWidth = useMediaQuery('(max-width: 480px)');
const volume = usePlayerVolume();
const muted = usePlayerMuted();
const volume = useVolume();
const muted = useMuted();
const server = useCurrentServer();
const { currentSong, previousSong } = usePlayerData();
const currentSong = useCurrentSong();
const previousSong = usePreviousSong();
const { setSideBar } = useAppStoreActions();
const { rightExpanded: isQueueExpanded } = useSidebarStore();
const { bindings } = useHotkeySettings();
const {
handleMute,
handleSpeed,
handleVolumeDown,
handleVolumeSlider,
handleVolumeUp,
handleVolumeWheel,
} = useRightControls();
const { setSettings } = useSettingsStoreActions();
const playbackSettings = usePlaybackSettings();
const playbackType = usePlaybackType();
const { volumeWheelStep } = useGeneralSettings();
const speed = usePlayerSpeed();
const speed = useSpeed();
const volumeWidth = useSettingsStore((state) => state.general.volumeWidth);
const speedPreservePitch = useSettingsStore((state) => state.playback.preservePitch);
const { mediaToggleMute, setSpeed, setVolume } = usePlayerContext();
const updateRatingMutation = useSetRating({});
const addToFavoritesMutation = useCreateFavorite({});
const removeFromFavoritesMutation = useDeleteFavorite({});
@@ -87,7 +72,7 @@ export const RightControls = () => {
if (!song?.id) return;
addToFavoritesMutation.mutate({
apiClientProps: { serverId: song?._serverId || '' },
apiClientProps: { serverId: song?.serverId || '' },
query: {
id: [song.id],
type: LibraryItem.SONG,
@@ -99,7 +84,7 @@ export const RightControls = () => {
if (!currentSong) return;
updateRatingMutation.mutate({
apiClientProps: { serverId: currentSong?._serverId || '' },
apiClientProps: { serverId: currentSong?.serverId || '' },
query: {
item: [currentSong],
rating,
@@ -111,7 +96,7 @@ export const RightControls = () => {
if (!song?.id) return;
removeFromFavoritesMutation.mutate({
apiClientProps: { serverId: song?._serverId || '' },
apiClientProps: { serverId: song?.serverId || '' },
query: {
id: [song.id],
type: LibraryItem.SONG,
@@ -129,46 +114,6 @@ export const RightControls = () => {
}
};
const handleVolumeDown = useCallback(() => {
setVolume(volume - 1);
}, [setVolume, volume]);
const handleVolumeUp = useCallback(() => {
setVolume(volume + 1);
}, [setVolume, volume]);
const handleMute = useCallback(() => {
mediaToggleMute();
}, [mediaToggleMute]);
const handleSpeed = useCallback(
(e: number) => {
setSpeed(e);
},
[setSpeed],
);
const handleVolumeSlider = useCallback(
(e: number) => {
setVolume(e);
},
[setVolume],
);
const handleVolumeWheel = useCallback(
(e: WheelEvent<HTMLButtonElement | HTMLDivElement>) => {
let volumeToSet;
if (e.deltaY > 0 || e.deltaX > 0) {
volumeToSet = calculateVolumeDown(volume, volumeWheelStep);
} else {
volumeToSet = calculateVolumeUp(volume, volumeWheelStep);
}
setVolume(volumeToSet);
},
[setVolume, volume, volumeWheelStep],
);
const handleToggleQueue = () => {
setSideBar({ rightExpanded: !isQueueExpanded });
};
@@ -242,9 +187,9 @@ export const RightControls = () => {
query: {
item: [
{
_serverId: currentSong?._serverId || '',
id,
itemType: LibraryItem.SONG,
serverId,
} as Song, // This is not a type-safe cast, but it works because those are all the prop
],
rating,
@@ -259,12 +204,7 @@ export const RightControls = () => {
}
return () => {};
}, [
addToFavoritesMutation,
currentSong?._serverId,
removeFromFavoritesMutation,
updateRatingMutation,
]);
}, [addToFavoritesMutation, removeFromFavoritesMutation, updateRatingMutation]);
return (
<Flex align="flex-end" direction="column" h="100%" px="1rem" py="0.5rem">
@@ -297,7 +237,7 @@ export const RightControls = () => {
/>
</DropdownMenu.Target>
<DropdownMenu.Dropdown>
{playbackType === PlayerType.WEB && (
{playbackType === PlaybackType.WEB && (
<Option>
<Option.Label>
{t('setting.preservePitch', {
@@ -399,7 +339,7 @@ export const RightControls = () => {
variant="subtle"
/>
{!isMinWidth ? (
<CustomPlayerbarSlider
<PlayerbarSlider
max={100}
min={0}
onChange={handleVolumeSlider}
@@ -1,467 +0,0 @@
import { QueryClient, useQueryClient } from '@tanstack/react-query';
import { createContext, useCallback, useContext, useMemo } from 'react';
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
import { AddToQueueType, usePlayerActions } from '/@/renderer/store';
import {
LibraryItem,
PlaylistSongListResponse,
QueueSong,
Song,
SongListResponse,
SongListSort,
SortOrder,
} from '/@/shared/types/domain-types';
import { Play, PlayerRepeat, PlayerShuffle } from '/@/shared/types/types';
interface PlayerContext {
addToQueueByData: (data: Song[], type: AddToQueueType) => void;
addToQueueByFetch: (
serverId: string,
id: string[],
itemType: LibraryItem,
type: AddToQueueType,
) => void;
clearQueue: () => void;
clearSelected: (items: QueueSong[]) => void;
decreaseVolume: (amount: number) => void;
increaseVolume: (amount: number) => void;
mediaNext: () => void;
mediaPause: () => void;
mediaPlay: (id?: string) => void;
mediaPrevious: () => void;
mediaSeekToTimestamp: (timestamp: number) => void;
mediaSkipBackward: () => void;
mediaSkipForward: () => void;
mediaStop: () => void;
mediaToggleMute: () => void;
mediaTogglePlayPause: () => void;
moveSelectedTo: (items: QueueSong[], edge: 'bottom' | 'top', uniqueId: string) => void;
moveSelectedToBottom: (items: QueueSong[]) => void;
moveSelectedToNext: (items: QueueSong[]) => void;
moveSelectedToTop: (items: QueueSong[]) => void;
setRepeat: (repeat: PlayerRepeat) => void;
setShuffle: (shuffle: PlayerShuffle) => void;
setSpeed: (speed: number) => void;
setVolume: (volume: number) => void;
shuffle: () => void;
shuffleAll: () => void;
shuffleSelected: (items: QueueSong[]) => void;
toggleRepeat: () => void;
toggleShuffle: () => void;
}
export const PlayerContext = createContext<PlayerContext>({
addToQueueByData: () => {},
addToQueueByFetch: () => {},
clearQueue: () => {},
clearSelected: () => {},
decreaseVolume: () => {},
increaseVolume: () => {},
mediaNext: () => {},
mediaPause: () => {},
mediaPlay: () => {},
mediaPrevious: () => {},
mediaSeekToTimestamp: () => {},
mediaSkipBackward: () => {},
mediaSkipForward: () => {},
mediaStop: () => {},
mediaToggleMute: () => {},
mediaTogglePlayPause: () => {},
moveSelectedTo: () => {},
moveSelectedToBottom: () => {},
moveSelectedToNext: () => {},
moveSelectedToTop: () => {},
setRepeat: () => {},
setShuffle: () => {},
setSpeed: () => {},
setVolume: () => {},
shuffle: () => {},
shuffleAll: () => {},
shuffleSelected: () => {},
toggleRepeat: () => {},
toggleShuffle: () => {},
});
export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
const queryClient = useQueryClient();
const storeActions = usePlayerActions();
const addToQueueByData = useCallback(
(data: Song[], type: AddToQueueType) => {
if (typeof type === 'object' && 'edge' in type && type.edge !== null) {
const edge = type.edge === 'top' ? 'top' : 'bottom';
storeActions.addToQueueByUniqueId(data, type.uniqueId, edge);
} else {
storeActions.addToQueueByType(data, type as Play);
}
},
[storeActions],
);
const addToQueueByFetch = useCallback(
async (serverId: string, id: string[], itemType: LibraryItem, type: AddToQueueType) => {
const songs = await fetchSongsByItemType(queryClient, serverId, {
id,
itemType,
});
if (typeof type === 'object' && 'edge' in type && type.edge !== null) {
const edge = type.edge === 'top' ? 'top' : 'bottom';
storeActions.addToQueueByUniqueId(songs, type.uniqueId, edge);
} else {
storeActions.addToQueueByType(songs, type as Play);
}
},
[queryClient, storeActions],
);
const clearQueue = useCallback(() => {
storeActions.clearQueue();
}, [storeActions]);
const clearSelected = useCallback(
(items: QueueSong[]) => {
storeActions.clearSelected(items);
},
[storeActions],
);
const decreaseVolume = useCallback(
(amount: number) => {
storeActions.decreaseVolume(amount);
},
[storeActions],
);
const increaseVolume = useCallback(
(amount: number) => {
storeActions.increaseVolume(amount);
},
[storeActions],
);
const mediaNext = useCallback(() => {
storeActions.mediaNext();
}, [storeActions]);
const mediaPause = useCallback(() => {
storeActions.mediaPause();
}, [storeActions]);
const mediaPlay = useCallback(
(id?: string) => {
storeActions.mediaPlay(id);
},
[storeActions],
);
const mediaPrevious = useCallback(() => {
storeActions.mediaPrevious();
}, [storeActions]);
const mediaStop = useCallback(() => {
storeActions.mediaStop();
}, [storeActions]);
const mediaSeekToTimestamp = useCallback(
(timestamp: number) => {
storeActions.mediaSeekToTimestamp(timestamp);
},
[storeActions],
);
const mediaSkipBackward = useCallback(() => {
storeActions.mediaSkipBackward();
}, [storeActions]);
const mediaSkipForward = useCallback(() => {
storeActions.mediaSkipForward();
}, [storeActions]);
const setSpeed = useCallback(
(speed: number) => {
storeActions.setSpeed(speed);
},
[storeActions],
);
const mediaToggleMute = useCallback(() => {
storeActions.mediaToggleMute();
}, [storeActions]);
const mediaTogglePlayPause = useCallback(() => {
storeActions.mediaTogglePlayPause();
}, [storeActions]);
const moveSelectedTo = useCallback(
(items: QueueSong[], edge: 'bottom' | 'top', uniqueId: string) => {
storeActions.moveSelectedTo(items, uniqueId, edge);
},
[storeActions],
);
const moveSelectedToBottom = useCallback(
(items: QueueSong[]) => {
storeActions.moveSelectedToBottom(items);
},
[storeActions],
);
const moveSelectedToNext = useCallback(
(items: QueueSong[]) => {
storeActions.moveSelectedToNext(items);
},
[storeActions],
);
const moveSelectedToTop = useCallback(
(items: QueueSong[]) => {
storeActions.moveSelectedToTop(items);
},
[storeActions],
);
const setVolume = useCallback(
(volume: number) => {
storeActions.setVolume(volume);
},
[storeActions],
);
const setRepeat = useCallback(
(repeat: PlayerRepeat) => {
storeActions.setRepeat(repeat);
},
[storeActions],
);
const setShuffle = useCallback(
(shuffle: PlayerShuffle) => {
storeActions.setShuffle(shuffle);
},
[storeActions],
);
const shuffle = useCallback(() => {
storeActions.shuffle();
}, [storeActions]);
const shuffleAll = useCallback(() => {
storeActions.shuffleAll();
}, [storeActions]);
const shuffleSelected = useCallback(
(items: QueueSong[]) => {
storeActions.shuffleSelected(items);
},
[storeActions],
);
const toggleRepeat = useCallback(() => {
storeActions.toggleRepeat();
}, [storeActions]);
const toggleShuffle = useCallback(() => {
storeActions.toggleShuffle();
}, [storeActions]);
const contextValue: PlayerContext = useMemo(
() => ({
addToQueueByData,
addToQueueByFetch,
clearQueue,
clearSelected,
decreaseVolume,
increaseVolume,
mediaNext,
mediaPause,
mediaPlay,
mediaPrevious,
mediaSeekToTimestamp,
mediaSkipBackward,
mediaSkipForward,
mediaStop,
mediaToggleMute,
mediaTogglePlayPause,
moveSelectedTo,
moveSelectedToBottom,
moveSelectedToNext,
moveSelectedToTop,
setRepeat,
setShuffle,
setSpeed,
setVolume,
shuffle,
shuffleAll,
shuffleSelected,
toggleRepeat,
toggleShuffle,
}),
[
addToQueueByData,
addToQueueByFetch,
clearQueue,
clearSelected,
decreaseVolume,
setSpeed,
increaseVolume,
mediaNext,
mediaPause,
mediaPlay,
mediaPrevious,
mediaSeekToTimestamp,
mediaSkipBackward,
mediaSkipForward,
mediaStop,
mediaToggleMute,
mediaTogglePlayPause,
moveSelectedTo,
moveSelectedToBottom,
moveSelectedToNext,
moveSelectedToTop,
setRepeat,
setShuffle,
setVolume,
shuffle,
shuffleAll,
shuffleSelected,
toggleRepeat,
toggleShuffle,
],
);
return <PlayerContext.Provider value={contextValue}>{children}</PlayerContext.Provider>;
};
export const usePlayerContext = () => {
return useContext(PlayerContext);
};
/**
* Fetches the songs from the server
* @param queryClient - The query client to use to fetch the data
* @param serverId - The library id to use to fetch the data
* @param type - The type of the item to add to the queue
* @param args - The arguments to use to fetch the data
* @returns The songs to add to the queue
*/
export async function fetchSongsByItemType(
queryClient: QueryClient,
serverId: string,
args: {
id: string[];
itemType: LibraryItem;
params?: Record<string, any>;
},
) {
const songs: Song[] = [];
switch (args.itemType) {
case LibraryItem.ALBUM: {
const promises: Promise<SongListResponse>[] = [];
for (const id of args.id) {
promises.push(
queryClient.fetchQuery(
songsQueries.list({
query: {
albumIds: [id],
sortBy: SongListSort.ID,
sortOrder: SortOrder.ASC,
startIndex: 0,
...args.params,
},
serverId: serverId,
}),
),
);
}
const results = await Promise.all(promises);
songs.push(...results.flatMap((r) => r.items));
break;
}
case LibraryItem.ALBUM_ARTIST:
case LibraryItem.ARTIST: {
const promises: Promise<SongListResponse>[] = [];
for (const id of args.id) {
promises.push(
queryClient.fetchQuery(
songsQueries.list({
query: {
albumArtistIds: [id],
limit: -1,
sortBy: SongListSort.ID,
sortOrder: SortOrder.ASC,
startIndex: 0,
...args.params,
},
serverId: serverId,
}),
),
);
}
const results = await Promise.all(promises);
songs.push(...results.flatMap((r) => r.items));
break;
}
case LibraryItem.GENRE: {
const promises: Promise<SongListResponse>[] = [];
for (const id of args.id) {
promises.push(
queryClient.fetchQuery(
songsQueries.list({
query: {
genreIds: [id],
limit: -1,
sortBy: SongListSort.ID,
sortOrder: SortOrder.ASC,
startIndex: 0,
...args.params,
},
serverId: serverId,
}),
),
);
}
const results = await Promise.all(promises);
songs.push(...results.flatMap((r) => r.items));
break;
}
case LibraryItem.PLAYLIST: {
const promises: Promise<PlaylistSongListResponse>[] = [];
for (const id of args.id) {
promises.push(
queryClient.fetchQuery(
playlistsQueries.songList({
query: {
id: id,
...args.params,
},
serverId: serverId,
}),
),
);
}
const results = await Promise.all(promises);
songs.push(...results.flatMap((r) => r.items));
break;
}
}
return songs;
}
File diff suppressed because it is too large Load Diff
@@ -1,226 +1,226 @@
// import { useQueryClient } from '@tanstack/react-query';
// import isElectron from 'is-electron';
// import { nanoid } from 'nanoid/non-secure';
// import { useCallback, useRef } from 'react';
// import { useTranslation } from 'react-i18next';
import { useQueryClient } from '@tanstack/react-query';
import isElectron from 'is-electron';
import { nanoid } from 'nanoid/non-secure';
import { useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
// import { queryKeys } from '/@/renderer/api/query-keys';
// import { PlayersRef } from '/@/renderer/features/player/ref/players-ref';
// import { updateSong } from '/@/renderer/features/player/update-remote-song';
// import {
// getAlbumArtistSongsById,
// getAlbumSongsById,
// getArtistSongsById,
// getGenreSongsById,
// getPlaylistSongsById,
// getSongById,
// getSongsByQuery,
// } from '/@/renderer/features/player/utils';
// import { useCurrentServer, usePlayerStore } from '/@/renderer/store';
// import { useGeneralSettings, usePlaybackType } from '/@/renderer/store/settings.store';
// import { setQueue, setQueueNext } from '/@/renderer/utils/set-transcoded-queue-data';
// import { toast } from '/@/shared/components/toast/toast';
// import {
// instanceOfCancellationError,
// LibraryItem,
// QueueSong,
// Song,
// SongListResponse,
// } from '/@/shared/types/domain-types';
// import { Play, PlaybackType, PlayQueueAddOptions } from '/@/shared/types/types';
import { queryKeys } from '/@/renderer/api/query-keys';
import { PlayersRef } from '/@/renderer/features/player/ref/players-ref';
import { updateSong } from '/@/renderer/features/player/update-remote-song';
import {
getAlbumArtistSongsById,
getAlbumSongsById,
getArtistSongsById,
getGenreSongsById,
getPlaylistSongsById,
getSongById,
getSongsByQuery,
} from '/@/renderer/features/player/utils';
import { useCurrentServer, usePlayerControls, usePlayerStore } from '/@/renderer/store';
import { useGeneralSettings, usePlaybackType } from '/@/renderer/store/settings.store';
import { setQueue, setQueueNext } from '/@/renderer/utils/set-transcoded-queue-data';
import { toast } from '/@/shared/components/toast/toast';
import {
instanceOfCancellationError,
LibraryItem,
QueueSong,
Song,
SongListResponse,
} from '/@/shared/types/domain-types';
import { Play, PlaybackType, PlayQueueAddOptions } from '/@/shared/types/types';
// const getRootQueryKey = (itemType: LibraryItem, serverId: string) => {
// let queryKey;
const getRootQueryKey = (itemType: LibraryItem, serverId: string) => {
let queryKey;
// switch (itemType) {
// case LibraryItem.ALBUM:
// queryKey = queryKeys.songs.list(serverId);
// break;
// case LibraryItem.ALBUM_ARTIST:
// queryKey = queryKeys.songs.list(serverId);
// break;
// case LibraryItem.GENRE:
// queryKey = queryKeys.songs.list(serverId);
// break;
// case LibraryItem.PLAYLIST:
// queryKey = queryKeys.playlists.songList(serverId);
// break;
// case LibraryItem.SONG:
// queryKey = queryKeys.songs.list(serverId);
// break;
// default:
// queryKey = queryKeys.songs.list(serverId);
// break;
// }
switch (itemType) {
case LibraryItem.ALBUM:
queryKey = queryKeys.songs.list(serverId);
break;
case LibraryItem.ALBUM_ARTIST:
queryKey = queryKeys.songs.list(serverId);
break;
case LibraryItem.GENRE:
queryKey = queryKeys.songs.list(serverId);
break;
case LibraryItem.PLAYLIST:
queryKey = queryKeys.playlists.songList(serverId);
break;
case LibraryItem.SONG:
queryKey = queryKeys.songs.list(serverId);
break;
default:
queryKey = queryKeys.songs.list(serverId);
break;
}
// return queryKey;
// };
return queryKey;
};
// const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
// const addToQueue = usePlayerStore.getState().actions.addToQueue;
const addToQueue = usePlayerStore.getState().actions.addToQueue;
// export const useHandlePlayQueueAdd = () => {
// const { t } = useTranslation();
// const queryClient = useQueryClient();
// const playbackType = usePlaybackType();
// const server = useCurrentServer();
// const { play } = usePlayerControls();
// const timeoutIds = useRef<null | Record<string, ReturnType<typeof setTimeout>>>({});
export const useHandlePlayQueueAdd = () => {
const { t } = useTranslation();
const queryClient = useQueryClient();
const playbackType = usePlaybackType();
const server = useCurrentServer();
const { play } = usePlayerControls();
const timeoutIds = useRef<null | Record<string, ReturnType<typeof setTimeout>>>({});
// const { doubleClickQueueAll } = useGeneralSettings();
const { doubleClickQueueAll } = useGeneralSettings();
// const handlePlayQueueAdd = useCallback(
// async (options: PlayQueueAddOptions) => {
// if (!server) return toast.error({ message: 'No server selected', type: 'error' });
// const { byData, byItemType, initialIndex, initialSongId, playType, query } = options;
// let songs: null | QueueSong[] = null;
// // Allow this to be undefined for "play shuffled". If undefined, default to 0,
// // otherwise, choose the selected item in the queue
// let initialSongIndex: number | undefined;
// let toastId: null | string = null;
const handlePlayQueueAdd = useCallback(
async (options: PlayQueueAddOptions) => {
if (!server) return toast.error({ message: 'No server selected', type: 'error' });
const { byData, byItemType, initialIndex, initialSongId, playType, query } = options;
let songs: null | QueueSong[] = null;
// Allow this to be undefined for "play shuffled". If undefined, default to 0,
// otherwise, choose the selected item in the queue
let initialSongIndex: number | undefined;
let toastId: null | string = null;
// if (byItemType) {
// let songList: SongListResponse | undefined;
// const { id, type: itemType } = byItemType;
if (byItemType) {
let songList: SongListResponse | undefined;
const { id, type: itemType } = byItemType;
// const fetchId = nanoid();
// timeoutIds.current = {
// ...timeoutIds.current,
// [fetchId]: setTimeout(() => {
// toastId = toast.info({
// autoClose: false,
// message: t('player.playbackFetchCancel', {
// postProcess: 'sentenceCase',
// }),
// onClose: () => {
// queryClient.cancelQueries({
// exact: false,
// queryKey: getRootQueryKey(itemType, server?.id),
// });
// },
// title: t('player.playbackFetchInProgress', {
// postProcess: 'sentenceCase',
// }),
// });
// }, 2000),
// };
const fetchId = nanoid();
timeoutIds.current = {
...timeoutIds.current,
[fetchId]: setTimeout(() => {
toastId = toast.info({
autoClose: false,
message: t('player.playbackFetchCancel', {
postProcess: 'sentenceCase',
}),
onClose: () => {
queryClient.cancelQueries({
exact: false,
queryKey: getRootQueryKey(itemType, server?.id),
});
},
title: t('player.playbackFetchInProgress', {
postProcess: 'sentenceCase',
}),
});
}, 2000),
};
// try {
// if (itemType === LibraryItem.PLAYLIST) {
// songList = await getPlaylistSongsById({
// id: id?.[0],
// query,
// queryClient,
// server,
// });
// } else if (itemType === LibraryItem.ALBUM) {
// songList = await getAlbumSongsById({ id, query, queryClient, server });
// } else if (itemType === LibraryItem.ALBUM_ARTIST) {
// songList = await getAlbumArtistSongsById({
// id,
// query,
// queryClient,
// server,
// });
// } else if (itemType === LibraryItem.ARTIST) {
// songList = await getArtistSongsById({
// id,
// query,
// queryClient,
// server,
// });
// } else if (itemType === LibraryItem.GENRE) {
// songList = await getGenreSongsById({ id, query, queryClient, server });
// } else if (itemType === LibraryItem.SONG) {
// if (id?.length === 1) {
// songList = await getSongById({ id: id?.[0], queryClient, server });
// } else if (!doubleClickQueueAll && initialSongId) {
// songList = await getSongById({
// id: initialSongId,
// queryClient,
// server,
// });
// } else {
// songList = await getSongsByQuery({ query, queryClient, server });
// }
// }
try {
if (itemType === LibraryItem.PLAYLIST) {
songList = await getPlaylistSongsById({
id: id?.[0],
query,
queryClient,
server,
});
} else if (itemType === LibraryItem.ALBUM) {
songList = await getAlbumSongsById({ id, query, queryClient, server });
} else if (itemType === LibraryItem.ALBUM_ARTIST) {
songList = await getAlbumArtistSongsById({
id,
query,
queryClient,
server,
});
} else if (itemType === LibraryItem.ARTIST) {
songList = await getArtistSongsById({
id,
query,
queryClient,
server,
});
} else if (itemType === LibraryItem.GENRE) {
songList = await getGenreSongsById({ id, query, queryClient, server });
} else if (itemType === LibraryItem.SONG) {
if (id?.length === 1) {
songList = await getSongById({ id: id?.[0], queryClient, server });
} else if (!doubleClickQueueAll && initialSongId) {
songList = await getSongById({
id: initialSongId,
queryClient,
server,
});
} else {
songList = await getSongsByQuery({ query, queryClient, server });
}
}
// clearTimeout(timeoutIds.current[fetchId] as ReturnType<typeof setTimeout>);
// delete timeoutIds.current[fetchId];
// if (toastId) {
// toast.hide(toastId);
// }
// } catch (err: any) {
// if (instanceOfCancellationError(err)) {
// return null;
// }
clearTimeout(timeoutIds.current[fetchId] as ReturnType<typeof setTimeout>);
delete timeoutIds.current[fetchId];
if (toastId) {
toast.hide(toastId);
}
} catch (err: any) {
if (instanceOfCancellationError(err)) {
return null;
}
// clearTimeout(timeoutIds.current[fetchId] as ReturnType<typeof setTimeout>);
// delete timeoutIds.current[fetchId];
// toast.hide(fetchId);
clearTimeout(timeoutIds.current[fetchId] as ReturnType<typeof setTimeout>);
delete timeoutIds.current[fetchId];
toast.hide(fetchId);
// return toast.error({
// message: err.message,
// title: t('error.genericError', { postProcess: 'sentenceCase' }) as string,
// });
// }
return toast.error({
message: err.message,
title: t('error.genericError', { postProcess: 'sentenceCase' }) as string,
});
}
// songs =
// songList?.items?.map((song: Song) => ({ ...song, uniqueId: nanoid() })) || null;
// } else if (byData) {
// songs = byData.map((song) => ({ ...song, uniqueId: nanoid() })) || null;
// }
songs =
songList?.items?.map((song: Song) => ({ ...song, uniqueId: nanoid() })) || null;
} else if (byData) {
songs = byData.map((song) => ({ ...song, uniqueId: nanoid() })) || null;
}
// if (!songs || songs?.length === 0)
// return toast.warn({
// message: t('common.noResultsFromQuery', { postProcess: 'sentenceCase' }),
// title: t('player.playbackFetchNoResults', { postProcess: 'sentenceCase' }),
// });
if (!songs || songs?.length === 0)
return toast.warn({
message: t('common.noResultsFromQuery', { postProcess: 'sentenceCase' }),
title: t('player.playbackFetchNoResults', { postProcess: 'sentenceCase' }),
});
// if (initialIndex) {
// initialSongIndex = initialIndex;
// } else if (initialSongId) {
// initialSongIndex = songs.findIndex((song) => song.id === initialSongId);
// }
if (initialIndex) {
initialSongIndex = initialIndex;
} else if (initialSongId) {
initialSongIndex = songs.findIndex((song) => song.id === initialSongId);
}
// const hadSong = usePlayerStore.getState().queue.default.length > 0;
// const playerData = addToQueue({ initialIndex: initialSongIndex, playType, songs });
const hadSong = usePlayerStore.getState().queue.default.length > 0;
const playerData = addToQueue({ initialIndex: initialSongIndex, playType, songs });
// updateSong(playerData.current.song);
updateSong(playerData.current.song);
// const replacesQueue = playType === Play.NOW || playType === Play.SHUFFLE;
const replacesQueue = playType === Play.NOW || playType === Play.SHUFFLE;
// if (playbackType === PlaybackType.LOCAL) {
// mpvPlayer!.volume(usePlayerStore.getState().volume);
if (playbackType === PlaybackType.LOCAL) {
mpvPlayer!.volume(usePlayerStore.getState().volume);
// if (replacesQueue || !hadSong) {
// mpvPlayer!.pause();
// setQueue(playerData, false);
// } else {
// setQueueNext(playerData);
// }
// } else {
// const player =
// playerData.current.player === 1
// ? PlayersRef.current?.player1
// : PlayersRef.current?.player2;
// const underlying = player?.getInternalPlayer();
// if (underlying && replacesQueue) {
// underlying.currentTime = 0;
// }
// }
if (replacesQueue || !hadSong) {
mpvPlayer!.pause();
setQueue(playerData, false);
} else {
setQueueNext(playerData);
}
} else {
const player =
playerData.current.player === 1
? PlayersRef.current?.player1
: PlayersRef.current?.player2;
const underlying = player?.getInternalPlayer();
if (underlying && replacesQueue) {
underlying.currentTime = 0;
}
}
// // We should only play if the queue was empty, or we are doing play NOW
// // (override the queue).
// if (replacesQueue || !hadSong) {
// play();
// }
// We should only play if the queue was empty, or we are doing play NOW
// (override the queue).
if (replacesQueue || !hadSong) {
play();
}
// return null;
// },
// [doubleClickQueueAll, play, playbackType, queryClient, server, t],
// );
return null;
},
[doubleClickQueueAll, play, playbackType, queryClient, server, t],
);
// return handlePlayQueueAdd;
// };
return handlePlayQueueAdd;
};
@@ -1,8 +1,8 @@
import { useEffect } from 'react';
import {
usePlayerSong,
usePlayerStatus,
useCurrentSong,
useCurrentStatus,
usePlaybackSettings,
useSettingsStore,
} from '/@/renderer/store';
@@ -28,8 +28,8 @@ export const useMediaSession = ({
handleStop: () => void;
}) => {
const { mediaSession: mediaSessionEnabled } = usePlaybackSettings();
const playerStatus = usePlayerStatus();
const currentSong = usePlayerSong();
const playerStatus = useCurrentStatus();
const currentSong = useCurrentSong();
const mediaSession = navigator.mediaSession;
const skip = useSettingsStore((state) => state.general.skipButtons);
@@ -1,14 +1,14 @@
import isElectron from 'is-electron';
import { useCallback, useEffect } from 'react';
import { usePlayerStatus } from '/@/renderer/store';
import { useCurrentStatus } from '/@/renderer/store';
import { useWindowSettings } from '/@/renderer/store';
import { PlayerStatus } from '/@/shared/types/types';
const ipc = isElectron() ? window.api.ipc : null;
export const usePowerSaveBlocker = () => {
const status = usePlayerStatus();
const status = useCurrentStatus();
const { preventSleepOnPlayback } = useWindowSettings();
const startPowerSaveBlocker = useCallback(async () => {
@@ -1,7 +1,13 @@
import isElectron from 'is-electron';
import { useCallback, useEffect, WheelEvent } from 'react';
import { usePlayerMuted, usePlayerSpeed, usePlayerVolume } from '/@/renderer/store';
import {
useMuted,
usePlayerControls,
useSetCurrentSpeed,
useSpeed,
useVolume,
} from '/@/renderer/store';
import { useGeneralSettings } from '/@/renderer/store/settings.store';
const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
@@ -34,12 +40,12 @@ const calculateVolumeDown = (volume: number, volumeWheelStep: number) => {
};
export const useRightControls = () => {
// const { setMuted, setVolume } = usePlayerControls();
const volume = usePlayerVolume();
const muted = usePlayerMuted();
const { setMuted, setVolume } = usePlayerControls();
const volume = useVolume();
const muted = useMuted();
const { volumeWheelStep } = useGeneralSettings();
const speed = usePlayerSpeed();
// const setCurrentSpeed = useSetCurrentSpeed();
const speed = useSpeed();
const setCurrentSpeed = useSetCurrentSpeed();
// Ensure that the mpv player volume is set on startup
useEffect(() => {
@@ -57,37 +63,40 @@ export const useRightControls = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleSpeed = useCallback((e: number) => {
// setCurrentSpeed(e);
if (mpvPlayer) {
mpvPlayer?.setProperties({ speed: e });
}
}, []);
const handleSpeed = useCallback(
(e: number) => {
setCurrentSpeed(e);
if (mpvPlayer) {
mpvPlayer?.setProperties({ speed: e });
}
},
[setCurrentSpeed],
);
const handleVolumeSlider = (e: number) => {
mpvPlayer?.volume(e);
remote?.updateVolume(e);
// setVolume(e);
setVolume(e);
};
const handleVolumeSliderState = (e: number) => {
remote?.updateVolume(e);
// setVolume(e);
setVolume(e);
};
const handleVolumeDown = useCallback(() => {
const volumeToSet = calculateVolumeDown(volume, volumeWheelStep);
mpvPlayer?.volume(volumeToSet);
remote?.updateVolume(volumeToSet);
// setVolume(volumeToSet);
}, [volume, volumeWheelStep]);
setVolume(volumeToSet);
}, [setVolume, volume, volumeWheelStep]);
const handleVolumeUp = useCallback(() => {
const volumeToSet = calculateVolumeUp(volume, volumeWheelStep);
mpvPlayer?.volume(volumeToSet);
remote?.updateVolume(volumeToSet);
// setVolume(volumeToSet);
}, [volume, volumeWheelStep]);
setVolume(volumeToSet);
}, [setVolume, volume, volumeWheelStep]);
const handleVolumeWheel = useCallback(
(e: WheelEvent<HTMLButtonElement | HTMLDivElement>) => {
@@ -100,15 +109,15 @@ export const useRightControls = () => {
mpvPlayer?.volume(volumeToSet);
remote?.updateVolume(volumeToSet);
// setVolume(volumeToSet);
setVolume(volumeToSet);
},
[volume, volumeWheelStep],
[setVolume, volume, volumeWheelStep],
);
const handleMute = useCallback(() => {
// setMuted(!muted);
setMuted(!muted);
mpvPlayer?.mute(!muted);
}, [muted]);
}, [muted, setMuted]);
useEffect(() => {
if (isElectron()) {
@@ -103,7 +103,7 @@ export const useScrobble = () => {
// Only trigger if the song changed, or the player changed. This should be the case
// anyways, but who knows
if (
currentSong._uniqueId !== previous[0]?._uniqueId ||
currentSong.uniqueId !== previous[0]?.uniqueId ||
current[2] !== previous[2]
) {
const artists =
@@ -141,15 +141,15 @@ export const useScrobble = () => {
if (
(!isCurrentSongScrobbled && shouldSubmitScrobble) ||
previousSong?._serverType === ServerType.JELLYFIN
previousSong?.serverType === ServerType.JELLYFIN
) {
const position =
previousSong?._serverType === ServerType.JELLYFIN
previousSong?.serverType === ServerType.JELLYFIN
? previousSongTimeSec * 1e7
: undefined;
sendScrobble.mutate({
apiClientProps: { serverId: previousSong?._serverId || '' },
apiClientProps: { serverId: previousSong?.serverId || '' },
query: {
id: previousSong.id,
position,
@@ -173,7 +173,7 @@ export const useScrobble = () => {
// Send start scrobble when song changes and the new song is playing
if (currentStatus === PlayerStatus.PLAYING && currentSong?.id) {
sendScrobble.mutate({
apiClientProps: { serverId: currentSong?._serverId || '' },
apiClientProps: { serverId: currentSong?.serverId || '' },
query: {
event: 'start',
id: currentSong.id,
@@ -182,7 +182,7 @@ export const useScrobble = () => {
},
});
if (currentSong?._serverType === ServerType.JELLYFIN) {
if (currentSong?.serverType === ServerType.JELLYFIN) {
// It is possible that another function sets an interval.
// We only want one running, so clear the existing interval
if (progressIntervalId.current) {
@@ -379,7 +379,7 @@ export const useScrobble = () => {
// multiple times in a row and playback goes normally (no next/previous)
equalityFn: (a, b) =>
// compute whether the song changed
a[0]?._uniqueId === b[0]?._uniqueId &&
a[0]?.uniqueId === b[0]?.uniqueId &&
// compute whether the same player: relevant for repeat one and repeat all (one track)
a[2] === b[2],
},
@@ -3,11 +3,12 @@ import { AxiosError } from 'axios';
import { api } from '/@/renderer/api';
import { MutationOptions } from '/@/renderer/lib/react-query';
import { useIncrementQueuePlayCount } from '/@/renderer/store';
import { usePlayEvent } from '/@/renderer/store/event.store';
import { ScrobbleArgs, ScrobbleResponse } from '/@/shared/types/domain-types';
export const useSendScrobble = (options?: MutationOptions) => {
// const incrementPlayCount = useIncrementQueuePlayCount();
const incrementPlayCount = useIncrementQueuePlayCount();
const sendPlayEvent = usePlayEvent();
return useMutation<ScrobbleResponse, AxiosError, ScrobbleArgs, null>({
@@ -20,7 +21,7 @@ export const useSendScrobble = (options?: MutationOptions) => {
onSuccess: (_data, variables) => {
// Manually increment the play count for the song in the queue if scrobble was submitted
if (variables.query.submission) {
// incrementPlayCount([variables.query.id]);
incrementPlayCount([variables.query.id]);
sendPlayEvent(variables.query.id);
}
},
@@ -29,8 +29,8 @@ import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-a
import { useAppFocus } from '/@/renderer/hooks';
import {
useCurrentServer,
usePlayerSong,
usePlayerStatus,
useCurrentSong,
useCurrentStatus,
usePlaylistDetailStore,
usePlaylistDetailTablePagination,
useSetPlaylistDetailTable,
@@ -58,9 +58,9 @@ interface PlaylistDetailContentProps {
export const PlaylistDetailSongListContent = ({ songs, tableRef }: PlaylistDetailContentProps) => {
const { playlistId } = useParams() as { playlistId: string };
const queryClient = useQueryClient();
const status = usePlayerStatus();
const status = useCurrentStatus();
const isFocused = useAppFocus();
const currentSong = usePlayerSong();
const currentSong = useCurrentSong();
const server = useCurrentServer();
const page = usePlaylistDetailStore();
const filters: PlaylistSongListQueryClientSide = useMemo(() => {
@@ -377,7 +377,7 @@ export const PlaylistDetailSongListHeaderFilters = ({
if (!detailQuery.data) return;
deletePlaylistMutation?.mutate(
{
apiClientProps: { serverId: detailQuery.data._serverId },
apiClientProps: { serverId: detailQuery.data.serverId },
query: { id: detailQuery.data.id },
},
{
@@ -56,7 +56,7 @@ const PlaylistDetailSongListRoute = () => {
createPlaylistMutation.mutate(
{
apiClientProps: { serverId: detailQuery?.data?._serverId },
apiClientProps: { serverId: detailQuery?.data?.serverId },
body: {
_custom: {
navidrome: {
@@ -83,7 +83,7 @@ const PlaylistDetailSongListRoute = () => {
},
);
deletePlaylistMutation.mutate({
apiClientProps: { serverId: detailQuery?.data?._serverId },
apiClientProps: { serverId: detailQuery?.data?.serverId },
query: { id: playlistId },
});
},
@@ -124,7 +124,7 @@ const PlaylistDetailSongListRoute = () => {
}),
)
}
serverId={detailQuery?.data?._serverId || ''}
serverId={detailQuery?.data?.serverId || ''}
/>
),
title: t('common.saveAs', { postProcess: 'sentenceCase' }),
@@ -1,152 +0,0 @@
import isElectron from 'is-electron';
import { useEffect } from 'react';
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
import { useCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
import { useSetRating } from '/@/renderer/features/shared/mutations/set-rating-mutation';
import { usePlayerActions, useRemoteSettings } from '/@/renderer/store';
import { toast } from '/@/shared/components/toast/toast';
import { LibraryItem, Song } from '/@/shared/types/domain-types';
import { PlayerShuffle } from '/@/shared/types/types';
const remote = isElectron() ? window.api.remote : null;
const ipc = isElectron() ? window.api.ipc : null;
export const useRemote = () => {
const { mediaSkipForward, setTimestamp, setVolume } = usePlayerActions();
const remoteSettings = useRemoteSettings();
const updateRatingMutation = useSetRating({});
const addToFavoritesMutation = useCreateFavorite({});
const removeFromFavoritesMutation = useDeleteFavorite({});
// Initialize the remote
useEffect(() => {
if (!remote) {
return;
}
remote
?.updateSetting(
remoteSettings.enabled,
remoteSettings.port,
remoteSettings.username,
remoteSettings.password,
)
.catch((error) => {
toast.warn({ message: error, title: 'Failed to enable remote' });
});
// We only want to fire this once
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (!remote) {
return;
}
remote.requestPosition((_e: unknown, data: { position: number }) => {
const newTime = data.position;
setTimestamp(newTime);
});
remote.requestSeek((_e: unknown, data: { offset: number }) => {
mediaSkipForward(data.offset);
});
remote.requestRating(
(_e: unknown, data: { id: string; rating: number; serverId: string }) => {
updateRatingMutation.mutate({
apiClientProps: { serverId: data.serverId },
query: {
item: [
{
_serverId: data.serverId,
id: data.id,
itemType: LibraryItem.SONG,
} as Song,
],
rating: data.rating,
},
});
},
);
remote.requestVolume((_e: unknown, data: { volume: number }) => {
setVolume(data.volume);
});
remote.requestFavorite(
(_e: unknown, data: { favorite: boolean; id: string; serverId: string }) => {
const mutator = data.favorite
? addToFavoritesMutation
: removeFromFavoritesMutation;
mutator.mutate({
apiClientProps: { serverId: data.serverId },
query: {
id: [data.id],
type: LibraryItem.SONG,
},
});
},
);
return () => {
ipc?.removeAllListeners('request-position');
ipc?.removeAllListeners('request-seek');
ipc?.removeAllListeners('request-volume');
ipc?.removeAllListeners('request-favorite');
ipc?.removeAllListeners('request-rating');
};
}, [
addToFavoritesMutation,
mediaSkipForward,
removeFromFavoritesMutation,
setTimestamp,
setVolume,
updateRatingMutation,
]);
usePlayerEvents(
{
onPlayerProgress: (properties) => {
if (!remote) {
return;
}
remote.updatePosition(properties.timestamp);
},
onPlayerRepeat: (properties) => {
if (!remote) {
return;
}
remote.updateRepeat(properties.repeat);
},
onPlayerShuffle: (properties) => {
if (!remote) {
return;
}
const isShuffleEnabled = properties.shuffle !== PlayerShuffle.NONE;
remote.updateShuffle(isShuffleEnabled);
},
onPlayerStatus: (properties) => {
if (!remote) {
return;
}
remote.updatePlayback(properties.status);
},
onPlayerVolume: (properties) => {
if (!remote) {
return;
}
remote.updateVolume(properties.volume);
},
},
[],
);
};
@@ -7,7 +7,7 @@ import {
} from '/@/renderer/features/settings/components/settings-section';
import { useHotkeySettings, usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store';
import { Switch } from '/@/shared/components/switch/switch';
import { PlayerType } from '/@/shared/types/types';
import { PlaybackType } from '/@/shared/types/types';
const localSettings = isElectron() ? window.api.localSettings : null;
const isWindows = isElectron() ? window.api.utils.isWindows() : false;
@@ -25,7 +25,9 @@ export const WindowHotkeySettings = () => {
defaultChecked={settings.globalMediaHotkeys}
disabled={
!isElectron() ||
(enableWindowsMediaSession && isWindows && playbackType === PlayerType.WEB)
(enableWindowsMediaSession &&
isWindows &&
playbackType === PlaybackType.WEB)
}
onChange={(e) => {
setSettings({
@@ -6,13 +6,14 @@ import {
SettingOption,
SettingsSection,
} from '/@/renderer/features/settings/components/settings-section';
import { usePlayerActions, usePlayerProperties, usePlayerStatus } from '/@/renderer/store';
import { useCurrentStatus, usePlayerStore } from '/@/renderer/store';
import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
import { setQueue } from '/@/renderer/utils/set-transcoded-queue-data';
import { Select } from '/@/shared/components/select/select';
import { Slider } from '/@/shared/components/slider/slider';
import { Switch } from '/@/shared/components/switch/switch';
import { toast } from '/@/shared/components/toast/toast';
import { CrossfadeStyle, PlayerStatus, PlayerStyle, PlayerType } from '/@/shared/types/types';
import { CrossfadeStyle, PlaybackStyle, PlaybackType, PlayerStatus } from '/@/shared/types/types';
const ipc = isElectron() ? window.api.ipc : null;
@@ -25,10 +26,7 @@ export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) =>
const { t } = useTranslation();
const settings = usePlaybackSettings();
const { setSettings } = useSettingsStoreActions();
const status = usePlayerStatus();
const { crossfadeDuration, transitionType } = usePlayerProperties();
const { setCrossfadeDuration, setTransitionType } = usePlayerActions();
const status = useCurrentStatus();
const [audioDevices, setAudioDevices] = useState<{ label: string; value: string }[]>([]);
@@ -45,7 +43,7 @@ export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) =>
);
};
if (settings.type === PlayerType.WEB) {
if (settings.type === PlaybackType.WEB) {
getAudioDevices();
}
}, [settings.type, t]);
@@ -58,15 +56,19 @@ export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) =>
{
disabled: !isElectron(),
label: 'MPV',
value: PlayerType.LOCAL,
value: PlaybackType.LOCAL,
},
{ label: 'Web', value: PlayerType.WEB },
{ label: 'Web', value: PlaybackType.WEB },
]}
defaultValue={settings.type}
disabled={status === PlayerStatus.PLAYING}
onChange={(e) => {
setSettings({ playback: { ...settings, type: e as PlayerType } });
setSettings({ playback: { ...settings, type: e as PlaybackType } });
ipc?.send('settings-set', { property: 'playbackType', value: e });
if (isElectron() && e === PlaybackType.LOCAL) {
const queueData = usePlayerStore.getState().actions.getPlayerData();
setQueue(queueData);
}
}}
/>
),
@@ -87,7 +89,7 @@ export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) =>
clearable
data={audioDevices}
defaultValue={settings.audioDeviceId}
disabled={settings.type !== PlayerType.WEB}
disabled={settings.type !== PlaybackType.WEB}
onChange={(e) => setSettings({ playback: { ...settings, audioDeviceId: e } })}
/>
),
@@ -95,7 +97,7 @@ export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) =>
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: !isElectron() || settings.type !== PlayerType.WEB,
isHidden: !isElectron() || settings.type !== PlaybackType.WEB,
title: t('setting.audioDevice', { postProcess: 'sentenceCase' }),
},
{
@@ -107,26 +109,28 @@ export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) =>
context: 'optionNormal',
postProcess: 'titleCase',
}),
value: PlayerStyle.GAPLESS,
value: PlaybackStyle.GAPLESS,
},
{
label: t('setting.playbackStyle', {
context: 'optionCrossFade',
postProcess: 'titleCase',
}),
value: PlayerStyle.CROSSFADE,
value: PlaybackStyle.CROSSFADE,
},
]}
defaultValue={transitionType}
disabled={settings.type !== PlayerType.WEB || status === PlayerStatus.PLAYING}
onChange={(e) => setTransitionType(e as PlayerStyle)}
defaultValue={settings.style}
disabled={settings.type !== PlaybackType.WEB || status === PlayerStatus.PLAYING}
onChange={(e) =>
setSettings({ playback: { ...settings, style: e as PlaybackStyle } })
}
/>
),
description: t('setting.playbackStyle', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: settings.type !== PlayerType.WEB,
isHidden: settings.type !== PlaybackType.WEB,
note: status === PlayerStatus.PLAYING ? 'Player must be paused' : undefined,
title: t('setting.playbackStyle', {
context: 'description',
@@ -148,7 +152,7 @@ export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) =>
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: settings.type !== PlayerType.WEB,
isHidden: settings.type !== PlaybackType.WEB,
note: t('common.restartRequired', { postProcess: 'sentenceCase' }),
title: t('setting.webAudio', {
postProcess: 'sentenceCase',
@@ -169,7 +173,7 @@ export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) =>
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: settings.type !== PlayerType.WEB,
isHidden: settings.type !== PlaybackType.WEB,
title: t('setting.preservePitch', {
postProcess: 'sentenceCase',
}),
@@ -177,15 +181,17 @@ export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) =>
{
control: (
<Slider
defaultValue={crossfadeDuration}
defaultValue={settings.crossfadeDuration}
disabled={
settings.type !== PlayerType.WEB ||
settings.style !== PlayerStyle.CROSSFADE ||
settings.type !== PlaybackType.WEB ||
settings.style !== PlaybackStyle.CROSSFADE ||
status === PlayerStatus.PLAYING
}
max={15}
min={3}
onChangeEnd={(e) => setCrossfadeDuration(e)}
min={0}
onChangeEnd={(e) =>
setSettings({ playback: { ...settings, crossfadeDuration: e } })
}
w={100}
/>
),
@@ -193,7 +199,7 @@ export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) =>
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: settings.type !== PlayerType.WEB,
isHidden: settings.type !== PlaybackType.WEB,
note: status === PlayerStatus.PLAYING ? 'Player must be paused' : undefined,
title: t('setting.crossfadeDuration', {
postProcess: 'sentenceCase',
@@ -218,8 +224,8 @@ export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) =>
]}
defaultValue={settings.crossfadeStyle}
disabled={
settings.type !== PlayerType.WEB ||
settings.style !== PlayerStyle.CROSSFADE ||
settings.type !== PlaybackType.WEB ||
settings.style !== PlaybackStyle.CROSSFADE ||
status === PlayerStatus.PLAYING
}
onChange={(e) => {
@@ -235,7 +241,7 @@ export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) =>
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: settings.type !== PlayerType.WEB,
isHidden: settings.type !== PlaybackType.WEB,
note: status === PlayerStatus.PLAYING ? 'Player must be paused' : undefined,
title: t('setting.crossfadeStyle', { postProcess: 'sentenceCase' }),
},
@@ -7,7 +7,7 @@ import {
} from '/@/renderer/features/settings/components/settings-section';
import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
import { Switch } from '/@/shared/components/switch/switch';
import { PlayerType } from '/@/shared/types/types';
import { PlaybackType } from '/@/shared/types/types';
const isWindows = isElectron() ? window.api.utils.isWindows() : null;
const isDesktop = isElectron();
@@ -30,7 +30,7 @@ export const MediaSessionSettings = () => {
<Switch
aria-label="Toggle media Session"
defaultChecked={mediaSession}
disabled={!isWindows || !isDesktop || playbackType !== PlayerType.WEB}
disabled={!isWindows || !isDesktop || playbackType !== PlaybackType.WEB}
onChange={handleMediaSessionChange}
/>
),
@@ -6,9 +6,11 @@ import {
SettingOption,
SettingsSection,
} from '/@/renderer/features/settings/components/settings-section';
import { usePlayerControls, usePlayerStore, useQueueControls } from '/@/renderer/store';
import {
SettingsState,
usePlaybackSettings,
useSettingsStore,
useSettingsStoreActions,
} from '/@/renderer/store/settings.store';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
@@ -20,7 +22,7 @@ import { Switch } from '/@/shared/components/switch/switch';
import { TextInput } from '/@/shared/components/text-input/text-input';
import { Text } from '/@/shared/components/text/text';
import { Textarea } from '/@/shared/components/textarea/textarea';
import { PlayerType } from '/@/shared/types/types';
import { PlaybackType } from '/@/shared/types/types';
const localSettings = isElectron() ? window.api.localSettings : null;
const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
@@ -72,8 +74,8 @@ export const MpvSettings = () => {
const { t } = useTranslation();
const settings = usePlaybackSettings();
const { setSettings } = useSettingsStoreActions();
// const { pause } = usePlayerControls();
// const { clearQueue } = useQueueControls();
const { pause } = usePlayerControls();
const { clearQueue } = useQueueControls();
const [mpvPath, setMpvPath] = useState(
(localSettings?.get('mpv_path') as string | undefined) || '',
@@ -127,21 +129,21 @@ export const MpvSettings = () => {
mpvPlayer?.setProperties(mpvSetting);
};
// const handleReloadMpv = () => {
// pause();
// clearQueue();
const handleReloadMpv = () => {
pause();
clearQueue();
// const extraParameters = useSettingsStore.getState().playback.mpvExtraParameters;
// const properties: Record<string, any> = {
// speed: usePlayerStore.getState().speed,
// ...getMpvProperties(useSettingsStore.getState().playback.mpvProperties),
// };
// mpvPlayer?.restart({
// binaryPath: mpvPath || undefined,
// extraParameters,
// properties,
// });
// };
const extraParameters = useSettingsStore.getState().playback.mpvExtraParameters;
const properties: Record<string, any> = {
speed: usePlayerStore.getState().speed,
...getMpvProperties(useSettingsStore.getState().playback.mpvProperties),
};
mpvPlayer?.restart({
binaryPath: mpvPath || undefined,
extraParameters,
properties,
});
};
const handleSetExtraParameters = (data: string[]) => {
setSettings({
@@ -158,7 +160,7 @@ export const MpvSettings = () => {
<Group gap="sm">
<ActionIcon
icon="refresh"
// onClick={handleReloadMpv}
onClick={handleReloadMpv}
tooltip={{
label: t('common.reload', { postProcess: 'titleCase' }),
openDelay: 0,
@@ -192,7 +194,7 @@ export const MpvSettings = () => {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: settings.type !== PlayerType.LOCAL,
isHidden: settings.type !== PlaybackType.LOCAL,
note: 'Restart required',
title: t('setting.mpvExecutablePath', { postProcess: 'sentenceCase' }),
},
@@ -233,7 +235,7 @@ export const MpvSettings = () => {
</Text>
</Stack>
),
isHidden: settings.type !== PlayerType.LOCAL,
isHidden: settings.type !== PlaybackType.LOCAL,
note: t('common.restartRequired', {
postProcess: 'sentenceCase',
}),
@@ -266,7 +268,7 @@ export const MpvSettings = () => {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: settings.type !== PlayerType.LOCAL,
isHidden: settings.type !== PlaybackType.LOCAL,
title: t('setting.gaplessAudio', { postProcess: 'sentenceCase' }),
},
{
@@ -309,7 +311,7 @@ export const MpvSettings = () => {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: settings.type !== PlayerType.LOCAL,
isHidden: settings.type !== PlaybackType.LOCAL,
title: t('setting.audioExclusiveMode', { postProcess: 'sentenceCase' }),
},
];
@@ -8,7 +8,7 @@ import { ScrobbleSettings } from '/@/renderer/features/settings/components/playb
import { TranscodeSettings } from '/@/renderer/features/settings/components/playback/transcode-settings';
import { useSettingsStore } from '/@/renderer/store';
import { Stack } from '/@/shared/components/stack/stack';
import { PlayerType } from '/@/shared/types/types';
import { PlaybackType } from '/@/shared/types/types';
const MpvSettings = lazy(() =>
import('/@/renderer/features/settings/components/playback/mpv-settings').then((module) => {
@@ -22,7 +22,7 @@ export const PlaybackTab = () => {
const hasFancyAudio = useMemo(() => {
return (
(isElectron() && audioType === PlayerType.LOCAL) ||
(isElectron() && audioType === PlaybackType.LOCAL) ||
(useWebAudio && 'AudioContext' in window)
);
}, [audioType, useWebAudio]);
@@ -1,5 +1,12 @@
.filter-bar {
z-index: 1;
padding: var(--theme-spacing-md) var(--theme-spacing-sm);
box-shadow: 0 5px 15px rgb(0 0 0 / 65%);
@mixin dark {
box-shadow: 0 5px 15px rgb(0 0 0 / 65%);
}
@mixin light {
box-shadow: 0 2px 0 var(--theme-colors-border);
}
}
@@ -5,7 +5,7 @@ import isElectron from 'is-electron';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { MutationHookArgs } from '/@/renderer/lib/react-query';
import { useSetAlbumListItemDataById } from '/@/renderer/store';
import { useSetAlbumListItemDataById, useSetQueueFavorite } from '/@/renderer/store';
import { useFavoriteEvent } from '/@/renderer/store/event.store';
import {
AlbumArtistDetailResponse,
@@ -21,7 +21,7 @@ export const useCreateFavorite = (args: MutationHookArgs) => {
const { options } = args || {};
const queryClient = useQueryClient();
const setAlbumListData = useSetAlbumListItemDataById();
// const setQueueFavorite = useSetQueueFavorite();
const setQueueFavorite = useSetQueueFavorite();
const setFavoriteEvent = useFavoriteEvent();
return useMutation<FavoriteResponse, AxiosError, FavoriteArgs, null>({
@@ -46,7 +46,7 @@ export const useCreateFavorite = (args: MutationHookArgs) => {
if (variables.query.type === LibraryItem.SONG) {
remote?.updateFavorite(true, serverId, variables.query.id);
// setQueueFavorite(variables.query.id, true);
setQueueFavorite(variables.query.id, true);
setFavoriteEvent(variables.query.id, true);
}
@@ -5,7 +5,7 @@ import isElectron from 'is-electron';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { MutationHookArgs } from '/@/renderer/lib/react-query';
import { useSetAlbumListItemDataById } from '/@/renderer/store';
import { useSetAlbumListItemDataById, useSetQueueFavorite } from '/@/renderer/store';
import { useFavoriteEvent } from '/@/renderer/store/event.store';
import {
AlbumArtistDetailResponse,
@@ -21,7 +21,7 @@ export const useDeleteFavorite = (args: MutationHookArgs) => {
const { options } = args || {};
const queryClient = useQueryClient();
const setAlbumListData = useSetAlbumListItemDataById();
// const setQueueFavorite = useSetQueueFavorite();
const setQueueFavorite = useSetQueueFavorite();
const setFavoriteEvent = useFavoriteEvent();
return useMutation<FavoriteResponse, AxiosError, FavoriteArgs, null>({
@@ -44,7 +44,7 @@ export const useDeleteFavorite = (args: MutationHookArgs) => {
if (variables.query.type === LibraryItem.SONG) {
remote?.updateFavorite(false, serverId, variables.query.id);
// setQueueFavorite(variables.query.id, false);
setQueueFavorite(variables.query.id, false);
setFavoriteEvent(variables.query.id, false);
}
@@ -5,7 +5,7 @@ import isElectron from 'is-electron';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { MutationHookArgs } from '/@/renderer/lib/react-query';
import { useSetAlbumListItemDataById } from '/@/renderer/store';
import { useSetAlbumListItemDataById, useSetQueueRating } from '/@/renderer/store';
import { useRatingEvent } from '/@/renderer/store/event.store';
import {
Album,
@@ -24,7 +24,7 @@ export const useSetRating = (args: MutationHookArgs) => {
const { options } = args || {};
const queryClient = useQueryClient();
const setAlbumListData = useSetAlbumListItemDataById();
// const setQueueRating = useSetQueueRating();
const setQueueRating = useSetQueueRating();
const setRatingEvent = useRatingEvent();
return useMutation<
@@ -46,7 +46,7 @@ export const useSetRating = (args: MutationHookArgs) => {
setAlbumListData(item.id, { userRating: item.userRating });
break;
case LibraryItem.SONG:
// setQueueRating([item.id], item.userRating);
setQueueRating([item.id], item.userRating);
setRatingEvent([item.id], item.userRating);
break;
}
@@ -67,14 +67,14 @@ export const useSetRating = (args: MutationHookArgs) => {
}
if (songIds.length > 0) {
// setQueueRating(songIds, variables.query.rating);
setQueueRating(songIds, variables.query.rating);
setRatingEvent(songIds, variables.query.rating);
}
if (remote) {
remote.updateRating(
variables.query.rating,
variables.query.item[0]._serverId,
variables.query.item[0].serverId,
songIds,
);
}
@@ -88,9 +88,9 @@ export const useSetRating = (args: MutationHookArgs) => {
variables.query.item[0].itemType === LibraryItem.ALBUM;
if (isAlbumDetailPage) {
const { id: albumId, _serverId } = variables.query.item[0] as Album;
const { id: albumId, serverId } = variables.query.item[0] as Album;
const queryKey = queryKeys.albums.detail(_serverId || '', { id: albumId });
const queryKey = queryKeys.albums.detail(serverId || '', { id: albumId });
const previous = queryClient.getQueryData<AlbumDetailResponse>(queryKey);
if (previous) {
queryClient.setQueryData<AlbumDetailResponse>(queryKey, {
@@ -17,7 +17,7 @@ import {
} from '/@/renderer/features/sidebar/components/sidebar-playlist-list';
import {
useAppStoreActions,
usePlayerSong,
useCurrentSong,
useFullScreenPlayerStore,
useSetFullScreenPlayerStore,
useSidebarStore,
@@ -42,7 +42,7 @@ export const Sidebar = () => {
const sidebar = useSidebarStore();
const { setSideBar } = useAppStoreActions();
const { sidebarPlaylistList } = useGeneralSettings();
const currentSong = usePlayerSong();
const currentSong = useCurrentSong();
const translatedSidebarItemMap = useMemo(
() => ({
@@ -9,6 +9,7 @@ import { getColumnDefs, VirtualTable } from '/@/renderer/components/virtual-tabl
import { ErrorFallback } from '/@/renderer/features/action-required/components/error-fallback';
import { SONG_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
import { useHandleTableContextMenu } from '/@/renderer/features/context-menu/hooks/use-handle-context-menu';
import { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-handle-playqueue-add';
import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
import { usePlayButtonBehavior, useTableSettings } from '/@/renderer/store';
import { Spinner } from '/@/shared/components/spinner/spinner';
@@ -23,6 +24,8 @@ export type SimilarSongsListProps = {
export const SimilarSongsList = ({ count, fullScreen, song }: SimilarSongsListProps) => {
const tableRef = useRef<AgGridReact<Song> | null>(null);
const tableConfig = useTableSettings(fullScreen ? 'fullScreen' : 'songs');
const handlePlayQueueAdd = useHandlePlayQueueAdd();
const playButtonBehavior = usePlayButtonBehavior();
const songQuery = useQuery(
songsQueries.similar({
@@ -34,7 +37,7 @@ export const SimilarSongsList = ({ count, fullScreen, song }: SimilarSongsListPr
count,
songId: song.id,
},
serverId: song?._serverId,
serverId: song?.serverId,
}),
);
@@ -48,11 +51,11 @@ export const SimilarSongsList = ({ count, fullScreen, song }: SimilarSongsListPr
const handleRowDoubleClick = (e: RowDoubleClickedEvent<Song>) => {
if (!e.data || !songQuery.data) return;
// handlePlayQueueAdd?.({
// byData: songQuery.data,
// initialSongId: e.data.id,
// playType: playButtonBehavior,
// });
handlePlayQueueAdd?.({
byData: songQuery.data,
initialSongId: e.data.id,
playType: playButtonBehavior,
});
};
return songQuery.isLoading ? (
@@ -12,8 +12,8 @@ import { SONG_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/conte
import { useAppFocus } from '/@/renderer/hooks';
import {
useCurrentServer,
usePlayerSong,
usePlayerStatus,
useCurrentSong,
useCurrentStatus,
usePlayButtonBehavior,
} from '/@/renderer/store';
import { LibraryItem, QueueSong, SongListQuery } from '/@/shared/types/domain-types';
@@ -27,8 +27,8 @@ export const SongListTableView = ({ itemCount, tableRef }: SongListTableViewProp
const server = useCurrentServer();
const { customFilters, handlePlay, id, pageKey } = useListContext();
const isFocused = useAppFocus();
const currentSong = usePlayerSong();
const status = usePlayerStatus();
const currentSong = useCurrentSong();
const status = useCurrentStatus();
const { rowClassRules } = useCurrentSongRowStyles({ tableRef });
+29 -7
View File
@@ -1,16 +1,22 @@
import { HotkeyItem, useHotkeys } from '@mantine/hooks';
import clsx from 'clsx';
import isElectron from 'is-electron';
import { lazy } from 'react';
import { lazy, useMemo } from 'react';
import { useNavigate } from 'react-router';
import styles from './default-layout.module.css';
import { ContextMenuProvider } from '/@/renderer/features/context-menu/context-menu-provider';
import { CommandPalette } from '/@/renderer/features/search/components/command-palette';
import { MainContent } from '/@/renderer/layouts/default-layout/main-content';
import { PlayerBar } from '/@/renderer/layouts/default-layout/player-bar';
import { AppRoute } from '/@/renderer/router/routes';
import { useCommandPalette } from '/@/renderer/store';
import {
useAppStore,
useCommandPalette,
useCurrentStatus,
useQueueStatus,
} from '/@/renderer/store';
import {
useGeneralSettings,
useHotkeySettings,
@@ -18,13 +24,13 @@ import {
useSettingsStoreActions,
useWindowSettings,
} from '/@/renderer/store/settings.store';
import { Platform, PlayerType } from '/@/shared/types/types';
import { Platform, PlaybackType, PlayerStatus } from '/@/shared/types/types';
if (!isElectron()) {
useSettingsStore.getState().actions.setSettings({
playback: {
...useSettingsStore.getState().playback,
type: PlayerType.WEB,
type: PlaybackType.WEB,
},
});
}
@@ -47,6 +53,9 @@ export const DefaultLayout = ({ shell }: DefaultLayoutProps) => {
const localSettings = isElectron() ? window.api.localSettings : null;
const settings = useGeneralSettings();
const { setSettings } = useSettingsStoreActions();
const playerStatus = useCurrentStatus();
const { currentSong, index, length } = useQueueStatus();
const { privateMode } = useAppStore();
const updateZoom = (increase: number) => {
const newVal = settings.zoomFactor + increase;
@@ -74,8 +83,21 @@ export const DefaultLayout = ({ shell }: DefaultLayoutProps) => {
...(isElectron() ? zoomHotkeys : []),
]);
const title = useMemo(() => {
const statusString = playerStatus === PlayerStatus.PAUSED ? '(Paused) ' : '';
const queueString = length ? `(${index + 1} / ${length}) ` : '';
const privateModeString = privateMode ? '(Private mode)' : '';
const title = `${
length
? `${statusString}${queueString}${currentSong?.name}${currentSong?.artistName ? `${currentSong?.artistName} — Feishin` : ''}`
: 'Feishin'
}${privateMode ? ` ${privateModeString}` : ''}`;
document.title = title;
return title;
}, [currentSong?.artistName, currentSong?.name, index, length, playerStatus, privateMode]);
return (
<>
<ContextMenuProvider>
<div
className={clsx(styles.layout, {
[styles.macos]: windowBarStyle === Platform.MACOS,
@@ -83,11 +105,11 @@ export const DefaultLayout = ({ shell }: DefaultLayoutProps) => {
})}
id="default-layout"
>
<WindowBar />
{windowBarStyle !== Platform.WEB && <WindowBar title={title} />}
<MainContent shell={shell} />
<PlayerBar />
</div>
<CommandPalette modalProps={{ handlers, opened }} />
</>
</ContextMenuProvider>
);
};
+8 -24
View File
@@ -1,6 +1,6 @@
import clsx from 'clsx';
import isElectron from 'is-electron';
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useState } from 'react';
import { RiCheckboxBlankLine, RiCloseLine, RiSubtractLine } from 'react-icons/ri';
import appIcon from '../../../assets/icons/32x32.png';
@@ -12,9 +12,9 @@ import macMinHover from './assets/min-mac-hover.png';
import macMin from './assets/min-mac.png';
import styles from './window-bar.module.css';
import { useAppStore, usePlayerData, usePlayerStatus, useWindowSettings } from '/@/renderer/store';
import { useWindowSettings } from '/@/renderer/store';
import { Text } from '/@/shared/components/text/text';
import { Platform, PlayerStatus } from '/@/shared/types/types';
import { Platform } from '/@/shared/types/types';
const localSettings = isElectron() ? window.api.localSettings : null;
@@ -121,13 +121,14 @@ const MacOsControls = ({ controls, title }: WindowBarControlsProps) => {
);
};
export const WindowBar = () => {
interface WindowBarProps {
title: string;
}
export const WindowBar = ({ title }: WindowBarProps) => {
const { windowBarStyle } = useWindowSettings();
const playerStatus = usePlayerStatus();
const { privateMode } = useAppStore();
const handleMinimize = () => minimize();
const { currentSong, index, queueLength } = usePlayerData();
const [max, setMax] = useState(localSettings?.env.START_MAXIMIZED || false);
const handleMaximize = useCallback(() => {
@@ -141,23 +142,6 @@ export const WindowBar = () => {
const handleClose = useCallback(() => close(), []);
const title = useMemo(() => {
const statusString = playerStatus === PlayerStatus.PAUSED ? '(Paused) ' : '';
const queueString = queueLength ? `(${index + 1} / ${queueLength}) ` : '';
const privateModeString = privateMode ? '(Private mode)' : '';
const title = `${
queueLength
? `${statusString}${queueString}${currentSong?.name}${currentSong?.artistName ? `${currentSong?.artistName} — Feishin` : ''}`
: 'Feishin'
}${privateMode ? ` ${privateModeString}` : ''}`;
document.title = title;
return title;
}, [currentSong?.artistName, currentSong?.name, index, playerStatus, privateMode, queueLength]);
if (windowBarStyle === Platform.WEB) {
return null;
}
return (
<>
{windowBarStyle === Platform.WINDOWS && (
-15
View File
@@ -1,15 +0,0 @@
import type { StoreApi, UseBoundStore } from 'zustand';
type WithSelectors<S> = S extends { getState: () => infer T }
? S & { use: { [K in keyof T]: () => T[K] } }
: never;
export const createSelectors = <S extends UseBoundStore<StoreApi<object>>>(_store: S) => {
const store = _store as WithSelectors<typeof _store>;
store.use = {};
for (const k of Object.keys(store.getState())) {
(store.use as any)[k] = () => store((s) => s[k as keyof typeof s]);
}
return store;
};
+14 -14
View File
@@ -4,7 +4,7 @@ import { Navigate, Outlet } from 'react-router-dom';
import { useServerAuthenticated } from '/@/renderer/hooks/use-server-authenticated';
import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer } from '/@/renderer/store';
import { useCurrentServer, useSetPlayerFallback } from '/@/renderer/store';
import { Center } from '/@/shared/components/center/center';
import { Spinner } from '/@/shared/components/spinner/spinner';
import { toast } from '/@/shared/components/toast/toast';
@@ -16,7 +16,7 @@ const mpvPlayerListener = isElectron() ? window.api.mpvPlayerListener : null;
export const AppOutlet = () => {
const currentServer = useCurrentServer();
// const setFallback = useSetPlayerFallback();
const setFallback = useSetPlayerFallback();
const authState = useServerAuthenticated();
const isActionsRequired = useMemo(() => {
@@ -28,20 +28,20 @@ export const AppOutlet = () => {
return isActionRequired;
}, [currentServer]);
// useEffect(() => {
// utils?.mainMessageListener((_event, data) => {
// toast.show(data);
// });
useEffect(() => {
utils?.mainMessageListener((_event, data) => {
toast.show(data);
});
// mpvPlayerListener?.rendererPlayerFallback((_event, data) => {
// setFallback(data);
// });
mpvPlayerListener?.rendererPlayerFallback((_event, data) => {
setFallback(data);
});
// return () => {
// ipc?.removeAllListeners('toast-from-main');
// ipc?.removeAllListeners('renderer-player-fallback');
// };
// }, [setFallback]);
return () => {
ipc?.removeAllListeners('toast-from-main');
ipc?.removeAllListeners('renderer-player-fallback');
};
}, [setFallback]);
if (authState === AuthState.LOADING) {
return (
File diff suppressed because it is too large Load Diff
+14 -7
View File
@@ -9,6 +9,7 @@ import { createWithEqualityFn } from 'zustand/traditional';
import i18n from '/@/i18n/i18n';
import { ContextMenuItemType } from '/@/renderer/features/context-menu/events';
import { AppRoute } from '/@/renderer/router/routes';
import { usePlayerStore } from '/@/renderer/store/player.store';
import { mergeOverridingColumns } from '/@/renderer/store/utils';
import { FontValueSchema } from '/@/renderer/types/fonts';
import { randomString } from '/@/renderer/utils';
@@ -20,8 +21,8 @@ import {
FontType,
Platform,
Play,
PlayerStyle,
PlayerType,
PlaybackStyle,
PlaybackType,
TableColumn,
TableType,
} from '/@/shared/types/types';
@@ -257,9 +258,9 @@ const PlaybackSettingsSchema = z.object({
muted: z.boolean(),
preservePitch: z.boolean(),
scrobble: ScrobbleSettingsSchema,
style: z.nativeEnum(PlayerStyle),
style: z.nativeEnum(PlaybackStyle),
transcode: TranscodingConfigSchema,
type: z.nativeEnum(PlayerType),
type: z.nativeEnum(PlaybackType),
webAudio: z.boolean(),
});
@@ -651,11 +652,11 @@ const initialState: SettingsState = {
scrobbleAtDuration: 240,
scrobbleAtPercentage: 75,
},
style: PlayerStyle.GAPLESS,
style: PlaybackStyle.GAPLESS,
transcode: {
enabled: false,
},
type: PlayerType.WEB,
type: PlaybackType.WEB,
webAudio: true,
},
remote: {
@@ -842,7 +843,7 @@ export const useSettingsStore = createWithEqualityFn<SettingsSlice>()(
...initialState,
playback: {
...initialState.playback,
type: PlayerType.WEB,
type: PlaybackType.WEB,
},
});
} else {
@@ -966,6 +967,12 @@ export const useGeneralSettings = () => useSettingsStore((state) => state.genera
export const usePlaybackType = () =>
useSettingsStore((state) => {
const isFallback = usePlayerStore.getState().fallback;
if (isFallback) {
return PlaybackType.WEB;
}
return state.playback.type;
});
@@ -12,7 +12,7 @@ const modifyUrl = (song: QueueSong): string => {
if (transcode.enabled) {
const streamUrl = api.controller.getTranscodingUrl({
apiClientProps: {
serverId: song._serverId,
serverId: song.serverId,
},
query: {
base: song.streamUrl,
-22
View File
@@ -1,22 +0,0 @@
export function shuffle<T>(array: T[]): T[] {
// Create a copy of the array to avoid mutating the original
const shuffled = [...array];
// Loop through the array from the last element to the first
for (let i = shuffled.length - 1; i > 0; i--) {
// Generate a random index from 0 to i
const j = Math.floor(Math.random() * (i + 1));
// Swap elements at positions i and j
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
}
export function shuffleInPlace<T>(array: T[]): T[] {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
+11 -8
View File
@@ -1,3 +1,4 @@
import { nanoid } from 'nanoid';
import { z } from 'zod';
import { jfType } from '/@/shared/api/jellyfin/jellyfin-types';
@@ -227,8 +228,6 @@ const normalizeSong = (
}
return {
_serverId: server?.id || '',
_serverType: ServerType.JELLYFIN,
album: item.Album,
albumArtists: item.AlbumArtists?.map((entry) => ({
id: entry.Id,
@@ -291,10 +290,13 @@ const normalizeSong = (
: null,
releaseYear: item.ProductionYear ? String(item.ProductionYear) : null,
sampleRate,
serverId: server?.id || '',
serverType: ServerType.JELLYFIN,
size,
streamUrl,
tags: getTags(item),
trackNumber: item.IndexNumber,
uniqueId: nanoid(),
updatedAt: item.DateCreated,
userFavorite: (item.UserData && item.UserData.IsFavorite) || false,
userRating: null,
@@ -307,8 +309,6 @@ const normalizeAlbum = (
imageSize?: number,
): Album => {
return {
_serverId: server?.id || '',
_serverType: ServerType.JELLYFIN,
albumArtist: item.AlbumArtist,
albumArtists:
item.AlbumArtists.map((entry) => ({
@@ -351,10 +351,13 @@ const normalizeAlbum = (
releaseDate: item.PremiereDate?.split('T')[0] || null,
releaseTypes: [],
releaseYear: item.ProductionYear || null,
serverId: server?.id || '',
serverType: ServerType.JELLYFIN,
size: null,
songCount: item?.ChildCount || null,
songs: item.Songs?.map((song) => normalizeSong(song, server, '', imageSize)),
tags: getTags(item),
uniqueId: nanoid(),
updatedAt: item?.DateLastMediaAdded || item.DateCreated,
userFavorite: item.UserData?.IsFavorite || false,
userRating: null,
@@ -383,8 +386,6 @@ const normalizeAlbumArtist = (
) || [];
return {
_serverId: server?.id || '',
_serverType: ServerType.JELLYFIN,
albumCount: item.AlbumCount ?? null,
backgroundImageUrl: null,
biography: item.Overview || null,
@@ -406,6 +407,8 @@ const normalizeAlbumArtist = (
mbz: item.ProviderIds?.MusicBrainzArtist || null,
name: item.Name,
playCount: item.UserData?.PlayCount || 0,
serverId: server?.id || '',
serverType: ServerType.JELLYFIN,
similarArtists,
songCount: item.SongCount ?? null,
userFavorite: item.UserData?.IsFavorite || false,
@@ -427,8 +430,6 @@ const normalizePlaylist = (
const imagePlaceholderUrl = null;
return {
_serverId: server?.id || '',
_serverType: ServerType.JELLYFIN,
description: item.Overview || null,
duration: item.RunTimeTicks / 10000,
genres: item.GenreItems?.map((entry) => ({
@@ -446,6 +447,8 @@ const normalizePlaylist = (
ownerId: null,
public: null,
rules: null,
serverId: server?.id || '',
serverType: ServerType.JELLYFIN,
size: null,
songCount: item?.ChildCount || null,
sync: null,
@@ -1,3 +1,4 @@
import { nanoid } from 'nanoid';
import z from 'zod';
import { ndType } from '/@/shared/api/navidrome/navidrome-types';
@@ -151,8 +152,6 @@ const normalizeSong = (
album: item.album,
albumId: item.albumId,
...getArtists(item),
_serverId: server?.id || 'unknown',
_serverType: ServerType.NAVIDROME,
artistName: item.artist,
bitDepth: item.bitDepth || null,
bitRate: item.bitRate,
@@ -205,10 +204,13 @@ const normalizeSong = (
).toISOString(),
releaseYear: String(item.year),
sampleRate: item.sampleRate || null,
serverId: server?.id || 'unknown',
serverType: ServerType.NAVIDROME,
size: item.size,
streamUrl: `${server?.url}/rest/stream.view?id=${id}&v=1.13.0&c=Feishin&${server?.credential}`,
tags: item.tags || null,
trackNumber: item.trackNumber,
uniqueId: nanoid(),
updatedAt: item.updatedAt,
userFavorite: item.starred || false,
userRating: item.rating || null,
@@ -278,8 +280,6 @@ const normalizeAlbum = (
return {
...parseAlbumTags(item),
...getArtists(item),
_serverId: server?.id || 'unknown',
_serverType: ServerType.NAVIDROME,
albumArtist: item.albumArtist,
backdropImageUrl: imageBackdropUrl,
comment: item.comment || null,
@@ -301,9 +301,9 @@ const normalizeAlbum = (
imagePlaceholderUrl,
imageUrl,
isCompilation: item.compilation,
itemType: LibraryItem.ALBUM,
lastPlayedAt: normalizePlayDate(item),
mbzId: item.mbzAlbumId || null,
name: item.name,
originalDate: item.originalDate
@@ -317,9 +317,12 @@ const normalizeAlbum = (
: new Date(Date.UTC(item.minYear, 0, 1))
).toISOString(),
releaseYear: item.minYear,
serverId: server?.id || 'unknown',
serverType: ServerType.NAVIDROME,
size: item.size,
songCount: item.songCount,
songs: item.songs ? item.songs.map((song) => normalizeSong(song, server)) : undefined,
uniqueId: nanoid(),
updatedAt: item.updatedAt,
userFavorite: item.starred,
userRating: item.rating || null,
@@ -362,8 +365,6 @@ const normalizeAlbumArtist = (
}
return {
_serverId: server?.id || 'unknown',
_serverType: ServerType.NAVIDROME,
albumCount,
backgroundImageUrl: null,
biography: item.biography || null,
@@ -381,6 +382,8 @@ const normalizeAlbumArtist = (
mbz: item.mbzArtistId || null,
name: item.name,
playCount: item.playCount || 0,
serverId: server?.id || 'unknown',
serverType: ServerType.NAVIDROME,
similarArtists:
item.similarArtists?.map((artist) => ({
id: artist.id,
@@ -409,8 +412,6 @@ const normalizePlaylist = (
const imagePlaceholderUrl = null;
return {
_serverId: server?.id || 'unknown',
_serverType: ServerType.NAVIDROME,
description: item.comment,
duration: item.duration * 1000,
genres: [],
@@ -423,6 +424,8 @@ const normalizePlaylist = (
ownerId: item.ownerId,
public: item.public,
rules: item?.rules || null,
serverId: server?.id || 'unknown',
serverType: ServerType.NAVIDROME,
size: item.size,
songCount: item.songCount,
sync: item.sync,
+13 -10
View File
@@ -1,3 +1,4 @@
import { nanoid } from 'nanoid';
import { z } from 'zod';
import { ssType } from '/@/shared/api/subsonic/subsonic-types';
@@ -8,10 +9,10 @@ import {
Genre,
LibraryItem,
Playlist,
QueueSong,
RelatedArtist,
ServerListItemWithCredential,
ServerType,
Song,
} from '/@/shared/types/domain-types';
const getCoverArtUrl = (args: {
@@ -118,7 +119,7 @@ const normalizeSong = (
item: z.infer<typeof ssType._response.song>,
server?: null | ServerListItemWithCredential,
size?: number,
): Song => {
): QueueSong => {
const imageUrl =
getCoverArtUrl({
baseUrl: server?.url,
@@ -130,8 +131,6 @@ const normalizeSong = (
const streamUrl = `${server?.url}/rest/stream.view?id=${item.id}&v=1.13.0&c=Feishin&${server?.credential}`;
return {
_serverId: server?.id || 'unknown',
_serverType: ServerType.SUBSONIC,
album: item.album || '',
albumArtists: getArtistList(item.albumArtists, item.artistId, item.artist),
albumId: item.albumId?.toString() || '',
@@ -184,10 +183,13 @@ const normalizeSong = (
releaseDate: null,
releaseYear: item.year ? String(item.year) : null,
sampleRate: item.samplingRate || null,
serverId: server?.id || 'unknown',
serverType: ServerType.SUBSONIC,
size: item.size,
streamUrl,
tags: null,
trackNumber: item.track || 1,
uniqueId: nanoid(),
updatedAt: '',
userFavorite: item.starred || false,
userRating: item.userRating || null,
@@ -210,8 +212,6 @@ const normalizeAlbumArtist = (
}) || null;
return {
_serverId: server?.id || 'unknown',
_serverType: ServerType.SUBSONIC,
albumCount: item.albumCount ? Number(item.albumCount) : 0,
backgroundImageUrl: null,
biography: null,
@@ -224,6 +224,8 @@ const normalizeAlbumArtist = (
mbz: null,
name: item.name,
playCount: null,
serverId: server?.id || 'unknown',
serverType: ServerType.SUBSONIC,
similarArtists: [],
songCount: null,
userFavorite: false,
@@ -245,8 +247,6 @@ const normalizeAlbum = (
}) || null;
return {
_serverId: server?.id || 'unknown',
_serverType: ServerType.SUBSONIC,
albumArtist: item.artist,
albumArtists: getArtistList(item.artists, item.artistId, item.artist),
artists: [],
@@ -276,6 +276,8 @@ const normalizeAlbum = (
releaseDate: item.year ? new Date(Date.UTC(item.year, 0, 1)).toISOString() : null,
releaseTypes: item.releaseTypes || [],
releaseYear: item.year ? Number(item.year) : null,
serverId: server?.id || 'unknown',
serverType: ServerType.SUBSONIC,
size: null,
songCount: item.songCount,
songs:
@@ -283,6 +285,7 @@ const normalizeAlbum = (
normalizeSong(song, server),
) || [],
tags: null,
uniqueId: nanoid(),
updatedAt: item.created,
userFavorite: item.starred || false,
userRating: item.userRating || null,
@@ -297,8 +300,6 @@ const normalizePlaylist = (
server?: null | ServerListItemWithCredential,
): Playlist => {
return {
_serverId: server?.id || 'unknown',
_serverType: ServerType.SUBSONIC,
description: item.comment || null,
duration: item.duration * 1000,
genres: [],
@@ -315,6 +316,8 @@ const normalizePlaylist = (
owner: item.owner,
ownerId: item.owner,
public: item.public,
serverId: server?.id || 'unknown',
serverType: ServerType.SUBSONIC,
size: null,
songCount: item.songCount,
};
+30 -31
View File
@@ -25,7 +25,7 @@ import {
NDUserListSort,
} from '/@/shared/api/navidrome/navidrome-types';
import { ServerFeatures } from '/@/shared/types/features-types';
import { PlayerRepeat, PlayerShuffle, PlayerStatus, PlayerStyle } from '/@/shared/types/types';
import { PlayerStatus } from '/@/shared/types/types';
export enum LibraryItem {
ALBUM = 'album',
@@ -58,33 +58,29 @@ export type AnyLibraryItems =
| Song[];
export interface PlayerData {
currentSong: QueueSong | undefined;
index: number;
muted: boolean;
nextSong: QueueSong | undefined;
num: 1 | 2;
player1: QueueSong | undefined;
player2: QueueSong | undefined;
previousSong: QueueSong | undefined;
current: {
index: number;
nextIndex?: number;
player: 1 | 2;
previousIndex?: number;
shuffledIndex: number;
song?: QueueSong;
status: PlayerStatus;
};
player1?: QueueSong;
player2?: QueueSong;
queue: QueueData;
queueLength: number;
repeat: PlayerRepeat;
shuffle: PlayerShuffle;
speed: number;
status: PlayerStatus;
transitionType: PlayerStyle;
volume: number;
}
export interface QueueData {
default: string[];
priority: string[];
shuffled: string[];
songs: Record<string, QueueSong>;
current?: QueueSong;
length: number;
next?: QueueSong;
previous?: QueueSong;
}
export type QueueSong = Song & {
_uniqueId: string;
uniqueId: string;
};
export type ServerListItem = {
@@ -165,8 +161,6 @@ export enum ImageType {
}
export type Album = {
_serverId: string;
_serverType: ServerType;
albumArtist: string;
albumArtists: RelatedArtist[];
artists: RelatedArtist[];
@@ -191,10 +185,13 @@ export type Album = {
releaseDate: null | string;
releaseTypes: string[];
releaseYear: null | number;
serverId: string;
serverType: ServerType;
size: null | number;
songCount: null | number;
songs?: Song[];
tags: null | Record<string, string[]>;
uniqueId: string;
updatedAt: string;
userFavorite: boolean;
userRating: null | number;
@@ -202,8 +199,6 @@ export type Album = {
} & { songs?: Song[] };
export type AlbumArtist = {
_serverId: string;
_serverType: ServerType;
albumCount: null | number;
backgroundImageUrl: null | string;
biography: null | string;
@@ -216,6 +211,8 @@ export type AlbumArtist = {
mbz: null | string;
name: string;
playCount: null | number;
serverId: string;
serverType: ServerType;
similarArtists: null | RelatedArtist[];
songCount: null | number;
userFavorite: boolean;
@@ -223,14 +220,15 @@ export type AlbumArtist = {
};
export type Artist = {
_serverId: string;
_serverType: ServerType;
biography: null | string;
createdAt: string;
id: string;
itemType: LibraryItem.ARTIST;
name: string;
remoteCreatedAt: null | string;
serverFolderId: string;
serverId: string;
serverType: ServerType;
updatedAt: string;
};
@@ -299,8 +297,6 @@ export type MusicFolder = {
export type MusicFoldersResponse = MusicFolder[];
export type Playlist = {
_serverId: string;
_serverType: ServerType;
description: null | string;
duration: null | number;
genres: Genre[];
@@ -313,6 +309,8 @@ export type Playlist = {
ownerId: null | string;
public: boolean | null;
rules?: null | Record<string, any>;
serverId: string;
serverType: ServerType;
size: null | number;
songCount: null | number;
sync?: boolean | null;
@@ -330,8 +328,6 @@ export type RelatedArtist = {
};
export type Song = {
_serverId: string;
_serverType: ServerType;
album: null | string;
albumArtists: RelatedArtist[];
albumId: string;
@@ -368,10 +364,13 @@ export type Song = {
releaseDate: null | string;
releaseYear: null | string;
sampleRate: null | number;
serverId: string;
serverType: ServerType;
size: number;
streamUrl: string;
tags: null | Record<string, string[]>;
trackNumber: number;
uniqueId: string;
updatedAt: string;
userFavorite: boolean;
userRating: null | number;
+8 -14
View File
@@ -101,16 +101,20 @@ export enum FontType {
}
export enum Play {
INDEX = 'index',
LAST = 'last',
NEXT = 'next',
NOW = 'now',
SHUFFLE = 'shuffle',
}
export enum PlayerQueueType {
DEFAULT = 'default',
PRIORITY = 'priority',
export enum PlaybackStyle {
CROSSFADE = 'crossfade',
GAPLESS = 'gapless',
}
export enum PlaybackType {
LOCAL = 'local',
WEB = 'web',
}
export enum PlayerRepeat {
@@ -130,16 +134,6 @@ export enum PlayerStatus {
PLAYING = 'playing',
}
export enum PlayerStyle {
CROSSFADE = 'crossfade',
GAPLESS = 'gapless',
}
export enum PlayerType {
LOCAL = 'local',
WEB = 'web',
}
export enum TableColumn {
ACTIONS = 'actions',
ALBUM = 'album',