mirror of
https://github.com/jeffvli/feishin.git
synced 2026-07-17 16:06:27 +02:00
Compare commits
16 Commits
v1.12.1
...
5ac0aaeec0
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ac0aaeec0 | |||
| 515cadb916 | |||
| 4b4d64c7fc | |||
| f7e1198482 | |||
| 7243ed7f15 | |||
| 7e9a78898f | |||
| 6aab8d4121 | |||
| 70594a696b | |||
| 08b4c620f2 | |||
| 7a20cf3853 | |||
| dd186c570f | |||
| d5e9d491b6 | |||
| 28dc822e4f | |||
| def1b1e710 | |||
| 2ff9e4b0a2 | |||
| 49bfc907cd |
@@ -12,7 +12,8 @@ jobs:
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v5
|
||||
- uses: dessant/lock-threads@v6
|
||||
|
||||
with:
|
||||
process-only: 'issues, prs'
|
||||
issue-inactive-days: 120
|
||||
|
||||
@@ -341,7 +341,8 @@
|
||||
"rename": "Reanomena",
|
||||
"newVersionAvailable": "Hi ha una nova versió disponible",
|
||||
"numberOfResults": "{{numberOfResults}} resultats",
|
||||
"back": "Enrere"
|
||||
"back": "Enrere",
|
||||
"openFolder": "Obre la carpeta"
|
||||
},
|
||||
"entity": {
|
||||
"album_one": "Àlbum",
|
||||
@@ -462,7 +463,7 @@
|
||||
"expireInvalid": "La data d'expiració ha de ser al futur",
|
||||
"createFailed": "No s'ha pogut crear el recurs compartit (està habilitat, l'ús compartit?)",
|
||||
"copyToClipboard": "Copiar al porta-retalls: Ctrl+C, enter",
|
||||
"successMustClick": "Compartició creada correctament. Feu clic aquí per obrir-la."
|
||||
"successMustClick": "Compartició creada correctament. Feu clic aquí per obrir-la"
|
||||
},
|
||||
"updateServer": {
|
||||
"success": "S'ha actualitzat el servidor amb èxit",
|
||||
@@ -495,7 +496,12 @@
|
||||
"input_played": "Reprodueix el filtre",
|
||||
"input_played_optionAll": "Totes les pistes",
|
||||
"input_played_optionUnplayed": "Només les pistes sense reproduir",
|
||||
"input_played_optionPlayed": "Només les pistes reproduïdes"
|
||||
"input_played_optionPlayed": "Només les pistes reproduïdes",
|
||||
"input_kind_albums": "Àlbums",
|
||||
"input_kind_songs": "Cançons",
|
||||
"input_kind": "Seleccions a l'atzar",
|
||||
"input_limit_albums": "Quants àlbums?",
|
||||
"input_limit_songs": "Quantes cançons?"
|
||||
},
|
||||
"createRadioStation": {
|
||||
"success": "Emissora de ràdio creada amb èxit",
|
||||
@@ -626,7 +632,7 @@
|
||||
"customCssEnable_description": "Permet escriure CSS personalitzat",
|
||||
"customCssNotice": "Atenció: tot i que hi ha un filtre (no es permet ni URL() ni content:), l'ús de CSS personalitzat pot presentar riscs si canvieu la interfície",
|
||||
"customCss": "Css personalitzat",
|
||||
"customCss_description": "Contingut del CSS personalitzat. Nota: la propietat \"content\" i els urls remots no es permeten. A sota hi teniu una previsualització. Els camps addicionals que no establiu hi apareixin pel filtre",
|
||||
"customCss_description": "Contingut del CSS personalitzat. Nota: la propietat \"content\" i els urls remots no es permeten. A sota hi teniu una previsualització. Els camps addicionals que no establiu hi apareixen a causa de la sanitització. Escriptori: Feishin llegeix i escriu custom.css al directori de configuració de l'aplicació i el recarrega quan el fitxer canvia",
|
||||
"customFontPath": "Ruta de font personalitzada",
|
||||
"customFontPath_description": "Estableix la ruta a una font personalitzada per utilitzar-la a l'aplicació",
|
||||
"discordApplicationId": "ID d'aplicació de {{discord}}",
|
||||
@@ -807,7 +813,7 @@
|
||||
"releaseChannel": "Canal de versions",
|
||||
"releaseChannel_description": "Trieu entre versions estables i beta o alfa (diàries) per les actualitzacions automàtiques",
|
||||
"mediaSession": "Activa media session",
|
||||
"mediaSession_description": "Activa la integració amb Media Session per mostrar els controls multimèdia i les metadades a l'indicador de volum del sistema i la pantalla de bloqueig",
|
||||
"mediaSession_description": "Activa la integració amb Media Session per mostrar els controls multimèdia i les metadades a l'indicador de volum del sistema i la pantalla de bloqueig. Requereix el Reproductor Web d'Àudio.",
|
||||
"crossfadeStyle": "Estil de fosa encadenada",
|
||||
"discordRichPresence": "Estat d'activitat de {{discord}}",
|
||||
"enableAutoTranslation_description": "Activa la traducció automàtica en carregar la lletra",
|
||||
@@ -828,7 +834,7 @@
|
||||
"transcode": "Activa la transcodificació",
|
||||
"autoDJ": "DJ automàtic",
|
||||
"autoDJ_itemCount": "Número d'elements",
|
||||
"autoDJ_itemCount_description": "El nombre d'elements que s'intenten afegir a la cua quan el DJ automàtic està activat",
|
||||
"autoDJ_itemCount_description": "El nombre d'elements que s'intenten afegir a la cua",
|
||||
"autoDJ_timing": "Temps",
|
||||
"autoDJ_timing_description": "El nombre de cançons que han de quedar a la cua per activar el DJ automàtic",
|
||||
"analyticsDisable": "Desactiva les analítiques basades en l'ús",
|
||||
@@ -958,7 +964,16 @@
|
||||
"sidebarPlaylistMode_description": "Com es mostra cada llista de reproducció a la llista de la barra lateral",
|
||||
"sidebarPlaylistMode": "Mode de llista de reproducció a la barra lateral",
|
||||
"sidebarPlaylistMode_optionCompact": "Compacte",
|
||||
"sidebarPlaylistMode_optionExpanded": "Expandit"
|
||||
"sidebarPlaylistMode_optionExpanded": "Expandit",
|
||||
"autoDJ_mode": "Mode",
|
||||
"autoDJ_mode_albums": "Àlbums",
|
||||
"autoDJ_mode_description": "Trieu si voleu afegir cançons o àlbums sencers a la cua",
|
||||
"autoDJ_mode_songs": "Cançons",
|
||||
"autoDJ_enabled": "Activa el DJ automàtic",
|
||||
"autoDJ_albumStrategy": "Mode de selecció d'àlbum",
|
||||
"autoDJ_songStrategy": "Mode de selecció de cançó",
|
||||
"autoDJ_strategy_option_library_random": "A l'atzar",
|
||||
"autoDJ_strategy_option_similar": "Similar"
|
||||
},
|
||||
"table": {
|
||||
"column": {
|
||||
|
||||
@@ -296,7 +296,7 @@
|
||||
"releaseChannel": "Kanál vydání",
|
||||
"releaseChannel_description": "Vyberte si mezi stabilními, beta nebo alpha (nočními) vydáními pro automatické aktualizace",
|
||||
"mediaSession": "Povolit relaci médií",
|
||||
"mediaSession_description": "Povolí integraci do služby Media Session, což zobrazí ovládání a metadata médií v překrytí systémové hlasitosti a na zamykací obrazovce",
|
||||
"mediaSession_description": "Povolí integraci do služby Media Session, což zobrazí ovládání a metadata médií v překrytí systémové hlasitosti a na zamykací obrazovce. Vyžaduje webový přehrávač zvuku.",
|
||||
"exportImportSettings_control_description": "Exportovat a importovat nastavení pomocí souboru JSON",
|
||||
"exportImportSettings_control_exportText": "Exportovat nastavení",
|
||||
"exportImportSettings_control_importText": "Importovat nastavení",
|
||||
|
||||
@@ -1093,7 +1093,7 @@
|
||||
"sidePlayQueueLayout_description": "Sets the layout of the attached side play queue",
|
||||
"sidePlayQueueLayout_optionHorizontal": "Horizontal",
|
||||
"sidePlayQueueLayout_optionVertical": "Vertical",
|
||||
"mediaSession_description": "Enables media session integration, displaying media controls and metadata in the system volume overlay and lock screen",
|
||||
"mediaSession_description": "Enables media session integration, displaying media controls and metadata in the system volume overlay and lock screen. Requires the Web Audio Player.",
|
||||
"mediaSession": "Enable media session",
|
||||
"sidePlayQueueStyle": "Side play queue style",
|
||||
"skipDuration_description": "Sets the duration to skip when using the skip buttons on the player bar",
|
||||
|
||||
@@ -296,7 +296,7 @@
|
||||
"releaseChannel_description": "Elige entre lanzamientos estables, beta, o alpha (nightly) para las actualizaciones automáticas",
|
||||
"artistBackground_description": "Añade una imagen de fondo para las páginas de artistas que contienen el arte de los artistas",
|
||||
"mediaSession": "Activar sesión de medios",
|
||||
"mediaSession_description": "Activa la integración de la sesión de medios, mostrando los controles de medios y los metadatos en la superposición del volumen del sistema y en la pantalla de bloqueo",
|
||||
"mediaSession_description": "Activa la integración de la sesión de medios, mostrando los controles de medios y los metadatos en la superposición del volumen del sistema y en la pantalla de bloqueo. Requiere el Reproductor Web de Audio.",
|
||||
"exportImportSettings_control_description": "Exporta e importa la configuración a través de JSON",
|
||||
"exportImportSettings_control_exportText": "Exportar configuración",
|
||||
"exportImportSettings_control_importText": "Importar configuración",
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
{
|
||||
"action": {
|
||||
"addToFavorites": "Lisa üksusesse $t(entity.favorite, {\"count\": 2})",
|
||||
"addToPlaylist": "Lisa üksusesse $t(entity.playlist, {\"count\": 1})",
|
||||
"addOrRemoveFromSelection": "Lisa või eemalda valikust",
|
||||
"selectRangeOfItems": "Vali mitu üksust korraga",
|
||||
"clearQueue": "Tühjenda järjekord",
|
||||
"goToCurrent": "Mine praeguse üksuse juurde",
|
||||
"collapseAllFolders": "Ahenda kõik kaustad",
|
||||
"expandAllFolders": "Laienda kõik kaustad",
|
||||
"createPlaylist": "Loo $t(entity.playlist, {\"count\": 1})",
|
||||
"createRadioStation": "Loo $t(entity.radioStation, {\"count\": 1})",
|
||||
"deletePlaylist": "Kustuta $t(entity.playlist, {\"count\": 1})",
|
||||
"deleteRadioStation": "Kustuta $t(entity.radioStation, {\"count\": 1})",
|
||||
"selectAll": "Vali kõik",
|
||||
"deselectAll": "Tühista kõigi valik",
|
||||
"downloadStarted": "{{count}} üksuse allalaadimine algas",
|
||||
"editPlaylist": "Muuda $t(entity.playlist, {\"count\": 1})",
|
||||
"goToPage": "Mine lehele",
|
||||
"moveToNext": "Järgmine",
|
||||
"moveToBottom": "Liiguta lõppu",
|
||||
"moveToTop": "Liiguta algusesse",
|
||||
"moveUp": "Liigu üles",
|
||||
"moveDown": "Liigu alla",
|
||||
"holdToMoveToTop": "Hoia algusesse liigutamiseks",
|
||||
"holdToMoveToBottom": "Hoia lõppu liigutamiseks",
|
||||
"moveItems": "Liiguta üksusi",
|
||||
"shuffle": "Sega",
|
||||
"shuffleAll": "Sega kõik",
|
||||
"shuffleSelected": "Sega valitud",
|
||||
"refresh": "$t(common.refresh)",
|
||||
"removeFromFavorites": "Eemalda üksusest $t(entity.favorite, {\"count\": 2})",
|
||||
"removeFromPlaylist": "Eemalda üksusest $t(entity.playlist, {\"count\": 1})",
|
||||
"removeFromQueue": "Eemalda järjekorrast",
|
||||
"setRating": "Hinda",
|
||||
"toggleSmartPlaylistEditor": "Lülita $t(entity.smartPlaylist) redaktor sisse/välja",
|
||||
"viewPlaylists": "Vaata $t(entity.playlist, {\"count\": 2})",
|
||||
"viewMore": "Vaata rohkem",
|
||||
"openApplicationDirectory": "Ava rakenduste kataloog",
|
||||
"openIn": {
|
||||
"lastfm": "Ava Last.fm-is",
|
||||
"listenbrainz": "Ava ListenBrainzis",
|
||||
"musicbrainz": "Ava MusicBrainzis",
|
||||
"qobuz": "Ava Qobuzis",
|
||||
"spotify": "Ava Spotifys"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"countSelected": "{{count}} valitud",
|
||||
"explicitStatus": "Ebasündsa sisu olek",
|
||||
"action_one": "Toiming",
|
||||
"action_other": "Toimingud",
|
||||
"add": "Lisa",
|
||||
"additionalParticipants": "Teised osalejad",
|
||||
"newVersion": "Uus versioon on paigaldatud ({{version}})",
|
||||
"viewReleaseNotes": "Kuva väljalaskemärkmed",
|
||||
"albumGain": "Albumi helitugevus (gain)",
|
||||
"albumPeak": "Albumi tippnivoo",
|
||||
"areYouSure": "Kas oled kindel?",
|
||||
"ascending": "Kasvav",
|
||||
"back": "Tagasi",
|
||||
"backward": "Tagasi",
|
||||
"biography": "Biograafia",
|
||||
"bitDepth": "Bititihedus",
|
||||
"bitrate": "Bitikiirus",
|
||||
"bpm": "BPM",
|
||||
"cancel": "Tühista",
|
||||
"center": "Keskel",
|
||||
"channel_one": "Kanal",
|
||||
"channel_other": "Kanalid",
|
||||
"clear": "Tühjenda",
|
||||
"close": "Sulge",
|
||||
"codec": "Koodek",
|
||||
"collapse": "Ahenda",
|
||||
"comingSoon": "Tulekul…",
|
||||
"configure": "Seadista",
|
||||
"confirm": "Kinnita",
|
||||
"create": "Loo",
|
||||
"currentSong": "Praegune $t(entity.track, {\"count\": 1})",
|
||||
"decrease": "Vähenda",
|
||||
"delete": "Kustuta",
|
||||
"descending": "Kahanev",
|
||||
"description": "Kirjeldus",
|
||||
"disable": "Keela",
|
||||
"disc": "Plaat",
|
||||
"dismiss": "Peida",
|
||||
"doNotShowAgain": "Ära seda enam näita",
|
||||
"duration": "Kestus",
|
||||
"view": "Vaata",
|
||||
"edit": "Muuda",
|
||||
"enable": "Luba",
|
||||
"expand": "Laienda",
|
||||
"example": "Näide",
|
||||
"externalLinks": "Välised lingid",
|
||||
"openFolder": "Ava kaust",
|
||||
"faster": "Kiiremini",
|
||||
"favorite": "Lemmik",
|
||||
"filter_one": "Filter",
|
||||
"filter_other": "Filtrid",
|
||||
"filters": "Filtrid",
|
||||
"filter_multiple": "Mitu",
|
||||
"filter_single": "Üksik",
|
||||
"forceRestartRequired": "Muudatuste rakendamiseks taaskäivita… taaskäivitamiseks sule teavitus",
|
||||
"forward": "Edasi",
|
||||
"gap": "Vahe",
|
||||
"home": "Avaleht",
|
||||
"increase": "Suurenda",
|
||||
"left": "Vasak",
|
||||
"limit": "Limiit",
|
||||
"manage": "Halda",
|
||||
"maximize": "Maksimeeri",
|
||||
"menu": "Menüü",
|
||||
"minimize": "Minimeeri",
|
||||
"modified": "Muudetud",
|
||||
"mbid": "MusicBrainz ID",
|
||||
"grouping": "Rühmitamine",
|
||||
"mood": "Meeleolu",
|
||||
"name": "Nimi",
|
||||
"no": "Ei",
|
||||
"none": "Puudub",
|
||||
"noResultsFromQuery": "Päring ei andnud vasteid",
|
||||
"numberOfResults": "{{numberOfResults}} vastet",
|
||||
"noFilters": "Seadistatud filtreid pole",
|
||||
"note": "Märkus",
|
||||
"ok": "Ok",
|
||||
"owner": "Omanik",
|
||||
"playerMustBePaused": "Mängija peab olema pausil",
|
||||
"preview": "Eelvaade",
|
||||
"previousSong": "Eelmine $t(entity.track, {\"count\": 1})",
|
||||
"private": "Privaatne",
|
||||
"public": "Avalik",
|
||||
"quit": "Välju",
|
||||
"random": "Juhuslik",
|
||||
"rating": "Hinne",
|
||||
"retry": "Proovi uuesti",
|
||||
"recordLabel": "Plaadifirma",
|
||||
"releaseType": "Väljaande tüüp",
|
||||
"refresh": "Värskenda",
|
||||
"reload": "Laadi uuesti",
|
||||
"rename": "Nimeta ümber",
|
||||
"reset": "Lähtesta",
|
||||
"resetToDefault": "Taasta vaikeväärtused",
|
||||
"restartRequired": "Vajalik on taaskäivitamine",
|
||||
"right": "Parem",
|
||||
"sampleRate": "Diskreetimissagedus",
|
||||
"save": "Salvesta",
|
||||
"saveAndReplace": "Salvesta ja asenda",
|
||||
"saveAs": "Salvesta nimega",
|
||||
"search": "Otsi",
|
||||
"setting_one": "Säte",
|
||||
"setting_other": "Sätted",
|
||||
"slower": "Aeglasemalt",
|
||||
"share": "Jaga",
|
||||
"size": "Suurus",
|
||||
"sort": "Järjesta",
|
||||
"sortOrder": "Järjestus",
|
||||
"tags": "Sildid",
|
||||
"title": "Pealkiri",
|
||||
"trackNumber": "Pala",
|
||||
"trackGain": "Pala võimendus",
|
||||
"trackPeak": "Pala tippväärtus",
|
||||
"translation": "Tõlge",
|
||||
"unknown": "Tundmatu",
|
||||
"version": "Versioon",
|
||||
"year": "Aasta",
|
||||
"yes": "Jah",
|
||||
"explicit": "Ebatsensuurne",
|
||||
"clean": "Puhas",
|
||||
"gridRows": "Ruudustiku ridu",
|
||||
"tableColumns": "Tabeli veerge",
|
||||
"itemsMore": "{{count}} veel",
|
||||
"newVersionAvailable": "Saadaval on uus versioon",
|
||||
"path": "Asukoht"
|
||||
},
|
||||
"entity": {
|
||||
"album_one": "Album",
|
||||
"album_other": "Albumid",
|
||||
"albumArtist_one": "Albumi esitaja",
|
||||
"albumArtist_other": "Albumi esitajad",
|
||||
"albumArtistCount_one": "{{count}} albumi esitaja",
|
||||
"albumArtistCount_other": "{{count}} albumi esitajat",
|
||||
"albumWithCount_one": "{{count}} album",
|
||||
"albumWithCount_other": "{{count}} albumit",
|
||||
"radioStation_one": "Raadiojaam",
|
||||
"radioStation_other": "Raadiojaamad",
|
||||
"radioStationWithCount_one": "{{count}} raadiojaam",
|
||||
"radioStationWithCount_other": "{{count}} raadiojaama",
|
||||
"artist_one": "Esitaja",
|
||||
"artist_other": "Esitajad",
|
||||
"artistWithCount_one": "{{count}} esitaja",
|
||||
"artistWithCount_other": "{{count}} esitajat",
|
||||
"favorite_one": "Lemmik",
|
||||
"favorite_other": "Lemmikud",
|
||||
"folder_one": "Kaust",
|
||||
"folder_other": "Kaustad",
|
||||
"folderWithCount_one": "{{count}} kaust",
|
||||
"folderWithCount_other": "{{count}} kausta",
|
||||
"genre_one": "Žanr",
|
||||
"genre_other": "Žanrid",
|
||||
"genreWithCount_one": "{{count}} žanr",
|
||||
"genreWithCount_other": "{{count}} žanrit",
|
||||
"playlist_one": "Esitusloend",
|
||||
"playlist_other": "Esitusloendid",
|
||||
"play_one": "{{count}} esitus",
|
||||
"play_other": "{{count}} esitust",
|
||||
"playlistWithCount_one": "{{count}} esitusloend",
|
||||
"playlistWithCount_other": "{{count}} esitusloendit",
|
||||
"smartPlaylist": "Nutikas $t(entity.playlist, {\"count\": 1})",
|
||||
"track_one": "Rada",
|
||||
"track_other": "Rajad",
|
||||
"song_one": "Lugu",
|
||||
"song_other": "Lood",
|
||||
"trackWithCount_one": "{{count}} rada",
|
||||
"trackWithCount_other": "{{count}} rada"
|
||||
},
|
||||
"error": {
|
||||
"apiRouteError": "Päringut ei saanud edastada",
|
||||
"audioDeviceFetchError": "Heliseadmete hankimisel tekkis viga",
|
||||
"authenticationFailed": "Autentimine nurjus",
|
||||
"badAlbum": "Näed seda lehte, kuna see lugu ei kuulu ühegi albumi alla. Kõige sagedamini juhtub see siis, kui lugu asub sinu muusikakausta juurkataloogis (otse muusikakaustas). Jellyfin rühmitab lood albumiteks ainult siis, kui need asuvad eraldi kaustas",
|
||||
"badValue": "Kehtetu valik \"{{value}}\". Seda väärtust enam ei eksisteeri",
|
||||
"credentialsRequired": "Nõutav on autentimine",
|
||||
"endpointNotImplementedError": "Lõpp-punkt {{endpoint}} pole serveri {{serverType}} puhul toetatud",
|
||||
"genericError": "Tekkis viga",
|
||||
"invalidJson": "Vigane JSON",
|
||||
"invalidServer": "Vigane server",
|
||||
"localFontAccessDenied": "Juurdepääs kohalikele fontidele on keelatud",
|
||||
"loginRateError": "Liiga palju sisselogimiskatseid, proovi mõne sekundi pärast uuesti",
|
||||
"mpvRequired": "Vajalik on MPV",
|
||||
"multipleServerSaveQueueError": "Esitusjärjekorras on lugusi, mis ei pärine praegusest serverist. See pole toetatud",
|
||||
"networkError": "Tekkis võrguviga",
|
||||
"noNetwork": "Server pole saadaval",
|
||||
"noNetworkDescription": "Selle serveriga ei õnnestunud ühendust luua",
|
||||
"notificationDenied": "Märguanded on keelatud. Sellel seadel pole mõju",
|
||||
"openError": "Faili avamine nurjus",
|
||||
"playbackError": "Meedia esitamisel tekkis viga",
|
||||
"playbackPausedDueToError": "Taasesitus peatati vea tõttu",
|
||||
"remoteDisableError": "Kaugserveri toimingu $t(common.disable) käigus tekkis viga",
|
||||
"remoteEnableError": "Tõrge kaugserveri toimingul: $t(common.enable)",
|
||||
"remotePortError": "Kaugserveri pordi määramisel tekkis viga",
|
||||
"remotePortWarning": "Uue pordi rakendamiseks taaskäivita server",
|
||||
"saveQueueFailed": "Järjekorra salvestamine nurjus",
|
||||
"serverLockSingleServer": "Lukustatud serveri korral on lubatud ainult üks server",
|
||||
"serverNotSelectedError": "Serverit pole valitud",
|
||||
"serverRequired": "Vajalik on server",
|
||||
"sessionExpiredError": "Su seanss on aegunud",
|
||||
"systemFontError": "Süsteemifontide hankimisel ilmnes viga",
|
||||
"settingsSyncError": "Esitaja (renderer) ja peaprotsessi seadete vahel leiti lahknevusi. Muudatuste rakendamiseks taaskäivita rakendus"
|
||||
},
|
||||
"filter": {
|
||||
"album": "$t(entity.album, {\"count\": 1})",
|
||||
"albumArtist": "$t(entity.albumArtist, {\"count\": 1})",
|
||||
"matchAnd": "ja",
|
||||
"matchOr": "või",
|
||||
"albumCount": "$t(entity.album, {\"count\": 2}) arv",
|
||||
"artist": "$t(entity.artist, {\"count\": 1})",
|
||||
"biography": "Biograafia",
|
||||
"bitrate": "Bitikiirus",
|
||||
"bpm": "BPM",
|
||||
"channels": "$t(common.channel, {\"count\": 2})",
|
||||
"comment": "Kommentaar",
|
||||
"communityRating": "Kogukonna hinne",
|
||||
"criticRating": "Kriitikute hinne",
|
||||
"dateAdded": "Lisatud",
|
||||
"disc": "Plaat",
|
||||
"duration": "Kestus",
|
||||
"favorited": "Lemmikuks lisatud",
|
||||
"fromYear": "Aastast",
|
||||
"genre": "$t(entity.genre, {\"count\": 1})",
|
||||
"id": "ID",
|
||||
"isCompilation": "on kogumik",
|
||||
"isFavorited": "on lemmik",
|
||||
"isPublic": "on avalik",
|
||||
"isRated": "on hinnatud",
|
||||
"isRecentlyPlayed": "on hiljuti esitatud",
|
||||
"lastPlayed": "Viimati esitatud",
|
||||
"mostPlayed": "Enim esitatud",
|
||||
"name": "Nimi",
|
||||
"note": "Märkus",
|
||||
"owner": "$t(common.owner)",
|
||||
"path": "Asukoht",
|
||||
"playCount": "Esituskordi",
|
||||
"random": "Juhuslik",
|
||||
"rating": "Hinne",
|
||||
"recentlyAdded": "Viimati lisatud",
|
||||
"recentlyPlayed": "Hiljuti esitatud",
|
||||
"recentlyUpdated": "Hiljuti uuendatud",
|
||||
"releaseDate": "Ilmumiskuupäev",
|
||||
"releaseYear": "Ilmumisaasta",
|
||||
"search": "Otsing",
|
||||
"songCount": "Lugude arv"
|
||||
}
|
||||
}
|
||||
@@ -979,7 +979,7 @@
|
||||
"preservePitch": "Utrzymuj ton",
|
||||
"preventSleepOnPlayback_description": "Powstrzymuje ekran przed uśpieniem, gdy muzyka jest odtwarzana",
|
||||
"preventSleepOnPlayback": "Powstrzymuj uśpienie podczas odtwarzania",
|
||||
"mediaSession_description": "Włącza integrację z Media Session, wyświetlając sterowanie mediami i metadane w systemowym oknie zmiany głośności i na ekranie blokady",
|
||||
"mediaSession_description": "Włącza integrację z Media Session, wyświetlając sterowanie mediami i metadane w systemowym oknie zmiany głośności i na ekranie blokad. Wymaga odtwarzacza web audio.",
|
||||
"mediaSession": "Włącz media session",
|
||||
"transcode": "Włącz transkodowanie",
|
||||
"queryBuilder": "Kreator zaptań",
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"action": {
|
||||
"addToFavorites": "Idagdag sa $t(entity.favorite, {\"count\": 2})",
|
||||
"addToPlaylist": "Idagdag sa $t(entity.playlist, {\"count\": 1})",
|
||||
"addOrRemoveFromSelection": "Idagdag o alisin sa pinili",
|
||||
"collapseAllFolders": "Isara lahat ng mga folder",
|
||||
"expandAllFolders": "Buksan lahat ng mga folder",
|
||||
"createPlaylist": "Gumawa $t(entity.playlist, {\"count\": 1})",
|
||||
"createRadioStation": "Gumawa $t(entity.radioStation, {\"count\": 1})",
|
||||
"selectAll": "Piliin lahat",
|
||||
"deselectAll": "Huwag piliin lahat",
|
||||
"downloadStarted": "Nagsimulang mag-dowload ng {{count}} (mga) aytem"
|
||||
}
|
||||
}
|
||||
@@ -169,7 +169,8 @@
|
||||
"tableColumns": "Стовпці таблиці",
|
||||
"itemsMore": "{{count}} більше",
|
||||
"numberOfResults": "{{numberOfResults}} результатів",
|
||||
"newVersionAvailable": "Доступна нова версія"
|
||||
"newVersionAvailable": "Доступна нова версія",
|
||||
"back": "Повернутися"
|
||||
},
|
||||
"entity": {
|
||||
"album_one": "Альбом",
|
||||
|
||||
@@ -666,7 +666,7 @@
|
||||
"preventSleepOnPlayback": "防止播放時進入睡眠狀態",
|
||||
"preventSleepOnPlayback_description": "在音樂播放時防止螢幕進入睡眠狀態",
|
||||
"mediaSession": "啟用 Media Session",
|
||||
"mediaSession_description": "啟用 Media Session 整合功能,於系統音量 Overlay 和鎖定畫面中顯示媒體資料與控制面板",
|
||||
"mediaSession_description": "啟用 Media Session 整合功能,在系統音量疊加層和鎖定畫面上顯示媒體控制項與中繼資料。此功能需要使用網頁播放器。",
|
||||
"releaseChannel": "發佈通道",
|
||||
"analyticsDisable": "選擇退出使用情況分析",
|
||||
"analyticsDisable_description": "經過匿名處理的使用情況資料將傳送給開發者,以協助改進應用程式",
|
||||
|
||||
@@ -141,6 +141,14 @@ ipcMain.on('settings-set', (__event, data: { property: string; value: any }) =>
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('settings-set-sync', (__event, data: { property: string; value: any }) => {
|
||||
if (data.value === null) {
|
||||
store.delete(data.property);
|
||||
} else {
|
||||
store.set(data.property, data.value);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('password-get', (_event, server: string): null | string => {
|
||||
if (safeStorage.isEncryptionAvailable()) {
|
||||
const servers = store.get('server') as Record<string, string> | undefined;
|
||||
|
||||
+60
-11
@@ -252,7 +252,9 @@ function createAlphaUpdaterInstance(): AppImageUpdater | MacUpdater | NsisUpdate
|
||||
return new NsisUpdater(ALPHA_UPDATER_CONFIG);
|
||||
}
|
||||
|
||||
protocol.registerSchemesAsPrivileged([{ privileges: { bypassCSP: true }, scheme: 'feishin' }]);
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{ privileges: { bypassCSP: true, corsEnabled: true }, scheme: 'feishin' },
|
||||
]);
|
||||
|
||||
process.on('uncaughtException', (error: any) => {
|
||||
console.error('Error in main process', error);
|
||||
@@ -989,14 +991,33 @@ app.on('window-all-closed', () => {
|
||||
}
|
||||
});
|
||||
|
||||
const FONT_HEADERS = [
|
||||
const FONT_HEADERS = new Set([
|
||||
'font/collection',
|
||||
'font/otf',
|
||||
'font/sfnt',
|
||||
'font/ttf',
|
||||
'font/woff',
|
||||
'font/woff2',
|
||||
];
|
||||
]);
|
||||
|
||||
const bytesToInt = (array: Uint8Array, length: number): number => {
|
||||
let value = 0;
|
||||
for (let i = 0; i < length; i++) {
|
||||
value = (value << 8) + array[i];
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
const FONT_FOUR_BYTE_MAGIC_NUMBERS = new Set([
|
||||
0x4f54544f, // font/otf
|
||||
0x774f4632, // font/woff2
|
||||
0x774f4646, // font/woff
|
||||
]);
|
||||
|
||||
const FONT_FIVE_BYTE_MAGIC_NUMBERS = new Set([
|
||||
0x0001000000, // ttf, collection, sfnt
|
||||
]);
|
||||
|
||||
const singleInstance = isDevelopment ? true : app.requestSingleInstanceLock();
|
||||
|
||||
@@ -1017,12 +1038,9 @@ if (!singleInstance) {
|
||||
|
||||
app.whenReady()
|
||||
.then(() => {
|
||||
protocol.handle('feishin', async (request) => {
|
||||
const filePath = `file:${request.url.slice('feishin:'.length)}`;
|
||||
const response = await net.fetch(filePath);
|
||||
const contentType = response.headers.get('content-type');
|
||||
|
||||
if (!contentType || !FONT_HEADERS.includes(contentType)) {
|
||||
protocol.handle('feishin', async () => {
|
||||
const filePath = store.get('local_font_path');
|
||||
if (typeof filePath !== 'string') {
|
||||
getMainWindow()?.webContents.send('custom-font-error', filePath);
|
||||
|
||||
return new Response(null, {
|
||||
@@ -1031,7 +1049,38 @@ if (!singleInstance) {
|
||||
});
|
||||
}
|
||||
|
||||
return response;
|
||||
const response = await net.fetch('file:' + filePath);
|
||||
const contentType = response.headers.get('content-type');
|
||||
|
||||
// On Linux, the mime type is included in the response header
|
||||
// In this case, we can forward the response with no further processing
|
||||
if (contentType && FONT_HEADERS.has(contentType)) {
|
||||
return response;
|
||||
}
|
||||
|
||||
// Otherwise, let's check the magic number to see if
|
||||
// the file is a font type. This is either four or five bytes
|
||||
const payload = await response.arrayBuffer();
|
||||
const magicNumber = new Uint8Array(payload.slice(0, 5));
|
||||
const fiveHex = bytesToInt(magicNumber, 5);
|
||||
const fourHex = bytesToInt(magicNumber, 4);
|
||||
|
||||
if (
|
||||
FONT_FIVE_BYTE_MAGIC_NUMBERS.has(fiveHex) ||
|
||||
FONT_FOUR_BYTE_MAGIC_NUMBERS.has(fourHex)
|
||||
) {
|
||||
// We have to create a new response with the payload, since it has been read now
|
||||
return new Response(payload, {
|
||||
headers: response.headers,
|
||||
});
|
||||
}
|
||||
|
||||
getMainWindow()?.webContents.send('custom-font-error', filePath);
|
||||
|
||||
return new Response(null, {
|
||||
status: 403,
|
||||
statusText: 'Forbidden',
|
||||
});
|
||||
});
|
||||
|
||||
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
|
||||
@@ -1039,7 +1088,7 @@ if (!singleInstance) {
|
||||
responseHeaders: {
|
||||
...details.responseHeaders,
|
||||
'Content-Security-Policy': [
|
||||
"script-src 'self' 'unsafe-inline' https://umami.jeffvli.org; style-src 'self' 'unsafe-inline'; media-src 'self' http: https: data: blob:; img-src 'self' http: https: data: blob:; connect-src 'self' http: https: ws: wss:; default-src 'self';",
|
||||
"script-src 'self' 'wasm-unsafe-eval' 'unsafe-inline' https://umami.jeffvli.org; style-src 'self' 'unsafe-inline'; media-src 'self' http: https: data: blob:; img-src 'self' http: https: data: blob:; connect-src 'self' http: https: ws: wss:; default-src 'self';",
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -9,6 +9,13 @@ const set = (
|
||||
ipcRenderer.send('settings-set', { property, value });
|
||||
};
|
||||
|
||||
const setSync = async (
|
||||
property: string,
|
||||
value: boolean | null | Record<string, unknown> | string | string[],
|
||||
) => {
|
||||
return ipcRenderer.invoke('settings-set-sync', { property, value });
|
||||
};
|
||||
|
||||
const get = async (property: string) => {
|
||||
return ipcRenderer.invoke('settings-get', { property });
|
||||
};
|
||||
@@ -99,6 +106,7 @@ export const localSettings = {
|
||||
passwordSet,
|
||||
restart,
|
||||
set,
|
||||
setSync,
|
||||
setZoomFactor,
|
||||
themeSet,
|
||||
};
|
||||
|
||||
@@ -139,6 +139,7 @@ export const utils = {
|
||||
rendererToggleSidebar,
|
||||
rendererUpdateAvailable,
|
||||
saveCustomCss,
|
||||
separator: isWindows() ? '\\' : '/',
|
||||
setInputFocused,
|
||||
startPowerSaveBlocker,
|
||||
stopPowerSaveBlocker,
|
||||
|
||||
@@ -366,7 +366,10 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
query.type === LibraryItem.ALBUM_ARTIST || query.type === LibraryItem.ARTIST
|
||||
? query.id
|
||||
: undefined,
|
||||
id: query.type === LibraryItem.SONG ? query.id : undefined,
|
||||
id:
|
||||
query.type === LibraryItem.SONG || query.type === LibraryItem.PLAYLIST_SONG
|
||||
? query.id
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -419,7 +422,10 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
query.type === LibraryItem.ALBUM_ARTIST || query.type === LibraryItem.ARTIST
|
||||
? query.id
|
||||
: undefined,
|
||||
id: query.type === LibraryItem.SONG ? query.id : undefined,
|
||||
id:
|
||||
query.type === LibraryItem.SONG || query.type === LibraryItem.PLAYLIST_SONG
|
||||
? query.id
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2327,7 +2333,6 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
case 'start':
|
||||
state = 'starting';
|
||||
break;
|
||||
case 'timeupdate':
|
||||
case 'unpause':
|
||||
state = 'playing';
|
||||
break;
|
||||
|
||||
@@ -4,50 +4,38 @@ import { generatePath, useNavigate } from 'react-router';
|
||||
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { ContextMenu } from '/@/shared/components/context-menu/context-menu';
|
||||
import {
|
||||
Album,
|
||||
AlbumArtist,
|
||||
Artist,
|
||||
LibraryItem,
|
||||
QueueSong,
|
||||
Song,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { Album, LibraryItem, QueueSong, Song } from '/@/shared/types/domain-types';
|
||||
|
||||
interface GoToActionProps {
|
||||
items: Album[] | AlbumArtist[] | Artist[] | QueueSong[] | Song[];
|
||||
items: Album[] | QueueSong[] | Song[];
|
||||
}
|
||||
|
||||
export const GoToAction = ({ items }: GoToActionProps) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { albumArtists, albumId } = useMemo(() => {
|
||||
const { albumId, artists } = useMemo(() => {
|
||||
const firstItem = items[0];
|
||||
|
||||
if (firstItem._itemType === LibraryItem.ALBUM) {
|
||||
return {
|
||||
albumArtists: firstItem.albumArtists || [],
|
||||
albumId: firstItem.id,
|
||||
};
|
||||
} else if (firstItem._itemType === LibraryItem.SONG) {
|
||||
return {
|
||||
albumArtists: firstItem.albumArtists || [],
|
||||
albumId: firstItem.albumId,
|
||||
};
|
||||
} else if (
|
||||
firstItem._itemType === LibraryItem.ARTIST ||
|
||||
firstItem._itemType === LibraryItem.ALBUM_ARTIST
|
||||
) {
|
||||
return {
|
||||
albumArtists: [{ id: firstItem.id, name: firstItem.name }],
|
||||
albumId: null,
|
||||
};
|
||||
switch (firstItem._itemType) {
|
||||
case LibraryItem.ALBUM:
|
||||
return {
|
||||
albumId: firstItem.id,
|
||||
artists: firstItem.albumArtists || [],
|
||||
};
|
||||
case LibraryItem.SONG:
|
||||
return {
|
||||
albumId: firstItem.albumId,
|
||||
artists:
|
||||
(firstItem.artists?.length ? firstItem.artists : firstItem.albumArtists) ||
|
||||
[],
|
||||
};
|
||||
default:
|
||||
return {
|
||||
albumId: null,
|
||||
artists: [],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
albumArtists: [],
|
||||
albumId: null,
|
||||
};
|
||||
}, [items]);
|
||||
|
||||
const handleGoToAlbum = useCallback(() => {
|
||||
@@ -55,7 +43,7 @@ export const GoToAction = ({ items }: GoToActionProps) => {
|
||||
navigate(generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId }));
|
||||
}, [albumId, navigate]);
|
||||
|
||||
const handleGoToAlbumArtist = useCallback(
|
||||
const handleGoToArtist = useCallback(
|
||||
(albumArtistId: string) => {
|
||||
navigate(generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, { albumArtistId }));
|
||||
},
|
||||
@@ -81,13 +69,13 @@ export const GoToAction = ({ items }: GoToActionProps) => {
|
||||
{t('page.contextMenu.goToAlbum')}
|
||||
</ContextMenu.Item>
|
||||
)}
|
||||
{albumArtists.map((albumArtist) => (
|
||||
{artists.map((artist) => (
|
||||
<ContextMenu.Item
|
||||
key={albumArtist.id}
|
||||
key={artist.id}
|
||||
leftIcon="artist"
|
||||
onSelect={() => handleGoToAlbumArtist(albumArtist.id)}
|
||||
onSelect={() => handleGoToArtist(artist.id)}
|
||||
>
|
||||
{`${t('page.contextMenu.goTo')} ${albumArtist.name}`}
|
||||
{`${t('page.contextMenu.goTo')} ${artist.name}`}
|
||||
</ContextMenu.Item>
|
||||
))}
|
||||
</ContextMenu.SubmenuContent>
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useMemo } from 'react';
|
||||
import { AddToPlaylistAction } from '/@/renderer/features/context-menu/actions/add-to-playlist-action';
|
||||
import { DownloadAction } from '/@/renderer/features/context-menu/actions/download-action';
|
||||
import { GetInfoAction } from '/@/renderer/features/context-menu/actions/get-info-action';
|
||||
import { GoToAction } from '/@/renderer/features/context-menu/actions/go-to-action';
|
||||
import { PlayAction } from '/@/renderer/features/context-menu/actions/play-action';
|
||||
import { PlayArtistRadioAction } from '/@/renderer/features/context-menu/actions/play-artist-radio-action';
|
||||
import { SetFavoriteAction } from '/@/renderer/features/context-menu/actions/set-favorite-action';
|
||||
@@ -39,8 +38,6 @@ export const AlbumArtistContextMenu = ({ items, type }: AlbumArtistContextMenuPr
|
||||
<DownloadAction ids={ids} />
|
||||
<ShareAction ids={ids} itemType={LibraryItem.ALBUM_ARTIST} />
|
||||
<ContextMenu.Divider />
|
||||
<GoToAction items={items} />
|
||||
<ContextMenu.Divider />
|
||||
<GetInfoAction disabled={items.length === 0} items={items} />
|
||||
</ContextMenu.Content>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useMemo } from 'react';
|
||||
import { AddToPlaylistAction } from '/@/renderer/features/context-menu/actions/add-to-playlist-action';
|
||||
import { DownloadAction } from '/@/renderer/features/context-menu/actions/download-action';
|
||||
import { GetInfoAction } from '/@/renderer/features/context-menu/actions/get-info-action';
|
||||
import { GoToAction } from '/@/renderer/features/context-menu/actions/go-to-action';
|
||||
import { PlayAction } from '/@/renderer/features/context-menu/actions/play-action';
|
||||
import { PlayArtistRadioAction } from '/@/renderer/features/context-menu/actions/play-artist-radio-action';
|
||||
import { SetFavoriteAction } from '/@/renderer/features/context-menu/actions/set-favorite-action';
|
||||
@@ -39,8 +38,6 @@ export const ArtistContextMenu = ({ items, type }: ArtistContextMenuProps) => {
|
||||
<DownloadAction ids={ids} />
|
||||
<ShareAction ids={ids} itemType={LibraryItem.ARTIST} />
|
||||
<ContextMenu.Divider />
|
||||
<GoToAction items={items} />
|
||||
<ContextMenu.Divider />
|
||||
<GetInfoAction disabled={items.length === 0} items={items} />
|
||||
</ContextMenu.Content>
|
||||
);
|
||||
|
||||
@@ -19,6 +19,8 @@ import {
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import { PlayerStatus } from '/@/shared/types/types';
|
||||
|
||||
let startupRestoreSessionHandled = false;
|
||||
|
||||
export const useQueueRestoreTimestamp = () => {
|
||||
const { mediaSeekToTimestamp } = usePlayerActions();
|
||||
|
||||
@@ -51,28 +53,65 @@ export const useInitialTimestampRestore = () => {
|
||||
|
||||
const startupRestoreInitializedRef = useRef(false);
|
||||
const startupSeekArmedRef = useRef<null | number>(null);
|
||||
const startupSeekTargetUniqueIdRef = useRef<null | string>(null);
|
||||
const startupSeekAppliedRef = useRef(false);
|
||||
|
||||
const applyStartupSeek = useCallback(() => {
|
||||
const cancelStartupSeek = useCallback(() => {
|
||||
if (startupSeekAppliedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const seekTimestamp = startupSeekArmedRef.current;
|
||||
if (!seekTimestamp || seekTimestamp <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
startupSeekAppliedRef.current = true;
|
||||
startupSeekArmedRef.current = null;
|
||||
startupSeekTargetUniqueIdRef.current = null;
|
||||
}, []);
|
||||
|
||||
const applyStartupSeek = useCallback(() => {
|
||||
const seekTimestamp = startupSeekArmedRef.current;
|
||||
|
||||
if (startupSeekAppliedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!seekTimestamp || seekTimestamp <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetUniqueId = startupSeekTargetUniqueIdRef.current;
|
||||
const currentUniqueId = usePlayerStore.getState().getQueue().items[
|
||||
usePlayerStore.getState().player.index
|
||||
]?._uniqueId;
|
||||
|
||||
if (targetUniqueId && currentUniqueId !== targetUniqueId) {
|
||||
cancelStartupSeek();
|
||||
return;
|
||||
}
|
||||
|
||||
startupSeekAppliedRef.current = true;
|
||||
startupSeekArmedRef.current = null;
|
||||
startupSeekTargetUniqueIdRef.current = null;
|
||||
|
||||
setTimeout(() => {
|
||||
mediaSeekToTimestamp(seekTimestamp);
|
||||
}, 100);
|
||||
}, [mediaSeekToTimestamp]);
|
||||
}, [cancelStartupSeek, mediaSeekToTimestamp]);
|
||||
|
||||
useEffect(() => {
|
||||
if (startupRestoreInitializedRef.current) {
|
||||
const targetUniqueId = startupSeekTargetUniqueIdRef.current;
|
||||
if (
|
||||
!targetUniqueId ||
|
||||
startupSeekAppliedRef.current ||
|
||||
!currentSong ||
|
||||
currentSong._uniqueId === targetUniqueId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
cancelStartupSeek();
|
||||
}, [cancelStartupSeek, currentSong]);
|
||||
|
||||
useEffect(() => {
|
||||
if (startupRestoreInitializedRef.current || startupRestoreSessionHandled) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -81,9 +120,11 @@ export const useInitialTimestampRestore = () => {
|
||||
}
|
||||
|
||||
startupRestoreInitializedRef.current = true;
|
||||
startupRestoreSessionHandled = true;
|
||||
|
||||
if (timestamp > 0) {
|
||||
startupSeekArmedRef.current = timestamp;
|
||||
startupSeekTargetUniqueIdRef.current = currentSong._uniqueId;
|
||||
}
|
||||
|
||||
if (playerStatus === PlayerStatus.PLAYING) {
|
||||
|
||||
@@ -45,7 +45,7 @@ const getPositionValue = (seconds: number, useTicks: boolean) => {
|
||||
return Math.round(seconds * 1e7);
|
||||
}
|
||||
|
||||
return seconds;
|
||||
return seconds * 1000;
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -459,12 +459,14 @@ export const useScrobble = () => {
|
||||
lastProgressEventRef.current = properties.timestamp;
|
||||
lastSeekEventRef.current = now;
|
||||
|
||||
const currentStatus = usePlayerStore.getState().player.status;
|
||||
|
||||
sendScrobble.mutate(
|
||||
{
|
||||
apiClientProps: { serverId: currentSong._serverId || '' },
|
||||
query: {
|
||||
albumId: currentSong.albumId,
|
||||
event: 'timeupdate',
|
||||
event: currentStatus === PlayerStatus.PLAYING ? 'unpause' : 'pause',
|
||||
id: currentSong.id,
|
||||
mediaType: mediaType,
|
||||
playbackRate: playbackRate,
|
||||
|
||||
@@ -36,6 +36,7 @@ import { FontType } from '/@/shared/types/types';
|
||||
|
||||
const localSettings = isElectron() ? window.api.localSettings : null;
|
||||
const ipc = isElectron() ? window.api.ipc : null;
|
||||
const utils = isElectron() ? window.api.utils : null;
|
||||
// Electron 32+ removed file.path, use this which is exposed in preload to get real path
|
||||
const getPathForFile = isElectron() ? window.api.getPathForFile : null;
|
||||
|
||||
@@ -289,21 +290,29 @@ export const ApplicationSettings = memo(() => {
|
||||
control: (
|
||||
<FileInput
|
||||
accept=".ttc,.ttf,.otf,.woff,.woff2"
|
||||
onChange={(e) =>
|
||||
clearable
|
||||
defaultValue={
|
||||
fontSettings.custom
|
||||
? new File([], fontSettings.custom.split(utils?.separator || '').pop()!)
|
||||
: null
|
||||
}
|
||||
onChange={async (e) => {
|
||||
const custom = e ? getPathForFile?.(e) || null : null;
|
||||
await localSettings?.setSync('local_font_path', custom);
|
||||
setSettings({
|
||||
font: {
|
||||
...fontSettings,
|
||||
custom: e ? getPathForFile?.(e) || null : null,
|
||||
custom,
|
||||
},
|
||||
})
|
||||
}
|
||||
});
|
||||
}}
|
||||
w={300}
|
||||
/>
|
||||
),
|
||||
description: t('setting.customFontPath', {
|
||||
context: 'description',
|
||||
}),
|
||||
isHidden: fontSettings.type !== FontType.CUSTOM,
|
||||
isHidden: !isElectron() || fontSettings.type !== FontType.CUSTOM,
|
||||
title: t('setting.customFontPath'),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -32,6 +32,7 @@ export const useSyncSettingsToMain = () => {
|
||||
const settingsFromStore = useSettingsStore.getState();
|
||||
|
||||
const settings = {
|
||||
font: settingsFromStore.font,
|
||||
general: settingsFromStore.general,
|
||||
hotkeys: settingsFromStore.hotkeys,
|
||||
lyrics: settingsFromStore.lyrics,
|
||||
@@ -101,6 +102,10 @@ export const useSyncSettingsToMain = () => {
|
||||
mainStoreKey: 'enableNeteaseTranslation',
|
||||
rendererValue: settings.lyrics.enableNeteaseTranslation,
|
||||
},
|
||||
{
|
||||
mainStoreKey: 'local_font_path',
|
||||
rendererValue: settings.font.custom,
|
||||
},
|
||||
];
|
||||
|
||||
// Compare and sync each setting
|
||||
|
||||
@@ -1185,6 +1185,9 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
|
||||
});
|
||||
},
|
||||
mediaSeekToTimestamp: (timestamp: number) => {
|
||||
// See mediaSkipBackward: update the timestamp store right away to
|
||||
// avoid the stale-read left by the ~500ms engine poll.
|
||||
setTimestampStore(timestamp);
|
||||
set((state) => {
|
||||
state.player.seekToTimestamp = uniqueSeekToTimestamp(timestamp);
|
||||
});
|
||||
@@ -1196,6 +1199,11 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
|
||||
const currentTimestamp = useTimestampStoreBase.getState().timestamp;
|
||||
const newTimestamp = Math.max(0, currentTimestamp - timeToSkip);
|
||||
|
||||
// Update the timestamp store right away so the UI and any
|
||||
// subsequent seek compute from the new position instead of the
|
||||
// stale value left by the ~500ms engine poll (otherwise mashing
|
||||
// the seek keys repeatedly lands on the same time).
|
||||
setTimestampStore(newTimestamp);
|
||||
set((state) => {
|
||||
state.player.seekToTimestamp = uniqueSeekToTimestamp(newTimestamp);
|
||||
});
|
||||
@@ -1217,6 +1225,9 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
|
||||
const currentTimestamp = useTimestampStoreBase.getState().timestamp;
|
||||
const newTimestamp = Math.min(duration - 1, currentTimestamp + timeToSkip);
|
||||
|
||||
// See mediaSkipBackward: update the timestamp store right away to
|
||||
// avoid the stale-read left by the ~500ms engine poll.
|
||||
setTimestampStore(newTimestamp);
|
||||
set((state) => {
|
||||
state.player.seekToTimestamp = uniqueSeekToTimestamp(newTimestamp);
|
||||
});
|
||||
|
||||
@@ -134,6 +134,11 @@ export const useAppTheme = (overrideTheme?: AppTheme) => {
|
||||
document.body.appendChild(textStyleRef.current);
|
||||
}
|
||||
|
||||
// Note: we change the url to bust caches when changing the path
|
||||
// The url provided here does NOT matter, validation is done
|
||||
// on the main process. Any feishin:/ url will fetch the same
|
||||
// item, which the renderer will check via magic number to be
|
||||
// some font item
|
||||
textStyleRef.current.textContent = `
|
||||
@font-face {
|
||||
font-family: "dynamic-font";
|
||||
|
||||
@@ -1363,7 +1363,7 @@ export type ScrobbleArgs = BaseEndpointArgs & {
|
||||
|
||||
export type ScrobbleQuery = {
|
||||
albumId?: string;
|
||||
event?: 'pause' | 'start' | 'timeupdate' | 'unpause';
|
||||
event?: 'pause' | 'start' | 'unpause';
|
||||
id: string;
|
||||
mediaType: 'podcast' | 'song';
|
||||
playbackRate: number;
|
||||
|
||||
@@ -2,6 +2,20 @@ import type { HotkeyItem } from '@mantine/hooks';
|
||||
|
||||
const RESERVED_KEYS = new Set(['alt', 'ctrl', 'meta', 'mod', 'shift']);
|
||||
|
||||
const PUNCTUATION_KEY_TO_PHYSICAL: Record<string, string> = {
|
||||
"'": 'Quote',
|
||||
',': 'Comma',
|
||||
'-': 'Minus',
|
||||
'.': 'Period',
|
||||
'/': 'Slash',
|
||||
';': 'Semicolon',
|
||||
'=': 'Equal',
|
||||
'[': 'BracketLeft',
|
||||
'\\': 'Backslash',
|
||||
']': 'BracketRight',
|
||||
'`': 'Backquote',
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts stored hotkey strings to Mantine's physical-key format.
|
||||
* Mantine matches KeyboardEvent.code via normalizeKey, which turns Digit1 into
|
||||
@@ -25,6 +39,11 @@ export const toPhysicalHotkey = (hotkey: string): string =>
|
||||
return `Digit${part}`;
|
||||
}
|
||||
|
||||
const punctuationPhysical = PUNCTUATION_KEY_TO_PHYSICAL[part];
|
||||
if (punctuationPhysical) {
|
||||
return punctuationPhysical;
|
||||
}
|
||||
|
||||
return part;
|
||||
})
|
||||
.join('+');
|
||||
@@ -33,5 +52,5 @@ export const withPhysicalKeys = (hotkeys: HotkeyItem[]): HotkeyItem[] =>
|
||||
hotkeys.map(([hotkey, handler, options]) => [
|
||||
toPhysicalHotkey(hotkey),
|
||||
handler,
|
||||
{ ...options, usePhysicalKeys: true },
|
||||
{ ...options, preventDefault: true, usePhysicalKeys: true },
|
||||
]);
|
||||
|
||||
@@ -3,7 +3,12 @@ const CODE_TO_HOTKEY_KEY: Record<string, string> = {
|
||||
ArrowLeft: 'arrowleft',
|
||||
ArrowRight: 'arrowright',
|
||||
ArrowUp: 'arrowup',
|
||||
Backquote: '`',
|
||||
Backslash: '\\',
|
||||
Backspace: 'backspace',
|
||||
BracketLeft: '[',
|
||||
BracketRight: ']',
|
||||
Comma: ',',
|
||||
Delete: 'delete',
|
||||
End: 'end',
|
||||
Enter: 'enter',
|
||||
@@ -14,6 +19,10 @@ const CODE_TO_HOTKEY_KEY: Record<string, string> = {
|
||||
Minus: 'minus',
|
||||
PageDown: 'pagedown',
|
||||
PageUp: 'pageup',
|
||||
Period: '.',
|
||||
Quote: "'",
|
||||
Semicolon: ';',
|
||||
Slash: '/',
|
||||
Space: 'space',
|
||||
Tab: 'tab',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user