mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ed1960c469 | |||
| dc9e181c54 | |||
| 4a91fa04e7 | |||
| af3e5d1a56 | |||
| 7b4e6340ca | |||
| 2e3febf66a | |||
| a5ff3482e5 | |||
| 8de279ac90 | |||
| 0efc56f8d6 | |||
| b59c37ebbd | |||
| ff194e724b | |||
| ca636c86e6 | |||
| 8b1117543e | |||
| ef123d8513 | |||
| 5aac3ba47e | |||
| d2f3bdf29a | |||
| e70313f66a | |||
| 70926283ff | |||
| 8357c09e9f | |||
| 21f97df788 | |||
| 00d6d7de30 | |||
| b10900618a | |||
| 9b8d6ddda0 | |||
| 4e6948693d | |||
| 6e3c65cced | |||
| c499fb83e7 | |||
| be347dc6ba | |||
| 716f068adf | |||
| e9d2cb1482 | |||
| 132394524e | |||
| 20499b821e | |||
| 803842d579 | |||
| eb119a31f0 | |||
| 9d4acea654 | |||
| 6a2f982292 | |||
| 7c88ea46e0 | |||
| 0f3de55e9e | |||
| d5b7dff7a9 | |||
| 7468554ea0 | |||
| 0d93c984be | |||
| 50322b69cc | |||
| fc212cecb4 | |||
| 3d6a9b59bf |
@@ -59,6 +59,7 @@ module.exports = {
|
||||
"react/function-component-definition": "off",
|
||||
"jsx-a11y/control-has-associated-label": "off",
|
||||
"react/button-has-type": "off",
|
||||
"no-labels": ["error", { "allowLoop": true }]
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020,
|
||||
|
||||
@@ -108,6 +108,11 @@
|
||||
{
|
||||
"username": "Taurus Arcade",
|
||||
"type": "gold"
|
||||
}
|
||||
|
||||
},
|
||||
{
|
||||
"username": "Mozz_Zm"
|
||||
},
|
||||
{
|
||||
"username": "clapxz"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -52,8 +52,9 @@
|
||||
"tabs": {
|
||||
"maps": {
|
||||
"actions": {
|
||||
"add-maps": {
|
||||
"text": "Hinzufügen"
|
||||
"drop-down": {
|
||||
"browse-maps": "Karten durchsuchen",
|
||||
"import-maps": "Karten importieren"
|
||||
},
|
||||
"link-maps": {
|
||||
"tooltips": {
|
||||
@@ -65,6 +66,21 @@
|
||||
"empty-maps": {
|
||||
"text": "Keine Maps",
|
||||
"button": "Maps herunterladen"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "Deine Karten importieren",
|
||||
"subtext": "Lasse deine ZIP-Dateien hier fallen, um deine Karten zu importieren"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
"drop-down": {
|
||||
"browse-playlists": "Wiedergabelisten durchsuchen",
|
||||
"create-a-playlist": "Eine Wiedergabeliste erstellen",
|
||||
"import-playlists": "Wiedergabelisten importieren"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "Importiere deine Wiedergabelisten",
|
||||
"subtext": "Ziehe deine \".bplist\" oder \".json\" Dateien hierhin, um sie zu importieren"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -250,11 +266,13 @@
|
||||
"errors": {
|
||||
"titles": {
|
||||
"operation-running": "Vorgang läuft",
|
||||
"no-internet": "Kein Internet"
|
||||
"no-internet": "Kein Internet",
|
||||
"file-not-supported": "Datei wird nicht unterstützt"
|
||||
},
|
||||
"msg": {
|
||||
"operation-running": "Warte auf das Ende des aktuellen Vorgangs und versuche es erneut.",
|
||||
"no-internet": "Überprüfe deine Verbindung und versuche es erneut."
|
||||
"no-internet": "Überprüfe deine Verbindung und versuche es erneut.",
|
||||
"file-not-supported": "Nur {types}-Dateien werden unterstützt."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -505,6 +523,19 @@
|
||||
"duplicates-maps-deleted": {
|
||||
"title": "Duplikate gelöscht",
|
||||
"msg": "Duplikate wurden gelöscht"
|
||||
},
|
||||
"import-map": {
|
||||
"titles": {
|
||||
"success": "Kartenimport abgeschlossen",
|
||||
"error": "Beim Kartenimport ist ein Fehler aufgetreten"
|
||||
},
|
||||
"msgs": {
|
||||
"success": "Die Karten wurden erfolgreich importiert.",
|
||||
"some-success": "Einige Karten wurden erfolgreich importiert.",
|
||||
"only-accept-zip": "Nur Zip-Dateien werden unterstützt.",
|
||||
"invalid-zip": "Die Zip-Datei(en) enthalten keine Karten.",
|
||||
"unknown": "Ein unbekannter Fehler ist aufgetreten."
|
||||
}
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -813,42 +844,42 @@
|
||||
"tech": "Tech"
|
||||
},
|
||||
"map-styles": {
|
||||
"dance": "Tanz",
|
||||
"swing": "Swing",
|
||||
"nightcore": "Nightcore",
|
||||
"folk": "Folk",
|
||||
"family": "Familie",
|
||||
"ambient": "Umgebung",
|
||||
"funk": "Funk",
|
||||
"jazz": "Jazz",
|
||||
"soul": "Soul",
|
||||
"speedcore": "Speedcore",
|
||||
"punk": "Punk",
|
||||
"rb": "R&B",
|
||||
"holiday": "Urlaub",
|
||||
"vocaloid": "Vokaloid",
|
||||
"j-rock": "J-Rock",
|
||||
"trance": "Trance",
|
||||
"drumbass": "Basstrommel",
|
||||
"comedy": "Komödie",
|
||||
"instrumental": "Instrumental",
|
||||
"hardcore": "Hardcore",
|
||||
"k-pop": "K-Pop",
|
||||
"indie": "Indie",
|
||||
"techno": "Techno",
|
||||
"house": "House",
|
||||
"game": "Videospiele",
|
||||
"film": "Filme",
|
||||
"alt": "Alternativ",
|
||||
"dubstep": "Dubstep",
|
||||
"metal": "Metal",
|
||||
"anime": "Anime",
|
||||
"hiphop": "HipHop",
|
||||
"j-pop": "J-Pop",
|
||||
"rock": "Rock",
|
||||
"pop": "Pop",
|
||||
"electronic": "Elektronisch",
|
||||
"classical-orchestral": "Klassisch & Orchester"
|
||||
"dance": "tanz",
|
||||
"swing": "swing",
|
||||
"nightcore": "nightcore",
|
||||
"folk-acoustic": "folk & akustisch",
|
||||
"kids-family": "kinder & familie",
|
||||
"ambient": "ambient",
|
||||
"funk-disco": "funk & disco",
|
||||
"jazz": "jazz",
|
||||
"soul": "soul",
|
||||
"speedcore": "speedcore",
|
||||
"punk": "punk",
|
||||
"rb": "r&b",
|
||||
"holiday": "urlaub",
|
||||
"vocaloid": "vocaloid",
|
||||
"j-rock": "j-rock",
|
||||
"trance": "trance",
|
||||
"drum-and-bass": "drum & bass",
|
||||
"comedy-meme": "komödie & meme",
|
||||
"instrumental": "instrumental",
|
||||
"hardcore": "hardcore",
|
||||
"k-pop": "k-pop",
|
||||
"indie": "indie",
|
||||
"techno": "techno",
|
||||
"house": "house",
|
||||
"video-game-soundtrack": "videospiel",
|
||||
"tv-movie-soundtrack": "TV & film",
|
||||
"alternative": "alternativ",
|
||||
"dubstep": "dubstep",
|
||||
"metal": "metal",
|
||||
"anime": "anime",
|
||||
"hip-hop-rap": "hip hop & rap",
|
||||
"j-pop": "j-pop",
|
||||
"rock": "rock",
|
||||
"pop": "pop",
|
||||
"electronic": "elektronisch",
|
||||
"classical-orchestral": "Klassisch & Orchestral"
|
||||
},
|
||||
"map-specificities": {
|
||||
"automapper": "KI",
|
||||
@@ -875,7 +906,8 @@
|
||||
"bsr-code" : "BSR Code",
|
||||
"download" : "Karte herunterladen",
|
||||
"downloading" :"Karte wird heruntergeladen",
|
||||
"cancel-download" : "Download abbrechen"
|
||||
"cancel-download" : "Download abbrechen",
|
||||
"hightlight-difficulty": "Schwierigkeit hervorheben"
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
@@ -1067,6 +1099,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"drop-zone": {
|
||||
"or-browse-files": "Oder Dateien durchsuchen"
|
||||
},
|
||||
"playlist": {
|
||||
"error-playlist-creation-title": "Fehler beim Erstellen der Playlist",
|
||||
"error-playlist-creation-desc": "Beim Erstellen der Playlist ist ein Fehler aufgetreten.",
|
||||
@@ -1181,6 +1216,24 @@
|
||||
"last-week": "Letzte Woche",
|
||||
"last-month": "Letzter Monat",
|
||||
"3-last-month": "Letzte 3 Monate"
|
||||
},
|
||||
"playlists-imported": "Wiedergabelisten importiert",
|
||||
"all-playlists-have-been-successfully-imported": "Alle Wiedergabelisten wurden erfolgreich importiert",
|
||||
"no-playlist-found": "Keine Wiedergabeliste gefunden",
|
||||
"no-playlist-found-in-selected-files": "Keine Wiedergabeliste in den ausgewählten Dateien gefunden",
|
||||
"some-playlists-not-imported": "Einige Wiedergabelisten wurden nicht importiert",
|
||||
"some-playlists-have-been-imported": {
|
||||
"INVALID_SOURCE": "Einige Wiedergabelisten konnten nicht gefunden werden",
|
||||
"INVALID_PLAYLIST_FILE": "Einige Wiedergabelisten sind ungültig",
|
||||
"CANNOT_PARSE_PLAYLIST": "Einige Wiedergabelisten sind nicht lesbar",
|
||||
"unknown": "Einige Wiedergabelisten konnten nicht importiert werden"
|
||||
},
|
||||
"no-playlists-imported": "Keine Wiedergabelisten importiert",
|
||||
"no-playlists-imported-errors": {
|
||||
"INVALID_SOURCE": "Wiedergabelisten konnten nicht gefunden werden",
|
||||
"INVALID_PLAYLIST_FILE": "Wiedergabelisten sind ungültig",
|
||||
"CANNOT_PARSE_PLAYLIST": "Wiedergabelisten sind nicht lesbar",
|
||||
"unknown": "Es konnte keine Wiedergabeliste importiert werden"
|
||||
}
|
||||
},
|
||||
"dateformat": {
|
||||
|
||||
@@ -52,8 +52,9 @@
|
||||
"tabs": {
|
||||
"maps": {
|
||||
"actions": {
|
||||
"add-maps": {
|
||||
"text": "Add"
|
||||
"drop-down": {
|
||||
"browse-maps": "Browse maps",
|
||||
"import-maps": "Import maps"
|
||||
},
|
||||
"link-maps": {
|
||||
"tooltips": {
|
||||
@@ -65,6 +66,21 @@
|
||||
"empty-maps": {
|
||||
"text": "No maps",
|
||||
"button": "Download maps"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "Import your maps",
|
||||
"subtext": "Drop your zip files here to import your maps"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
"drop-down": {
|
||||
"browse-playlists": "Browse playlists",
|
||||
"create-a-playlist": "Create a playlist",
|
||||
"import-playlists": "Import playlists"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "Import your playlists",
|
||||
"subtext": "Drop your \".bplist\" or \".json\" files here to import them"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -255,11 +271,13 @@
|
||||
"errors": {
|
||||
"titles": {
|
||||
"operation-running": "Operation running",
|
||||
"no-internet": "No internet"
|
||||
"no-internet": "No internet",
|
||||
"file-not-supported": "File not supported"
|
||||
},
|
||||
"msg": {
|
||||
"operation-running": "Wait for the current operation to finish, then try again.",
|
||||
"no-internet": "Check your connection and try again."
|
||||
"no-internet": "Check your connection and try again.",
|
||||
"file-not-supported": "Only {types} files are supported."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -512,6 +530,19 @@
|
||||
"duplicates-maps-deleted": {
|
||||
"title": "Duplicates Deleted",
|
||||
"msg": "Duplicates were deleted"
|
||||
},
|
||||
"import-map": {
|
||||
"titles": {
|
||||
"success": "Maps import completed",
|
||||
"error": "An error occurred during the maps import"
|
||||
},
|
||||
"msgs": {
|
||||
"success": "Maps successfully imported.",
|
||||
"some-success": "Some maps were successfully imported.",
|
||||
"only-accept-zip": "Only zip files are supported.",
|
||||
"invalid-zip": "The zip file(s) do not contain any maps.",
|
||||
"unknown": "An unknown error occurred."
|
||||
}
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -823,10 +854,10 @@
|
||||
"dance": "dance",
|
||||
"swing": "swing",
|
||||
"nightcore": "nightcore",
|
||||
"folk": "folk",
|
||||
"family": "family",
|
||||
"folk-acoustic": "folk & acoustic",
|
||||
"kids-family": "kids & family",
|
||||
"ambient": "ambient",
|
||||
"funk": "funk",
|
||||
"funk-disco": "funk & disco",
|
||||
"jazz": "jazz",
|
||||
"soul": "soul",
|
||||
"speedcore": "speedcore",
|
||||
@@ -836,21 +867,21 @@
|
||||
"vocaloid": "vocaloid",
|
||||
"j-rock": "j-rock",
|
||||
"trance": "trance",
|
||||
"drumbass": "drum & bass",
|
||||
"comedy": "comedy",
|
||||
"drum-and-bass": "drum & bass",
|
||||
"comedy-meme": "comedy & meme",
|
||||
"instrumental": "instrumental",
|
||||
"hardcore": "hardcore",
|
||||
"k-pop": "k-pop",
|
||||
"indie": "indie",
|
||||
"techno": "techno",
|
||||
"house": "house",
|
||||
"game": "video game",
|
||||
"film": "film",
|
||||
"alt": "alternative",
|
||||
"video-game-soundtrack": "video game",
|
||||
"tv-movie-soundtrack": "TV & film",
|
||||
"alternative": "alternative",
|
||||
"dubstep": "dubstep",
|
||||
"metal": "metal",
|
||||
"anime": "anime",
|
||||
"hiphop": "hiphop",
|
||||
"hip-hop-rap": "hip hop & rap",
|
||||
"j-pop": "j-pop",
|
||||
"rock": "rock",
|
||||
"pop": "pop",
|
||||
@@ -882,7 +913,8 @@
|
||||
"bsr-code" : "BSR code",
|
||||
"download" : "Download map",
|
||||
"downloading" :"Downloading map",
|
||||
"cancel-download" : "Cancel download"
|
||||
"cancel-download" : "Cancel download",
|
||||
"hightlight-difficulty": "Highlight difficulty"
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
@@ -1074,6 +1106,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"drop-zone": {
|
||||
"or-browse-files": "Or browse files"
|
||||
},
|
||||
"playlist": {
|
||||
"error-playlist-creation-title": "Error creating playlist",
|
||||
"error-playlist-creation-desc": "An error occurred while creating the playlist.",
|
||||
@@ -1188,6 +1223,24 @@
|
||||
"last-week": "Last week",
|
||||
"last-month": "Last month",
|
||||
"3-last-month": "Last 3 months"
|
||||
},
|
||||
"playlists-imported": "Playlists imported",
|
||||
"all-playlists-have-been-successfully-imported": "All playlists have been successfully imported",
|
||||
"no-playlist-found": "No playlist found",
|
||||
"no-playlist-found-in-selected-files": "No playlist found in the selected files",
|
||||
"some-playlists-not-imported": "Some playlists not imported",
|
||||
"some-playlists-have-been-imported": {
|
||||
"INVALID_SOURCE": "Some playlists could not be found",
|
||||
"INVALID_PLAYLIST_FILE": "Some playlists are invalid",
|
||||
"CANNOT_PARSE_PLAYLIST": "Some playlists are unreadable",
|
||||
"unknown": "Some playlists could not be imported"
|
||||
},
|
||||
"no-playlists-imported": "No playlists imported",
|
||||
"no-playlists-imported-errors": {
|
||||
"INVALID_SOURCE": "Playlists could not be found",
|
||||
"INVALID_PLAYLIST_FILE": "Playlists are invalid",
|
||||
"CANNOT_PARSE_PLAYLIST": "Playlists are unreadable",
|
||||
"unknown": "No playlist could be imported"
|
||||
}
|
||||
},
|
||||
"dateformat": {
|
||||
|
||||
@@ -52,8 +52,9 @@
|
||||
"tabs": {
|
||||
"maps": {
|
||||
"actions": {
|
||||
"add-maps": {
|
||||
"text": "Añadir"
|
||||
"drop-down": {
|
||||
"browse-maps": "Explorar mapas",
|
||||
"import-maps": "Importar mapas"
|
||||
},
|
||||
"link-maps": {
|
||||
"tooltips": {
|
||||
@@ -65,6 +66,21 @@
|
||||
"empty-maps": {
|
||||
"text": "No hay mapas",
|
||||
"button": "Descargar mapas"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "Importar tus mapas",
|
||||
"subtext": "Suelta tus archivos zip aquí para importar tus mapas"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
"drop-down": {
|
||||
"browse-playlists": "Explorar listas de reproducción",
|
||||
"create-a-playlist": "Crear una lista de reproducción",
|
||||
"import-playlists": "Importar listas de reproducción"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "Importa tus listas de reproducción",
|
||||
"subtext": "Suelta tus archivos \".bplist\" o \".json\" aquí para importarlos"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -250,11 +266,13 @@
|
||||
"errors": {
|
||||
"titles": {
|
||||
"operation-running": "Operación en curso",
|
||||
"no-internet": "Sin internet"
|
||||
"no-internet": "Sin internet",
|
||||
"file-not-supported": "Archivo no compatible"
|
||||
},
|
||||
"msg": {
|
||||
"operation-running": "Espera a que termine la operación actual y vuelve a empezar.",
|
||||
"no-internet": "Comprueba tu conexión a Internet e inténtalo de nuevo."
|
||||
"no-internet": "Comprueba tu conexión a Internet e inténtalo de nuevo.",
|
||||
"file-not-supported": "Solo se admiten archivos {types}."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -505,6 +523,19 @@
|
||||
"duplicates-maps-deleted": {
|
||||
"title": "Duplicados eliminados",
|
||||
"msg": "Se eliminaron los duplicados"
|
||||
},
|
||||
"import-map": {
|
||||
"titles": {
|
||||
"success": "Importación de mapas completada",
|
||||
"error": "Ocurrió un error durante la importación de mapas"
|
||||
},
|
||||
"msgs": {
|
||||
"success": "Los mapas se han importado con éxito.",
|
||||
"some-success": "Algunos mapas se han importado con éxito.",
|
||||
"only-accept-zip": "Solo se admiten archivos zip.",
|
||||
"invalid-zip": "El o los archivos zip no contienen mapas.",
|
||||
"unknown": "Se produjo un error desconocido."
|
||||
}
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -813,13 +844,13 @@
|
||||
"tech": "tech"
|
||||
},
|
||||
"map-styles": {
|
||||
"dance": "baile",
|
||||
"dance": "danza",
|
||||
"swing": "swing",
|
||||
"nightcore": "nightcore",
|
||||
"folk": "folk",
|
||||
"family": "familia",
|
||||
"ambient": "ambiente",
|
||||
"funk": "funk",
|
||||
"folk-acoustic": "folk & acústico",
|
||||
"kids-family": "niños & familia",
|
||||
"ambient": "ambiental",
|
||||
"funk-disco": "funk & disco",
|
||||
"jazz": "jazz",
|
||||
"soul": "soul",
|
||||
"speedcore": "speedcore",
|
||||
@@ -829,26 +860,26 @@
|
||||
"vocaloid": "vocaloid",
|
||||
"j-rock": "j-rock",
|
||||
"trance": "trance",
|
||||
"drumbass": "drum & bass",
|
||||
"comedy": "comedia",
|
||||
"drum-and-bass": "drum & bass",
|
||||
"comedy-meme": "comedia & meme",
|
||||
"instrumental": "instrumental",
|
||||
"hardcore": "hardcore",
|
||||
"k-pop": "k-pop",
|
||||
"indie": "indie",
|
||||
"techno": "tecno",
|
||||
"techno": "techno",
|
||||
"house": "house",
|
||||
"game": "videojuego",
|
||||
"film": "film",
|
||||
"alt": "alternativa",
|
||||
"video-game-soundtrack": "videojuego",
|
||||
"tv-movie-soundtrack": "TV & cine",
|
||||
"alternative": "alternativo",
|
||||
"dubstep": "dubstep",
|
||||
"metal": "metal",
|
||||
"anime": "anime",
|
||||
"hiphop": "hiphop",
|
||||
"hip-hop-rap": "hip hop & rap",
|
||||
"j-pop": "j-pop",
|
||||
"rock": "rock",
|
||||
"pop": "pop",
|
||||
"electronic": "electrónico",
|
||||
"classical-orchestral": "Clásico y orquestal"
|
||||
"electronic": "electrónica",
|
||||
"classical-orchestral": "Clásica & Orquestal"
|
||||
},
|
||||
"map-specificities": {
|
||||
"automapper": "IA",
|
||||
@@ -875,7 +906,8 @@
|
||||
"bsr-code" : "Código BSR",
|
||||
"download" : "Descargar mapa",
|
||||
"downloading" :"Descargando mapa",
|
||||
"cancel-download" : "Cancelar descarga"
|
||||
"cancel-download" : "Cancelar descarga",
|
||||
"hightlight-difficulty": "Resaltar la dificultad"
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
@@ -1067,6 +1099,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"drop-zone": {
|
||||
"or-browse-files": "O navegar por los archivos"
|
||||
},
|
||||
"playlist": {
|
||||
"error-playlist-creation-title": "Error al crear la lista de reproducción",
|
||||
"error-playlist-creation-desc": "Ocurrió un error al crear la lista de reproducción.",
|
||||
@@ -1181,6 +1216,24 @@
|
||||
"last-week": "Última semana",
|
||||
"last-month": "Último mes",
|
||||
"3-last-month": "Últimos 3 meses"
|
||||
},
|
||||
"playlists-imported": "Listas de reproducción importadas",
|
||||
"all-playlists-have-been-successfully-imported": "Todas las listas de reproducción se han importado con éxito",
|
||||
"no-playlist-found": "No se encontró ninguna lista de reproducción",
|
||||
"no-playlist-found-in-selected-files": "No se encontró ninguna lista de reproducción en los archivos seleccionados",
|
||||
"some-playlists-not-imported": "Algunas listas de reproducción no se importaron",
|
||||
"some-playlists-have-been-imported": {
|
||||
"INVALID_SOURCE": "No se pudieron encontrar algunas listas de reproducción",
|
||||
"INVALID_PLAYLIST_FILE": "Algunas listas de reproducción no son válidas",
|
||||
"CANNOT_PARSE_PLAYLIST": "Algunas listas de reproducción no se pueden leer",
|
||||
"unknown": "No se pudieron importar algunas listas de reproducción"
|
||||
},
|
||||
"no-playlists-imported": "No se importaron listas de reproducción",
|
||||
"no-playlists-imported-errors": {
|
||||
"INVALID_SOURCE": "No se pudieron encontrar las listas de reproducción",
|
||||
"INVALID_PLAYLIST_FILE": "Las listas de reproducción no son válidas",
|
||||
"CANNOT_PARSE_PLAYLIST": "Las listas de reproducción no se pueden leer",
|
||||
"unknown": "No se pudo importar ninguna lista de reproducción"
|
||||
}
|
||||
},
|
||||
"dateformat": {
|
||||
|
||||
@@ -52,8 +52,9 @@
|
||||
"tabs": {
|
||||
"maps": {
|
||||
"actions": {
|
||||
"add-maps": {
|
||||
"text": "Ajouter"
|
||||
"drop-down": {
|
||||
"browse-maps": "Explorer les maps",
|
||||
"import-maps": "Importer des maps"
|
||||
},
|
||||
"link-maps": {
|
||||
"tooltips": {
|
||||
@@ -65,6 +66,21 @@
|
||||
"empty-maps": {
|
||||
"text": "Aucune map",
|
||||
"button": "Télécharger des maps"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "Importer vos maps",
|
||||
"subtext": "Déposez vos fichiers zip ici pour importer vos maps"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
"drop-down": {
|
||||
"browse-playlists": "Explorer les playlists",
|
||||
"create-a-playlist": "Créer une playlist",
|
||||
"import-playlists": "Importer des playlists"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "Importer vos playlists",
|
||||
"subtext": "Déposez vos fichiers \".bplist\" ou \".json\" ici pour les importer"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -250,11 +266,13 @@
|
||||
"errors": {
|
||||
"titles": {
|
||||
"operation-running": "Opération en cours",
|
||||
"no-internet": "Pas d'accès Internet"
|
||||
"no-internet": "Pas d'accès Internet",
|
||||
"file-not-supported": "Fichier non supporté"
|
||||
},
|
||||
"msg": {
|
||||
"operation-running": "Attends la fin de l'opération en cours puis recommence.",
|
||||
"no-internet": "Vérifie ta connexion internet et ressaye."
|
||||
"no-internet": "Vérifie ta connexion internet et ressaye.",
|
||||
"file-not-supported": "Seuls les fichiers {types} sont pris en charge."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -505,6 +523,19 @@
|
||||
"duplicates-maps-deleted": {
|
||||
"title": "Doublons supprimés",
|
||||
"msg": "Les doublons ont été supprimés"
|
||||
},
|
||||
"import-map": {
|
||||
"titles": {
|
||||
"success": "Importation des maps terminée",
|
||||
"error": "Une erreur est survenue lors de l'importation des maps"
|
||||
},
|
||||
"msgs": {
|
||||
"success": "Les maps ont été importées avec succès.",
|
||||
"some-success": "Certaines maps ont été importées avec succès.",
|
||||
"only-accept-zip": "Seules les fichiers zip sont pris en charge.",
|
||||
"invalid-zip": "Le ou les fichiers zip ne contiennent aucune maps.",
|
||||
"unknown": "Une erreur inconnue s'est produite."
|
||||
}
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -814,42 +845,42 @@
|
||||
"tech": "tech"
|
||||
},
|
||||
"map-styles": {
|
||||
"dance": "dance",
|
||||
"dance": "danse",
|
||||
"swing": "swing",
|
||||
"nightcore": "nightcore",
|
||||
"folk": "folk",
|
||||
"family": "famille",
|
||||
"ambient": "ambiante",
|
||||
"funk": "funk",
|
||||
"folk-acoustic": "folk & acoustique",
|
||||
"kids-family": "enfants & famille",
|
||||
"ambient": "ambiant",
|
||||
"funk-disco": "funk & disco",
|
||||
"jazz": "jazz",
|
||||
"soul": "soul",
|
||||
"speedcore": "speedcore",
|
||||
"punk": "punk",
|
||||
"rb": "r&b",
|
||||
"holiday": "vacance",
|
||||
"holiday": "vacances",
|
||||
"vocaloid": "vocaloid",
|
||||
"j-rock": "j-rock",
|
||||
"trance": "trance",
|
||||
"drumbass": "drum & bass",
|
||||
"comedy": "comédie",
|
||||
"drum-and-bass": "drum & bass",
|
||||
"comedy-meme": "comédie & meme",
|
||||
"instrumental": "instrumental",
|
||||
"hardcore": "hardcore",
|
||||
"k-pop": "k-pop",
|
||||
"indie": "indé",
|
||||
"indie": "indie",
|
||||
"techno": "techno",
|
||||
"house": "house",
|
||||
"game": "jeu vidéo",
|
||||
"film": "film",
|
||||
"alt": "alternative",
|
||||
"video-game-soundtrack": "jeu vidéo",
|
||||
"tv-movie-soundtrack": "TV & film",
|
||||
"alternative": "alternatif",
|
||||
"dubstep": "dubstep",
|
||||
"metal": "metal",
|
||||
"metal": "métal",
|
||||
"anime": "anime",
|
||||
"hiphop": "hiphop",
|
||||
"hip-hop-rap": "hip hop & rap",
|
||||
"j-pop": "j-pop",
|
||||
"rock": "rock",
|
||||
"pop": "pop",
|
||||
"electronic": "électronique",
|
||||
"classical-orchestral": "classique & orchestrale"
|
||||
"classical-orchestral": "Classique & Orchestral"
|
||||
},
|
||||
"map-specificities": {
|
||||
"automapper": "IA",
|
||||
@@ -1069,6 +1100,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"drop-zone": {
|
||||
"or-browse-files": "Ou parcourir les fichiers"
|
||||
},
|
||||
"playlist": {
|
||||
"error-playlist-creation-title": "Erreur lors de la création de la playlist",
|
||||
"error-playlist-creation-desc": "Une erreur est survenue lors de la création de la playlist.",
|
||||
@@ -1183,7 +1217,26 @@
|
||||
"last-week": "Dernière semaine",
|
||||
"last-month": "Dernier mois",
|
||||
"3-last-month": "3 derniers mois"
|
||||
},
|
||||
"playlists-imported": "Playlists importées",
|
||||
"all-playlists-have-been-successfully-imported": "Toutes les playlists ont été importées avec succès",
|
||||
"no-playlist-found": "Aucune playlist trouvée",
|
||||
"no-playlist-found-in-selected-files": "Aucune playlist trouvée dans les fichiers sélectionnés",
|
||||
"some-playlists-not-imported": "Certaines playlists non importées",
|
||||
"some-playlists-have-been-imported": {
|
||||
"INVALID_SOURCE": "Certaines playlists n'ont pas pu être trouvées",
|
||||
"INVALID_PLAYLIST_FILE": "Certaines playlists ne sont pas valides",
|
||||
"CANNOT_PARSE_PLAYLIST": "Certaines playlists ne sont pas lisibles",
|
||||
"unknown": "Certaines playlists n'ont pas pu être importées"
|
||||
},
|
||||
"no-playlists-imported": "Aucune playlist importée",
|
||||
"no-playlists-imported-errors": {
|
||||
"INVALID_SOURCE": "Les playlists n'ont pas été trouvées",
|
||||
"INVALID_PLAYLIST_FILE": "Les playlists ne sont pas valides",
|
||||
"CANNOT_PARSE_PLAYLIST": "Les playlists ne sont pas lisibles",
|
||||
"unknown": "Aucune playlist n'a pu être importée"
|
||||
}
|
||||
|
||||
},
|
||||
"dateformat": {
|
||||
"dayNames": ["Dim", "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dimanche", "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi"],
|
||||
|
||||
@@ -52,8 +52,9 @@
|
||||
"tabs": {
|
||||
"maps": {
|
||||
"actions": {
|
||||
"add-maps": {
|
||||
"text": "追加"
|
||||
"drop-down": {
|
||||
"browse-maps": "マップを閲覧",
|
||||
"import-maps": "マップをインポート"
|
||||
},
|
||||
"link-maps": {
|
||||
"tooltips": {
|
||||
@@ -65,6 +66,21 @@
|
||||
"empty-maps": {
|
||||
"text": "マップがありません",
|
||||
"button": "マップをダウンロードする"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "マップをインポート",
|
||||
"subtext": "ZIPファイルをここにドロップしてマップをインポート"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
"drop-down": {
|
||||
"browse-playlists": "プレイリストを閲覧",
|
||||
"create-a-playlist": "プレイリストを作成",
|
||||
"import-playlists": "プレイリストをインポート"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "プレイリストをインポート",
|
||||
"subtext": "\".bplist\" または \".json\" ファイルをここにドロップしてインポート"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -250,11 +266,13 @@
|
||||
"errors": {
|
||||
"titles": {
|
||||
"operation-running": "稼働中",
|
||||
"no-internet": "インターネット接続がありません"
|
||||
"no-internet": "インターネット接続がありません",
|
||||
"file-not-supported": "ファイルはサポートされていません"
|
||||
},
|
||||
"msg": {
|
||||
"operation-running": "現在の処理が終了するのを待ってから、もう一度試してください。",
|
||||
"no-internet": "インターネット接続を確認し、もう一度お試しください。"
|
||||
"no-internet": "インターネット接続を確認し、もう一度お試しください。",
|
||||
"file-not-supported": "{types}ファイルのみがサポートされています。"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -505,6 +523,19 @@
|
||||
"duplicates-maps-deleted": {
|
||||
"title": "重複削除",
|
||||
"msg": "重複が削除されました"
|
||||
},
|
||||
"import-map": {
|
||||
"titles": {
|
||||
"success": "マップのインポートが完了しました",
|
||||
"error": "マップのインポート中にエラーが発生しました"
|
||||
},
|
||||
"msgs": {
|
||||
"success": "マップが正常にインポートされました。",
|
||||
"some-success": "一部のマップが正常にインポートされました。",
|
||||
"only-accept-zip": "zipファイルのみがサポートされています。",
|
||||
"invalid-zip": "zipファイルにマップが含まれていません。",
|
||||
"unknown": "不明なエラーが発生しました。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -816,39 +847,39 @@
|
||||
"dance": "ダンス",
|
||||
"swing": "スウィング",
|
||||
"nightcore": "ナイトコア",
|
||||
"folk": "フォーク",
|
||||
"family": "ファミリー",
|
||||
"folk-acoustic": "フォーク & アコースティック",
|
||||
"kids-family": "キッズ & ファミリー",
|
||||
"ambient": "アンビエント",
|
||||
"funk": "ファンク",
|
||||
"funk-disco": "ファンク & ディスコ",
|
||||
"jazz": "ジャズ",
|
||||
"soul": "ソウル",
|
||||
"speedcore": "スピードコア ",
|
||||
"speedcore": "スピードコア",
|
||||
"punk": "パンク",
|
||||
"rb": "r&b",
|
||||
"holiday": "休日",
|
||||
"holiday": "ホリデー",
|
||||
"vocaloid": "ボーカロイド",
|
||||
"j-rock": "j-rock",
|
||||
"j-rock": "J-ロック",
|
||||
"trance": "トランス",
|
||||
"drumbass": "ドラムンベース",
|
||||
"comedy": "コメディ",
|
||||
"instrumental": "器楽",
|
||||
"drum-and-bass": "ドラム & ベース",
|
||||
"comedy-meme": "コメディ & ミーム",
|
||||
"instrumental": "インストゥルメンタル",
|
||||
"hardcore": "ハードコア",
|
||||
"k-pop": "k-pop",
|
||||
"indie": "インディー",
|
||||
"techno": "テクノ",
|
||||
"house": "家",
|
||||
"game": "ビデオゲーム",
|
||||
"film": "映画音楽",
|
||||
"alt": "オルタナティヴ",
|
||||
"house": "ハウス",
|
||||
"video-game-soundtrack": "ゲーム音楽",
|
||||
"tv-movie-soundtrack": "TV & 映画",
|
||||
"alternative": "オルタナティブ",
|
||||
"dubstep": "ダブステップ",
|
||||
"metal": "メタル",
|
||||
"anime": "アニメ",
|
||||
"hiphop": "ヒップポップ",
|
||||
"j-pop": "j-pop",
|
||||
"hip-hop-rap": "ヒップホップ & ラップ",
|
||||
"j-pop": "J-ポップ",
|
||||
"rock": "ロック",
|
||||
"pop": "ポップ",
|
||||
"electronic": "エレクトロニック",
|
||||
"classical-orchestral": "Classical & Orchestral"
|
||||
"classical-orchestral": "クラシック & オーケストラ"
|
||||
},
|
||||
"map-specificities": {
|
||||
"automapper": "AI",
|
||||
@@ -875,7 +906,8 @@
|
||||
"bsr-code" : "BSRコード",
|
||||
"download" : "マップをダウンロード",
|
||||
"downloading" :"マップをダウンロード中",
|
||||
"cancel-download" : "ダウンロードをキャンセル"
|
||||
"cancel-download" : "ダウンロードをキャンセル",
|
||||
"hightlight-difficulty": "難易度をハイライト"
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
@@ -1067,6 +1099,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"drop-zone": {
|
||||
"or-browse-files": "またはファイルを参照"
|
||||
},
|
||||
"playlist": {
|
||||
"error-playlist-creation-title": "プレイリストの作成エラー",
|
||||
"error-playlist-creation-desc": "プレイリストの作成中にエラーが発生しました。",
|
||||
@@ -1181,6 +1216,24 @@
|
||||
"last-week": "先週",
|
||||
"last-month": "先月",
|
||||
"3-last-month": "過去3ヶ月"
|
||||
},
|
||||
"playlists-imported": "プレイリストがインポートされました",
|
||||
"all-playlists-have-been-successfully-imported": "すべてのプレイリストが正常にインポートされました",
|
||||
"no-playlist-found": "プレイリストが見つかりません",
|
||||
"no-playlist-found-in-selected-files": "選択されたファイルにプレイリストが見つかりません",
|
||||
"some-playlists-not-imported": "一部のプレイリストはインポートされませんでした",
|
||||
"some-playlists-have-been-imported": {
|
||||
"INVALID_SOURCE": "一部のプレイリストが見つかりませんでした",
|
||||
"INVALID_PLAYLIST_FILE": "一部のプレイリストは無効です",
|
||||
"CANNOT_PARSE_PLAYLIST": "一部のプレイリストは読み取れません",
|
||||
"unknown": "一部のプレイリストをインポートできませんでした"
|
||||
},
|
||||
"no-playlists-imported": "プレイリストはインポートされませんでした",
|
||||
"no-playlists-imported-errors": {
|
||||
"INVALID_SOURCE": "プレイリストが見つかりませんでした",
|
||||
"INVALID_PLAYLIST_FILE": "プレイリストは無効です",
|
||||
"CANNOT_PARSE_PLAYLIST": "プレイリストは読み取れません",
|
||||
"unknown": "プレイリストをインポートできませんでした"
|
||||
}
|
||||
},
|
||||
"dateformat": {
|
||||
|
||||
@@ -52,8 +52,9 @@
|
||||
"tabs": {
|
||||
"maps": {
|
||||
"actions": {
|
||||
"add-maps": {
|
||||
"text": "Добавить"
|
||||
"drop-down": {
|
||||
"browse-maps": "Просмотр карт",
|
||||
"import-maps": "Импорт карт"
|
||||
},
|
||||
"link-maps": {
|
||||
"tooltips": {
|
||||
@@ -65,6 +66,21 @@
|
||||
"empty-maps": {
|
||||
"text": "Нет карт",
|
||||
"button": "Скачать карты"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "Импортируйте свои карты",
|
||||
"subtext": "Перетащите сюда файлы ZIP, чтобы импортировать свои карты"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
"drop-down": {
|
||||
"browse-playlists": "Просмотр плейлистов",
|
||||
"create-a-playlist": "Создать плейлист",
|
||||
"import-playlists": "Импорт плейлистов"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "Импортируйте ваши плейлисты",
|
||||
"subtext": "Перетащите сюда файлы \".bplist\" или \".json\" для их импорта"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -250,11 +266,13 @@
|
||||
"errors": {
|
||||
"titles": {
|
||||
"operation-running": "Операция в процессе",
|
||||
"no-internet": "Нет интернета"
|
||||
"no-internet": "Нет интернета",
|
||||
"file-not-supported": "Файл не поддерживается"
|
||||
},
|
||||
"msg": {
|
||||
"operation-running": "Дождитесь завершения операции, потом попробуйте снова.",
|
||||
"no-internet": "Проверьте своё соединение и попробуйте снова."
|
||||
"no-internet": "Проверьте своё соединение и попробуйте снова.",
|
||||
"file-not-supported": "Поддерживаются только файлы {types}."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -505,6 +523,19 @@
|
||||
"duplicates-maps-deleted": {
|
||||
"title": "Дубликаты удалены",
|
||||
"msg": "Дубликаты были удалены"
|
||||
},
|
||||
"import-map": {
|
||||
"titles": {
|
||||
"success": "Импорт карт завершен",
|
||||
"error": "Произошла ошибка при импорте карт"
|
||||
},
|
||||
"msgs": {
|
||||
"success": "Карты были успешно импортированы.",
|
||||
"some-success": "Некоторые карты успешно импортированы.",
|
||||
"only-accept-zip": "Поддерживаются только zip-файлы.",
|
||||
"invalid-zip": "В zip-файле(ах) нет карт.",
|
||||
"unknown": "Произошла неизвестная ошибка."
|
||||
}
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -815,36 +846,36 @@
|
||||
"dance": "танец",
|
||||
"swing": "свинг",
|
||||
"nightcore": "найткор",
|
||||
"folk": "народные",
|
||||
"family": "семейные",
|
||||
"folk-acoustic": "фолк и акустика",
|
||||
"kids-family": "дети и семья",
|
||||
"ambient": "эмбиент",
|
||||
"funk": "фанк",
|
||||
"funk-disco": "фанк и диско",
|
||||
"jazz": "джаз",
|
||||
"soul": "соул",
|
||||
"speedcore": "спидкор",
|
||||
"punk": "панк",
|
||||
"rb": "ар-н-би",
|
||||
"holiday": "праздник",
|
||||
"vocaloid": "вокалоиды",
|
||||
"rb": "r&b",
|
||||
"holiday": "праздники",
|
||||
"vocaloid": "вокалоид",
|
||||
"j-rock": "джей-рок",
|
||||
"trance": "транс",
|
||||
"drumbass": "драм-н-бейс",
|
||||
"comedy": "комедия",
|
||||
"instrumental": "инструментальные",
|
||||
"drum-and-bass": "драм-н-бейс",
|
||||
"comedy-meme": "комедия и мемы",
|
||||
"instrumental": "инструментал",
|
||||
"hardcore": "хардкор",
|
||||
"k-pop": "кей-поп",
|
||||
"k-pop": "k-pop",
|
||||
"indie": "инди",
|
||||
"techno": "техно",
|
||||
"house": "хаус",
|
||||
"game": "видеоигры",
|
||||
"film": "фильмы",
|
||||
"alt": "альтернативные",
|
||||
"dubstep": "дапстеп",
|
||||
"video-game-soundtrack": "видеоигра",
|
||||
"tv-movie-soundtrack": "ТВ и кино",
|
||||
"alternative": "альтернатива",
|
||||
"dubstep": "дабстеп",
|
||||
"metal": "метал",
|
||||
"anime": "аниме",
|
||||
"hiphop": "хипхоп",
|
||||
"j-pop": "джей-поп",
|
||||
"rock": "прк",
|
||||
"hip-hop-rap": "хип-хоп и рэп",
|
||||
"j-pop": "j-pop",
|
||||
"rock": "рок",
|
||||
"pop": "поп",
|
||||
"electronic": "электроника",
|
||||
"classical-orchestral": "Классика и Оркестр"
|
||||
@@ -874,7 +905,8 @@
|
||||
"bsr-code" : "Код BSR",
|
||||
"download" : "Скачать карту",
|
||||
"downloading" :"Загрузка карты",
|
||||
"cancel-download" : "Отменить загрузку"
|
||||
"cancel-download" : "Отменить загрузку",
|
||||
"hightlight-difficulty": "Выделить сложность"
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
@@ -1066,6 +1098,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"drop-zone": {
|
||||
"or-browse-files": "Или просмотреть файлы"
|
||||
},
|
||||
"playlist": {
|
||||
"error-playlist-creation-title": "Ошибка при создании плейлиста",
|
||||
"error-playlist-creation-desc": "Произошла ошибка при создании плейлиста.",
|
||||
@@ -1180,6 +1215,24 @@
|
||||
"last-week": "Последняя неделя",
|
||||
"last-month": "Последний месяц",
|
||||
"3-last-month": "Последние 3 месяца"
|
||||
},
|
||||
"playlists-imported": "Плейлисты импортированы",
|
||||
"all-playlists-have-been-successfully-imported": "Все плейлисты успешно импортированы",
|
||||
"no-playlist-found": "Плейлист не найден",
|
||||
"no-playlist-found-in-selected-files": "Плейлист не найден в выбранных файлах",
|
||||
"some-playlists-not-imported": "Некоторые плейлисты не импортированы",
|
||||
"some-playlists-have-been-imported": {
|
||||
"INVALID_SOURCE": "Некоторые плейлисты не найдены",
|
||||
"INVALID_PLAYLIST_FILE": "Некоторые плейлисты недействительны",
|
||||
"CANNOT_PARSE_PLAYLIST": "Некоторые плейлисты нечитаемы",
|
||||
"unknown": "Некоторые плейлисты не удалось импортировать"
|
||||
},
|
||||
"no-playlists-imported": "Плейлисты не импортированы",
|
||||
"no-playlists-imported-errors": {
|
||||
"INVALID_SOURCE": "Плейлисты не найдены",
|
||||
"INVALID_PLAYLIST_FILE": "Плейлисты недействительны",
|
||||
"CANNOT_PARSE_PLAYLIST": "Плейлисты нечитаемы",
|
||||
"unknown": "Не удалось импортировать плейлисты"
|
||||
}
|
||||
},
|
||||
"dateformat": {
|
||||
|
||||
@@ -52,8 +52,9 @@
|
||||
"tabs": {
|
||||
"maps": {
|
||||
"actions": {
|
||||
"add-maps": {
|
||||
"text": "新增"
|
||||
"drop-down": {
|
||||
"browse-maps": "瀏覽地圖",
|
||||
"import-maps": "匯入地圖"
|
||||
},
|
||||
"link-maps": {
|
||||
"tooltips": {
|
||||
@@ -65,6 +66,21 @@
|
||||
"empty-maps": {
|
||||
"text": "無譜面",
|
||||
"button": "下載譜面"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "導入您的地圖",
|
||||
"subtext": "將ZIP文件拖放到此處以導入您的地圖"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
"drop-down": {
|
||||
"browse-playlists": "瀏覽播放列表",
|
||||
"create-a-playlist": "建立播放列表",
|
||||
"import-playlists": "匯入播放列表"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "匯入您的播放列表",
|
||||
"subtext": "將您的 \".bplist\" 或 \".json\" 檔案拖曳到這裡進行匯入"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -250,11 +266,13 @@
|
||||
"errors": {
|
||||
"titles": {
|
||||
"operation-running": "操作進行中",
|
||||
"no-internet": "無網路"
|
||||
"no-internet": "無網路",
|
||||
"file-not-supported": "文件不受支持"
|
||||
},
|
||||
"msg": {
|
||||
"operation-running": "等待當前操作完成,然後重試。",
|
||||
"no-internet": "檢查你的連接並重試。"
|
||||
"no-internet": "檢查你的連接並重試。",
|
||||
"file-not-supported": "僅支援{types}檔案。"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -505,6 +523,19 @@
|
||||
"duplicates-maps-deleted": {
|
||||
"title": "重複已刪除",
|
||||
"msg": "重複已刪除"
|
||||
},
|
||||
"import-map": {
|
||||
"titles": {
|
||||
"success": "地圖匯入完成",
|
||||
"error": "地圖匯入時發生錯誤"
|
||||
},
|
||||
"msgs": {
|
||||
"success": "地圖已成功匯入。",
|
||||
"some-success": "部分地圖已成功匯入。",
|
||||
"only-accept-zip": "僅支援zip檔案。",
|
||||
"invalid-zip": "zip檔案中沒有地圖。",
|
||||
"unknown": "發生了未知錯誤。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -813,42 +844,42 @@
|
||||
"tech": "技術"
|
||||
},
|
||||
"map-styles": {
|
||||
"dance": "Dance",
|
||||
"swing": "Swing",
|
||||
"nightcore": "Nightcore",
|
||||
"folk": "Folk",
|
||||
"family": "Family",
|
||||
"ambient": "Ambient",
|
||||
"funk": "Funk",
|
||||
"jazz": "Jazz",
|
||||
"soul": "Soul",
|
||||
"speedcore": "Speedcore",
|
||||
"punk": "Punk",
|
||||
"rb": "R&B",
|
||||
"holiday": "Holiday",
|
||||
"vocaloid": "Vocaloid",
|
||||
"j-rock": "J-rock",
|
||||
"trance": "Trance",
|
||||
"drumbass": "Drum & Bass",
|
||||
"comedy": "Comedy",
|
||||
"instrumental": "Instrumental",
|
||||
"hardcore": "Hardcore",
|
||||
"k-pop": "K-pop",
|
||||
"indie": "Indie",
|
||||
"techno": "Techno",
|
||||
"house": "House",
|
||||
"game": "Video game",
|
||||
"film": "Film",
|
||||
"alt": "Alternative",
|
||||
"dubstep": "Dubstep",
|
||||
"metal": "Metal",
|
||||
"anime": "Anime",
|
||||
"hiphop": "Hiphop",
|
||||
"j-pop": "J-pop",
|
||||
"rock": "Rock",
|
||||
"pop": "Pop",
|
||||
"electronic": "Electronic",
|
||||
"classical-orchestral": "Classical & Orchestral"
|
||||
"dance": "舞蹈",
|
||||
"swing": "搖擺",
|
||||
"nightcore": "夜核",
|
||||
"folk-acoustic": "民謠 & 原聲",
|
||||
"kids-family": "兒童 & 家庭",
|
||||
"ambient": "氛圍音樂",
|
||||
"funk-disco": "放克 & 迪斯科",
|
||||
"jazz": "爵士",
|
||||
"soul": "靈魂",
|
||||
"speedcore": "極速核",
|
||||
"punk": "龐克",
|
||||
"rb": "r&b",
|
||||
"holiday": "假日",
|
||||
"vocaloid": "聲庫",
|
||||
"j-rock": "日式搖滾",
|
||||
"trance": "迷幻",
|
||||
"drum-and-bass": "鼓 & 貝斯",
|
||||
"comedy-meme": "喜劇 & 表情包",
|
||||
"instrumental": "純音樂",
|
||||
"hardcore": "硬核",
|
||||
"k-pop": "k-pop",
|
||||
"indie": "獨立",
|
||||
"techno": "電子舞曲",
|
||||
"house": "浩室音樂",
|
||||
"video-game-soundtrack": "遊戲音樂",
|
||||
"tv-movie-soundtrack": "電視 & 電影",
|
||||
"alternative": "另類",
|
||||
"dubstep": "電音鼓",
|
||||
"metal": "金屬",
|
||||
"anime": "動漫",
|
||||
"hip-hop-rap": "嘻哈 & 說唱",
|
||||
"j-pop": "日式流行",
|
||||
"rock": "搖滾",
|
||||
"pop": "流行",
|
||||
"electronic": "電子",
|
||||
"classical-orchestral": "古典 & 管弦樂"
|
||||
},
|
||||
"map-specificities": {
|
||||
"automapper": "AI",
|
||||
@@ -875,7 +906,8 @@
|
||||
"bsr-code" : "BSR代碼",
|
||||
"download" : "下載地圖",
|
||||
"downloading" :"正在下載地圖",
|
||||
"cancel-download" : "取消下載"
|
||||
"cancel-download" : "取消下載",
|
||||
"hightlight-difficulty": "突出顯示難度"
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
@@ -1067,6 +1099,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"drop-zone": {
|
||||
"or-browse-files": "或瀏覽檔案"
|
||||
},
|
||||
"playlist": {
|
||||
"error-playlist-creation-title": "建立播放清單時發生錯誤",
|
||||
"error-playlist-creation-desc": "建立播放清單時發生錯誤。",
|
||||
@@ -1181,6 +1216,24 @@
|
||||
"last-week": "上週",
|
||||
"last-month": "上個月",
|
||||
"3-last-month": "最近3個月"
|
||||
},
|
||||
"playlists-imported": "播放清單已匯入",
|
||||
"all-playlists-have-been-successfully-imported": "所有播放清單已成功匯入",
|
||||
"no-playlist-found": "未找到播放清單",
|
||||
"no-playlist-found-in-selected-files": "在選定檔案中未找到播放清單",
|
||||
"some-playlists-not-imported": "部分播放清單未匯入",
|
||||
"some-playlists-have-been-imported": {
|
||||
"INVALID_SOURCE": "某些播放清單未找到",
|
||||
"INVALID_PLAYLIST_FILE": "某些播放清單無效",
|
||||
"CANNOT_PARSE_PLAYLIST": "某些播放清單無法解析",
|
||||
"unknown": "某些播放清單無法匯入"
|
||||
},
|
||||
"no-playlists-imported": "未匯入任何播放清單",
|
||||
"no-playlists-imported-errors": {
|
||||
"INVALID_SOURCE": "播放清單未找到",
|
||||
"INVALID_PLAYLIST_FILE": "播放清單無效",
|
||||
"CANNOT_PARSE_PLAYLIST": "播放清單無法解析",
|
||||
"unknown": "無法匯入任何播放清單"
|
||||
}
|
||||
},
|
||||
"dateformat": {
|
||||
|
||||
@@ -52,8 +52,9 @@
|
||||
"tabs": {
|
||||
"maps": {
|
||||
"actions": {
|
||||
"add-maps": {
|
||||
"text": "添加"
|
||||
"drop-down": {
|
||||
"browse-maps": "浏览地图",
|
||||
"import-maps": "导入地图"
|
||||
},
|
||||
"link-maps": {
|
||||
"tooltips": {
|
||||
@@ -65,6 +66,21 @@
|
||||
"empty-maps": {
|
||||
"text": "无谱面",
|
||||
"button": "下载谱面"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "导入您的地图",
|
||||
"subtext": "将ZIP文件拖放到此处以导入您的地图"
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
"drop-down": {
|
||||
"browse-playlists": "浏览播放列表",
|
||||
"create-a-playlist": "创建播放列表",
|
||||
"import-playlists": "导入播放列表"
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "导入您的播放列表",
|
||||
"subtext": "将您的 \".bplist\" 或 \".json\" 文件拖放到这里进行导入"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -250,11 +266,13 @@
|
||||
"errors": {
|
||||
"titles": {
|
||||
"operation-running": "操作进行中",
|
||||
"no-internet": "无网络"
|
||||
"no-internet": "无网络",
|
||||
"file-not-supported": "文件不受支持"
|
||||
},
|
||||
"msg": {
|
||||
"operation-running": "等待当前操作完成,然后重试。",
|
||||
"no-internet": "检查你的连接并重试。"
|
||||
"no-internet": "检查你的连接并重试。",
|
||||
"file-not-supported": "仅支持{types}文件。"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -505,6 +523,19 @@
|
||||
"duplicates-maps-deleted": {
|
||||
"title": "重复已删除",
|
||||
"msg": "重复已删除"
|
||||
},
|
||||
"import-map": {
|
||||
"titles": {
|
||||
"success": "地图导入完成",
|
||||
"error": "导入地图时发生错误"
|
||||
},
|
||||
"msgs": {
|
||||
"success": "地图已成功导入。",
|
||||
"some-success": "部分地图已成功导入。",
|
||||
"only-accept-zip": "仅支持zip文件。",
|
||||
"invalid-zip": "zip文件中没有地图。",
|
||||
"unknown": "发生了未知错误。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"playlists": {
|
||||
@@ -813,42 +844,42 @@
|
||||
"tech": "技术"
|
||||
},
|
||||
"map-styles": {
|
||||
"dance": "dance",
|
||||
"swing": "swing",
|
||||
"nightcore": "nightcore",
|
||||
"folk": "folk",
|
||||
"family": "family",
|
||||
"ambient": "ambient",
|
||||
"funk": "funk",
|
||||
"jazz": "jazz",
|
||||
"soul": "soul",
|
||||
"speedcore": "speedcore",
|
||||
"punk": "punk",
|
||||
"dance": "舞蹈",
|
||||
"swing": "摇摆",
|
||||
"nightcore": "夜核",
|
||||
"folk-acoustic": "民谣 & 原声",
|
||||
"kids-family": "儿童 & 家庭",
|
||||
"ambient": "氛围音乐",
|
||||
"funk-disco": "放克 & 迪斯科",
|
||||
"jazz": "爵士",
|
||||
"soul": "灵魂",
|
||||
"speedcore": "极速核",
|
||||
"punk": "朋克",
|
||||
"rb": "r&b",
|
||||
"holiday": "holiday",
|
||||
"vocaloid": "vocaloid",
|
||||
"j-rock": "j-rock",
|
||||
"trance": "trance",
|
||||
"drumbass": "drum & bass",
|
||||
"comedy": "comedy",
|
||||
"instrumental": "instrumental",
|
||||
"hardcore": "hardcore",
|
||||
"holiday": "假日",
|
||||
"vocaloid": "声库",
|
||||
"j-rock": "日式摇滚",
|
||||
"trance": "迷幻",
|
||||
"drum-and-bass": "鼓 & 贝斯",
|
||||
"comedy-meme": "喜剧 & 表情包",
|
||||
"instrumental": "纯音乐",
|
||||
"hardcore": "硬核",
|
||||
"k-pop": "k-pop",
|
||||
"indie": "indie",
|
||||
"techno": "techno",
|
||||
"house": "house",
|
||||
"game": "video game",
|
||||
"film": "film",
|
||||
"alt": "alternative",
|
||||
"dubstep": "dubstep",
|
||||
"metal": "metal",
|
||||
"anime": "anime",
|
||||
"hiphop": "hiphop",
|
||||
"j-pop": "j-pop",
|
||||
"rock": "rock",
|
||||
"pop": "pop",
|
||||
"electronic": "electronic",
|
||||
"classical-orchestral": "Classical & Orchestral"
|
||||
"indie": "独立",
|
||||
"techno": "电子舞曲",
|
||||
"house": "浩室音乐",
|
||||
"video-game-soundtrack": "游戏音乐",
|
||||
"tv-movie-soundtrack": "电视 & 电影",
|
||||
"alternative": "另类",
|
||||
"dubstep": "电音鼓",
|
||||
"metal": "金属",
|
||||
"anime": "动漫",
|
||||
"hip-hop-rap": "嘻哈 & 说唱",
|
||||
"j-pop": "日式流行",
|
||||
"rock": "摇滚",
|
||||
"pop": "流行",
|
||||
"electronic": "电子",
|
||||
"classical-orchestral": "古典 & 管弦乐"
|
||||
},
|
||||
"map-specificities": {
|
||||
"automapper": "AI",
|
||||
@@ -875,7 +906,8 @@
|
||||
"bsr-code" : "BSR代码",
|
||||
"download" : "下载地图",
|
||||
"downloading" :"正在下载地图",
|
||||
"cancel-download" : "取消下载"
|
||||
"cancel-download" : "取消下载",
|
||||
"hightlight-difficulty": "突出显示难度"
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
@@ -1067,6 +1099,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"drop-zone": {
|
||||
"or-browse-files": "或浏览文件"
|
||||
},
|
||||
"playlist": {
|
||||
"error-playlist-creation-title": "创建播放列表时出错",
|
||||
"error-playlist-creation-desc": "创建播放列表时发生错误。",
|
||||
@@ -1181,6 +1216,24 @@
|
||||
"last-week": "上周",
|
||||
"last-month": "上个月",
|
||||
"3-last-month": "最近3个月"
|
||||
},
|
||||
"playlists-imported": "播放列表已导入",
|
||||
"all-playlists-have-been-successfully-imported": "所有播放列表已成功导入",
|
||||
"no-playlist-found": "未找到播放列表",
|
||||
"no-playlist-found-in-selected-files": "在选定文件中未找到播放列表",
|
||||
"some-playlists-not-imported": "部分播放列表未导入",
|
||||
"some-playlists-have-been-imported": {
|
||||
"INVALID_SOURCE": "某些播放列表未找到",
|
||||
"INVALID_PLAYLIST_FILE": "某些播放列表无效",
|
||||
"CANNOT_PARSE_PLAYLIST": "某些播放列表无法解析",
|
||||
"unknown": "某些播放列表无法导入"
|
||||
},
|
||||
"no-playlists-imported": "未导入任何播放列表",
|
||||
"no-playlists-imported-errors": {
|
||||
"INVALID_SOURCE": "播放列表未找到",
|
||||
"INVALID_PLAYLIST_FILE": "播放列表无效",
|
||||
"CANNOT_PARSE_PLAYLIST": "播放列表无法解析",
|
||||
"unknown": "无法导入任何播放列表"
|
||||
}
|
||||
},
|
||||
"dateformat": {
|
||||
|
||||
@@ -8,6 +8,7 @@ import crypto from "crypto";
|
||||
import { execSync } from "child_process";
|
||||
import { tryit } from "../../shared/helpers/error.helpers";
|
||||
import { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||
import { ErrorObject } from "serialize-error";
|
||||
|
||||
export async function pathExist(path: string): Promise<boolean> {
|
||||
try {
|
||||
@@ -279,4 +280,5 @@ export interface Progression<T = unknown, D = unknown> {
|
||||
diff?: number;
|
||||
data?: T;
|
||||
extra?: D;
|
||||
lastError?: ErrorObject;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,12 @@ ipc.on("export-maps", async (args, reply) => {
|
||||
reply(await maps.exportMaps(args.version, args.maps, args.outPath));
|
||||
});
|
||||
|
||||
ipc.on("download-map", async (args, reply) => {
|
||||
ipc.on("bs-maps.import-maps", async (args, reply) => {
|
||||
const maps = LocalMapsManagerService.getInstance();
|
||||
reply(maps.importMaps(args.paths, args.version));
|
||||
})
|
||||
|
||||
ipc.on("bs-maps.download-map", async (args, reply) => {
|
||||
const maps = LocalMapsManagerService.getInstance();
|
||||
reply(from(maps.downloadMap(args.map, args.version)));
|
||||
});
|
||||
|
||||
@@ -3,8 +3,6 @@ import { IpcService } from "../services/ipc.service";
|
||||
import { from, lastValueFrom, mergeMap, of } from "rxjs";
|
||||
import { LocalMapsManagerService } from "../services/additional-content/maps/local-maps-manager.service";
|
||||
import { Progression } from "main/helpers/fs.helpers";
|
||||
import { isValidUrl } from "shared/helpers/url.helpers";
|
||||
import { pathToFileURL } from "url";
|
||||
|
||||
const ipc = IpcService.getInstance();
|
||||
|
||||
@@ -31,10 +29,8 @@ ipc.on("is-playlists-deep-links-enabled", (args, reply) => {
|
||||
ipc.on("download-playlist", (args, reply) => {
|
||||
const playlists = LocalPlaylistsManagerService.getInstance();
|
||||
|
||||
const downloadUrl = isValidUrl(args.downloadSource) ? args.downloadSource : pathToFileURL(args.downloadSource).href;
|
||||
|
||||
return reply(playlists.downloadPlaylist({
|
||||
bpListUrl: downloadUrl,
|
||||
bplistSource: args.downloadSource,
|
||||
version: args.version,
|
||||
ignoreSongsHashs: args.ignoreSongsHashs,
|
||||
dest: args.dest
|
||||
@@ -63,6 +59,11 @@ ipc.on("export-playlists", (args, reply) => {
|
||||
reply(playlists.exportPlaylists(args));
|
||||
});
|
||||
|
||||
ipc.on("import-playlists", (args, reply) => {
|
||||
const playlists = LocalPlaylistsManagerService.getInstance();
|
||||
reply(playlists.importPlaylists(args));
|
||||
});
|
||||
|
||||
ipc.on("install-playlist-file", (args, reply) => {
|
||||
const playlists = LocalPlaylistsManagerService.getInstance();
|
||||
|
||||
|
||||
@@ -13,6 +13,11 @@ ipc.on("new-window", (args, reply) => {
|
||||
reply(from(shell.openExternal(args)));
|
||||
});
|
||||
|
||||
ipc.on("open-dialog", (args, reply) => {
|
||||
// Use this ipc instead of "choose-folder" or "choose-file" to have more control over the dialog
|
||||
reply(from(dialog.showOpenDialog(args)));
|
||||
})
|
||||
|
||||
ipc.on("choose-folder", (args, reply) => {
|
||||
reply(from(dialog.showOpenDialog({ properties: ["openDirectory"], defaultPath: args ?? "" })));
|
||||
});
|
||||
|
||||
+4
-1
@@ -1,4 +1,4 @@
|
||||
import { contextBridge, ipcRenderer, IpcRendererEvent } from "electron";
|
||||
import { contextBridge, ipcRenderer, IpcRendererEvent, webUtils } from "electron";
|
||||
import { ProviderPlatform } from "shared/models/provider-platform.enum";
|
||||
|
||||
const sep = process.platform === ProviderPlatform.WINDOWS ? "\\" : "/";
|
||||
@@ -30,5 +30,8 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
join: (...args: string[]): string => {
|
||||
return args.join(sep);
|
||||
}
|
||||
},
|
||||
webUtils: {
|
||||
getPathForFile: webUtils.getPathForFile
|
||||
}
|
||||
});
|
||||
|
||||
@@ -16,7 +16,6 @@ import { FileAssociationService } from "../file-association.service";
|
||||
import { SongDetailsCacheService } from "./maps/song-details-cache.service";
|
||||
import { sToMs } from "shared/helpers/time.helpers";
|
||||
import { LocalBPList, LocalBPListsDetails } from "shared/models/playlists/local-playlist.models";
|
||||
import { SongCacheService } from "./maps/song-cache.service";
|
||||
import { InstallationLocationService } from "../installation-location.service";
|
||||
import sanitize from "sanitize-filename";
|
||||
import { isValidUrl } from "shared/helpers/url.helpers";
|
||||
@@ -27,6 +26,7 @@ import { tryit } from "shared/helpers/error.helpers";
|
||||
import recursiveReadDir from "recursive-readdir";
|
||||
import { BsvMapDetail, SongDetails } from "shared/models/maps";
|
||||
import { findHashInString } from "shared/helpers/string.helpers";
|
||||
import { serializeError } from "serialize-error";
|
||||
|
||||
export class LocalPlaylistsManagerService {
|
||||
private static instance: LocalPlaylistsManagerService;
|
||||
@@ -42,6 +42,7 @@ export class LocalPlaylistsManagerService {
|
||||
private readonly DEEP_LINKS = {
|
||||
BeatSaver: "bsplaylist",
|
||||
};
|
||||
private readonly PLAYLIST_FILETYPES = [".bplist", ".json"];
|
||||
|
||||
private readonly versions: BSLocalVersionService;
|
||||
private readonly maps: LocalMapsManagerService;
|
||||
@@ -51,7 +52,6 @@ export class LocalPlaylistsManagerService {
|
||||
private readonly windows: WindowManagerService;
|
||||
private readonly bsaver: BeatSaverService;
|
||||
private readonly songDetails: SongDetailsCacheService;
|
||||
private readonly songCache: SongCacheService;
|
||||
private readonly bsmFs: InstallationLocationService;
|
||||
|
||||
private constructor() {
|
||||
@@ -63,7 +63,6 @@ export class LocalPlaylistsManagerService {
|
||||
this.windows = WindowManagerService.getInstance();
|
||||
this.bsaver = BeatSaverService.getInstance();
|
||||
this.songDetails = SongDetailsCacheService.getInstance();
|
||||
this.songCache = SongCacheService.getInstance();
|
||||
this.bsmFs = InstallationLocationService.getInstance();
|
||||
|
||||
|
||||
@@ -89,11 +88,15 @@ export class LocalPlaylistsManagerService {
|
||||
return fullPath;
|
||||
}
|
||||
|
||||
private acceptPlaylistFiletype(filename: string): boolean {
|
||||
return this.PLAYLIST_FILETYPES.includes(path.extname(filename).toLowerCase());
|
||||
}
|
||||
|
||||
public writeBPListFile(opt: { bpList: BPList, version?: BSVersion, dest?: string }): Observable<LocalBPList> {
|
||||
return new Observable<LocalBPList>(obs => {
|
||||
(async () => {
|
||||
const dest = await (async () => {
|
||||
if(opt.dest && path.isAbsolute(opt.dest) && path.extname(opt.dest) === ".bplist") { return opt.dest; }
|
||||
if(opt.dest && path.isAbsolute(opt.dest) && this.acceptPlaylistFiletype(opt.dest)) { return opt.dest; }
|
||||
const playlistFolder = await this.getPlaylistsFolder(opt.version);
|
||||
const playlistPath = path.join(playlistFolder, `${sanitize(opt.bpList.playlistTitle)}.bplist`);
|
||||
return getUniqueFileNamePath(playlistPath);
|
||||
@@ -111,14 +114,14 @@ export class LocalPlaylistsManagerService {
|
||||
}
|
||||
|
||||
private async installBPListFile(opt: {
|
||||
bslistSource: string,
|
||||
bplistSource: string,
|
||||
version?: BSVersion,
|
||||
dest?: string
|
||||
}): Promise<{path: string, localBPList: LocalBPList}> {
|
||||
const bplist = await this.readPlaylistFromSource(opt.bslistSource);
|
||||
const bplist = await this.readPlaylistFromSource(opt.bplistSource);
|
||||
|
||||
const dest = await (async () => {
|
||||
if(opt.dest && path.isAbsolute(opt.dest) && path.extname(opt.dest) === ".bplist") { return opt.dest; }
|
||||
if(opt.dest && path.isAbsolute(opt.dest) && this.acceptPlaylistFiletype(opt.dest)) { return opt.dest; }
|
||||
const playlistFolder = await this.getPlaylistsFolder(opt.version);
|
||||
return path.join(playlistFolder, `${sanitize(bplist.playlistTitle)}.bplist`);
|
||||
})();
|
||||
@@ -135,13 +138,22 @@ export class LocalPlaylistsManagerService {
|
||||
const isLocalFile = await pathExists(source).catch(e => { log.error(e); return false; });
|
||||
|
||||
if(!isLocalFile && !isValidUrl(source)) {
|
||||
throw new Error(`Invalid source ${source}`);
|
||||
throw new CustomError(`Invalid source (${source})`, "INVALID_SOURCE");
|
||||
}
|
||||
|
||||
const bpList: BPList = isLocalFile ? JSON.parse(readFileSync(source).toString()) : await this.request.getJSON<BPList>(source);
|
||||
const bpList: BPList = await (async () => {
|
||||
if(isLocalFile){
|
||||
const res = await tryit(async () => JSON.parse(readFileSync(source).toString()));
|
||||
if(res.error) {
|
||||
throw CustomError.fromError(res.error, "CANNOT_PARSE_PLAYLIST");
|
||||
}
|
||||
return res.result;
|
||||
}
|
||||
return this.request.getJSON<BPList>(source);
|
||||
})();
|
||||
|
||||
if(!bpList?.playlistTitle) {
|
||||
throw new Error(`Invalid playlist file ${source}`);
|
||||
throw new CustomError(`Invalid playlist file (${source})`, "INVALID_PLAYLIST_FILE");
|
||||
}
|
||||
|
||||
bpList.songs = (bpList.songs ?? []).map(s => s.hash ? (
|
||||
@@ -168,7 +180,7 @@ export class LocalPlaylistsManagerService {
|
||||
}
|
||||
|
||||
const ignoreFunc = (file: string, stats: Stats): boolean => {
|
||||
if(stats.isFile() && path.extname(file) !== ".bplist") { return true; }
|
||||
if(stats.isFile() && !this.acceptPlaylistFiletype(file)) { return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -347,8 +359,8 @@ export class LocalPlaylistsManagerService {
|
||||
});
|
||||
}
|
||||
|
||||
public downloadPlaylist({ bpListUrl, version, ignoreSongsHashs = [], dest }: {
|
||||
bpListUrl: string,
|
||||
public downloadPlaylist({ bplistSource, version, ignoreSongsHashs = [], dest }: {
|
||||
bplistSource: string,
|
||||
version?: BSVersion
|
||||
ignoreSongsHashs?: string[]
|
||||
dest?: string
|
||||
@@ -359,7 +371,7 @@ export class LocalPlaylistsManagerService {
|
||||
return new Observable<Progression<DownloadPlaylistProgressionData>>(obs => {
|
||||
(async () => {
|
||||
|
||||
const { localBPList } = await this.installBPListFile({ bslistSource: bpListUrl, version, dest });
|
||||
const { localBPList } = await this.installBPListFile({ bplistSource, version, dest });
|
||||
|
||||
await lastValueFrom(this.downloadPlaylistSongs(localBPList, ignoreSongsHashs, version).pipe(
|
||||
tap({ next: p => obs.next(p) }),
|
||||
@@ -428,13 +440,56 @@ export class LocalPlaylistsManagerService {
|
||||
return archive.finalize();
|
||||
}
|
||||
|
||||
public oneClickInstallPlaylist(bpListUrl: string): Observable<Progression<DownloadPlaylistProgressionData>> {
|
||||
public importPlaylists({ version, paths }: { version?: BSVersion, paths: string[] }): Observable<Progression<LocalBPListsDetails>> {
|
||||
return new Observable<Progression<LocalBPListsDetails>>(obs => {
|
||||
|
||||
let canceled = false;
|
||||
|
||||
(async () => {
|
||||
const bplistPaths = paths.filter(p => this.acceptPlaylistFiletype(p));
|
||||
const progress: Progression<LocalBPListsDetails> = { current: 0, total: bplistPaths.length, data: null };
|
||||
|
||||
obs.next(progress);
|
||||
|
||||
for(const playlistPath of bplistPaths) {
|
||||
if(canceled) {
|
||||
log.info("Playlist import canceled");
|
||||
return;
|
||||
}
|
||||
|
||||
const { result: localBPList, error } = await tryit(() => this.installBPListFile({ bplistSource: playlistPath, version }));
|
||||
|
||||
if(error) {
|
||||
progress.lastError = serializeError(CustomError.fromError(error));
|
||||
obs.next(progress);
|
||||
|
||||
log.error(error);
|
||||
continue;
|
||||
}
|
||||
|
||||
const bpListDetails = this.getLocalBPListDetails(localBPList.localBPList);
|
||||
|
||||
progress.current += 1;
|
||||
progress.data = bpListDetails;
|
||||
obs.next(progress);
|
||||
}
|
||||
})()
|
||||
.catch(err => obs.error(err))
|
||||
.finally(() => obs.complete());
|
||||
|
||||
return () => {
|
||||
canceled = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public oneClickInstallPlaylist(bplistUrl: string): Observable<Progression<DownloadPlaylistProgressionData>> {
|
||||
|
||||
return new Observable<Progression<DownloadPlaylistProgressionData>>(obs => {
|
||||
(async () => {
|
||||
const versions = await this.versions.getInstalledVersions();
|
||||
|
||||
const download$ = this.downloadPlaylist({ bpListUrl, version: versions.pop() }).pipe(tap({
|
||||
const download$ = this.downloadPlaylist({ bplistSource: bplistUrl, version: versions.pop() }).pipe(tap({
|
||||
next: progress => obs.next(progress),
|
||||
error: err => obs.error(err),
|
||||
}));
|
||||
@@ -446,7 +501,7 @@ export class LocalPlaylistsManagerService {
|
||||
const realSourceMapsFolder = await realpath(path.dirname(downloadedMaps[0].path));
|
||||
|
||||
for (const version of versions) {
|
||||
await this.installBPListFile({ bslistSource: playlist.path, version});
|
||||
await this.installBPListFile({ bplistSource: playlist.path, version});
|
||||
|
||||
const versionMapsFolder = await this.maps.getMapsFolderPath(version);
|
||||
const realDestMapsFolder = await realpath(versionMapsFolder).catch(e => {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import path from "path";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { BsvMapDetail, RawMapInfoData } from "shared/models/maps";
|
||||
import { BsvMapDetail } from "shared/models/maps";
|
||||
import { BsmLocalMap, BsmLocalMapsProgress, DeleteMapsProgress } from "shared/models/maps/bsm-local-map.interface";
|
||||
import { BSLocalVersionService } from "../../bs-local-version.service";
|
||||
import { InstallationLocationService } from "../../installation-location.service";
|
||||
import { UtilsService } from "../../utils.service";
|
||||
import crypto from "crypto";
|
||||
import crypto, { BinaryLike } from "crypto";
|
||||
import { lstatSync } from "fs";
|
||||
import { copy, createReadStream, ensureDir, pathExists, pathExistsSync, realpath, unlink } from "fs-extra";
|
||||
import StreamZip from "node-stream-zip";
|
||||
@@ -26,6 +26,10 @@ import { SongCacheService } from "./song-cache.service";
|
||||
import { pathToFileURL } from "url";
|
||||
import { sToMs } from "../../../../shared/helpers/time.helpers";
|
||||
import { FieldRequired } from "shared/helpers/type.helpers";
|
||||
import { MapInfo } from "shared/models/maps/info/map-info.model";
|
||||
import { parseMapInfoDat } from "shared/parsers/maps/map-info.parser";
|
||||
import { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||
import { tryit } from "shared/helpers/error.helpers";
|
||||
|
||||
export class LocalMapsManagerService {
|
||||
private static instance: LocalMapsManagerService;
|
||||
@@ -97,24 +101,35 @@ export class LocalMapsManagerService {
|
||||
}
|
||||
|
||||
private async computeMapHash(mapPath: string, rawInfoString: string): Promise<string> {
|
||||
const mapRawInfo: RawMapInfoData = JSON.parse(rawInfoString);
|
||||
const { result: mapInfo, error } = tryit(() => parseMapInfoDat(JSON.parse(rawInfoString)));
|
||||
|
||||
if(!mapInfo || error) {
|
||||
log.error(`Unable to cumpute hash, cannot parse map info at ${mapPath}`, error);
|
||||
throw CustomError.fromError(error, `Unable to cumpute hash, cannot parse map info at ${mapPath}`, "cannot-parse-map-info");
|
||||
}
|
||||
|
||||
const shasum = crypto.createHash("sha1");
|
||||
shasum.update(rawInfoString);
|
||||
|
||||
const hashFile = (filePath: string): Promise<void> => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const stream = createReadStream(filePath);
|
||||
stream.on("data", data => shasum.update(data));
|
||||
stream.on("data", data => shasum.update(data as BinaryLike));
|
||||
stream.on("error", reject);
|
||||
stream.on("close", resolve);
|
||||
});
|
||||
};
|
||||
|
||||
for (const set of mapRawInfo._difficultyBeatmapSets) {
|
||||
for (const diff of set._difficultyBeatmaps) {
|
||||
const diffFilePath = path.join(mapPath, diff._beatmapFilename);
|
||||
for (const diff of mapInfo.difficulties) {
|
||||
if(diff.beatmapFilename){
|
||||
const diffFilePath = path.join(mapPath, diff.beatmapFilename);
|
||||
await hashFile(diffFilePath);
|
||||
}
|
||||
|
||||
if(diff.lightshowDataFilename) {
|
||||
const lightshowFilePath = path.join(mapPath, diff.lightshowDataFilename);
|
||||
await hashFile(lightshowFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
return shasum.digest("hex");
|
||||
@@ -122,16 +137,16 @@ export class LocalMapsManagerService {
|
||||
|
||||
public async loadMapInfoFromPath(mapPath: string): Promise<BsmLocalMap> {
|
||||
|
||||
const getUrlsAndReturn = (rawInfo: RawMapInfoData, hash: string, mapPath: string) => {
|
||||
const coverUrl = pathToFileURL(path.join(mapPath, rawInfo._coverImageFilename)).href;
|
||||
const songUrl = pathToFileURL(path.join(mapPath, rawInfo._songFilename)).href;
|
||||
return { rawInfo, coverUrl, songUrl, hash, path: mapPath, songDetails: this.songDetailsCache.getSongDetails(hash) } as BsmLocalMap;
|
||||
const getUrlsAndReturn = (mapInfo: MapInfo, hash: string, mapPath: string): BsmLocalMap => {
|
||||
const coverUrl = pathToFileURL(path.join(mapPath, mapInfo.coverImageFilename)).href;
|
||||
const songUrl = pathToFileURL(path.join(mapPath, mapInfo.songFilename)).href;
|
||||
return { mapInfo, coverUrl, songUrl, hash, path: mapPath, songDetails: this.songDetailsCache.getSongDetails(hash) };
|
||||
};
|
||||
|
||||
const cachedInfos = this.songCache.getMapInfoFromDirname(path.basename(mapPath));
|
||||
const cachedMapInfos = this.songCache.getMapInfoFromDirname(path.basename(mapPath));
|
||||
|
||||
if (cachedInfos) {
|
||||
return getUrlsAndReturn(cachedInfos.rawInfo, cachedInfos.hash, mapPath);
|
||||
if (cachedMapInfos) {
|
||||
return getUrlsAndReturn(cachedMapInfos.mapInfo, cachedMapInfos.hash, mapPath);
|
||||
}
|
||||
|
||||
const files = await getFilesInFolder(mapPath);
|
||||
@@ -142,10 +157,16 @@ export class LocalMapsManagerService {
|
||||
}
|
||||
|
||||
const rawInfoString = await readFile(infoFile, { encoding: "utf-8" });
|
||||
const rawInfo: RawMapInfoData = JSON.parse(rawInfoString);
|
||||
const { result: mapInfo, error } = tryit(() => parseMapInfoDat(JSON.parse(rawInfoString)));
|
||||
|
||||
if (error) {
|
||||
log.error(`Cannot parse map info.dat. Map path: ${mapPath}`, error);
|
||||
throw CustomError.fromError(error, `Cannot read map info.dat. Map path: ${mapPath}`, "cannot-parse-map-info");
|
||||
}
|
||||
|
||||
const hash = await this.computeMapHash(mapPath, rawInfoString);
|
||||
|
||||
return getUrlsAndReturn(rawInfo, hash, mapPath);
|
||||
return getUrlsAndReturn(mapInfo, hash, mapPath);
|
||||
}
|
||||
|
||||
private async downloadMapZip(zipUrl: string): Promise<{ zip: StreamZip.StreamZipAsync; zipPath: string }> {
|
||||
@@ -193,7 +214,7 @@ export class LocalMapsManagerService {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.songCache.setMapInfoFromDirname(path.basename(mapPath), { rawInfo: mapInfo.rawInfo, hash: mapInfo.hash });
|
||||
this.songCache.setMapInfoFromDirname(path.basename(mapPath), { mapInfo: mapInfo.mapInfo, hash: mapInfo.hash });
|
||||
|
||||
progression.loaded++;
|
||||
observer.next(progression);
|
||||
@@ -267,8 +288,8 @@ export class LocalMapsManagerService {
|
||||
const mapsPaths = await getFoldersInFolder(versionMapsPath);
|
||||
|
||||
for (const mapPath of mapsPaths) {
|
||||
const mapInfo = await this.loadMapInfoFromPath(mapPath);
|
||||
if (hashs.includes(mapInfo.hash)) {
|
||||
const { result: mapInfo } = await tryit(() => this.loadMapInfoFromPath(mapPath));
|
||||
if (mapInfo && hashs.includes(mapInfo.hash)) {
|
||||
await deleteFolder(mapPath);
|
||||
this.songCache.deleteMapInfoFromDirname(path.basename(mapPath));
|
||||
progress.current++;
|
||||
@@ -296,8 +317,8 @@ export class LocalMapsManagerService {
|
||||
const mapsPaths = await getFoldersInFolder(versionMapsPath);
|
||||
|
||||
for (const mapPath of mapsPaths) {
|
||||
const mapInfo = await this.loadMapInfoFromPath(mapPath);
|
||||
if (mapInfo.hash === hash) {
|
||||
const { result: mapInfo } = await tryit(() => this.loadMapInfoFromPath(mapPath));
|
||||
if (mapInfo?.hash === hash) {
|
||||
return mapInfo;
|
||||
}
|
||||
}
|
||||
@@ -305,6 +326,111 @@ export class LocalMapsManagerService {
|
||||
return null;
|
||||
}
|
||||
|
||||
public importMaps(zipPaths: string[], version?: BSVersion): Observable<Progression<BsmLocalMap>> {
|
||||
return new Observable<Progression<BsmLocalMap>>(obs => {
|
||||
let unsubscribed = false;
|
||||
let progress: Progression<BsmLocalMap> = { total: 0, current: 0 };
|
||||
let nbImportedMaps = 0;
|
||||
|
||||
(async () => {
|
||||
const mapsPath = await this.getMapsFolderPath(version);
|
||||
for(const zipPath of zipPaths) {
|
||||
|
||||
if(unsubscribed) {
|
||||
log.info("Maps importation from zip has been cancelled");
|
||||
return;
|
||||
}
|
||||
|
||||
progress = { total: 0, current: 0 };
|
||||
obs.next(progress); // reset progress for each zip
|
||||
|
||||
if(!pathExistsSync(zipPath)) { continue; }
|
||||
|
||||
const zip = new StreamZip.async({ file: zipPath });
|
||||
const { result: zipEntries, error } = await tryit(() => zip.entries());
|
||||
|
||||
if(error) {
|
||||
const res = await tryit(() => zip.close());
|
||||
log.error("Could not read zip entries", zipPath, error, res?.error);
|
||||
continue;
|
||||
}
|
||||
|
||||
const zipEntriesValues = Object.values(zipEntries);
|
||||
|
||||
const mapsFolders = zipEntriesValues.reduce((acc, entry) => {
|
||||
if(!/(^|\/)[Ii]nfo\.dat$/.test(entry.name)){ return acc; }
|
||||
acc.push(path.dirname(entry.name));
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
if(mapsFolders.length === 0) {
|
||||
log.warn("No maps \"info.dat\" found in zip", zipPath);
|
||||
}
|
||||
|
||||
progress.total = mapsFolders.length;
|
||||
obs.next(progress);
|
||||
|
||||
for(const folder of mapsFolders) {
|
||||
|
||||
if(unsubscribed) {
|
||||
log.info("Maps importation from zip has been cancelled");
|
||||
await zip.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const isRoot = folder === ".";
|
||||
const dest = isRoot ? path.join(mapsPath, path.basename(zipPath, ".zip")) : path.join(mapsPath, folder);
|
||||
|
||||
let extract: () => Promise<BsmLocalMap>;
|
||||
|
||||
if(isRoot){
|
||||
const entries = zipEntriesValues.filter(entry => entry.isFile && path.dirname(entry.name) === ".");
|
||||
extract = async () => {
|
||||
await Promise.all(entries.map(entry => {
|
||||
log.info("Extracting", `"${entry.name}"`, "from", `"${zipPath}"`, "into", `"${path.join(dest, path.basename(entry.name))}"`);
|
||||
return zip.extract(entry.name, path.join(dest, path.basename(entry.name)));
|
||||
}));
|
||||
return this.loadMapInfoFromPath(dest);
|
||||
};
|
||||
} else {
|
||||
extract = async () => {
|
||||
log.info("Extracting", `"${folder}"`, "from", `"${zipPath}"`, "into", `"${mapsPath}"`);
|
||||
await zip.extract(folder, dest);
|
||||
return this.loadMapInfoFromPath(dest);
|
||||
}
|
||||
}
|
||||
|
||||
await ensureDir(dest);
|
||||
const { result: bsmMap, error } = await tryit(extract);
|
||||
|
||||
if(error) {
|
||||
log.error("Could not extract map", zipPath, folder, mapsPath, error);
|
||||
continue;
|
||||
}
|
||||
|
||||
nbImportedMaps++;
|
||||
progress.current++;
|
||||
progress.data = bsmMap;
|
||||
obs.next(progress);
|
||||
}
|
||||
|
||||
await zip.close();
|
||||
}
|
||||
})()
|
||||
.then(() => {
|
||||
if(!nbImportedMaps){
|
||||
throw new CustomError("No \"Info.dat\" file located in any of the zip files", "invalid-zip");
|
||||
}
|
||||
return log.info("Successfully imported", nbImportedMaps, "maps from", zipPaths.length, "zips");
|
||||
})
|
||||
.catch(e => obs.error(e))
|
||||
.finally(() => obs.complete());
|
||||
|
||||
return () => {
|
||||
unsubscribed = true;
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public async downloadMap(map: BsvMapDetail, version?: BSVersion): Promise<BsmLocalMap> {
|
||||
if (!map.versions.at(0).hash) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CACHE_PATH } from "main/constants";
|
||||
import { JsonCache } from "main/models/json-cache.class";
|
||||
import path from "path";
|
||||
import { RawMapInfoData } from "shared/models/maps";
|
||||
import { MapInfo } from "shared/models/maps/info/map-info.model";
|
||||
|
||||
export class SongCacheService {
|
||||
|
||||
@@ -14,34 +14,35 @@ export class SongCacheService {
|
||||
return SongCacheService.instance;
|
||||
}
|
||||
|
||||
private readonly RAW_INFOS_CACHE_PATH = path.join(CACHE_PATH, "song-raw-info-cache.json");
|
||||
private readonly MAPS_INFO_CACHE_PATH = path.join(CACHE_PATH, "map-info-cache.json");
|
||||
|
||||
private readonly rawInfosCache: JsonCache<CachedRawInfoWithHash>;
|
||||
private readonly mapsInfoCache: JsonCache<CachedMapInfoWithHash>;
|
||||
|
||||
private constructor(){
|
||||
this.rawInfosCache = new JsonCache(this.RAW_INFOS_CACHE_PATH);
|
||||
console.log(this.MAPS_INFO_CACHE_PATH);
|
||||
this.mapsInfoCache = new JsonCache(this.MAPS_INFO_CACHE_PATH);
|
||||
}
|
||||
|
||||
public getMapInfoFromDirname(dirname: string): CachedRawInfoWithHash {
|
||||
return this.rawInfosCache.get(dirname);
|
||||
public getMapInfoFromDirname(dirname: string): CachedMapInfoWithHash {
|
||||
return this.mapsInfoCache.get(dirname);
|
||||
}
|
||||
|
||||
public getMapInfoFromHash(hash: string): { dirname: string, info: CachedRawInfoWithHash } | undefined {
|
||||
const res = Object.entries(this.rawInfosCache.cache).find(([, info]) => info.hash === hash);
|
||||
public getMapInfoFromHash(hash: string): { dirname: string, info: CachedMapInfoWithHash } | undefined {
|
||||
const res = Object.entries(this.mapsInfoCache.cache).find(([, info]) => info.hash === hash);
|
||||
return res ? { dirname: res[0], info: res[1] } : undefined;
|
||||
}
|
||||
|
||||
public setMapInfoFromDirname(dirname: string, info: CachedRawInfoWithHash): void {
|
||||
this.rawInfosCache.set(dirname, info);
|
||||
public setMapInfoFromDirname(dirname: string, info: CachedMapInfoWithHash): void {
|
||||
this.mapsInfoCache.set(dirname, info);
|
||||
}
|
||||
|
||||
public deleteMapInfoFromDirname(dirname: string): void {
|
||||
this.rawInfosCache.delete(dirname);
|
||||
this.mapsInfoCache.delete(dirname);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export type CachedRawInfoWithHash = {
|
||||
export type CachedMapInfoWithHash = {
|
||||
hash: string;
|
||||
rawInfo: RawMapInfoData;
|
||||
mapInfo: MapInfo;
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import { DepotDownloaderArgsOptions, DepotDownloaderErrorEvent, DepotDownloaderE
|
||||
import { DepotDownloader } from "../../models/depot-downloader.class";
|
||||
import { app } from "electron";
|
||||
import { BsStore } from "../../../shared/models/bs-store.enum";
|
||||
import { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||
|
||||
export class BsSteamDownloaderService {
|
||||
private static instance: BsSteamDownloaderService;
|
||||
@@ -96,7 +97,7 @@ export class BsSteamDownloaderService {
|
||||
depotDownloader.$events().pipe(
|
||||
map(event => {
|
||||
if(event.type === DepotDownloaderEventType.Error){
|
||||
throw event;
|
||||
throw new CustomError(JSON.stringify(event.data) ?? "An error occur in the DepotDownloader process", event?.subType ?? DepotDownloaderErrorEvent.Unknown, event)
|
||||
}
|
||||
return event;
|
||||
}),
|
||||
|
||||
@@ -40,6 +40,9 @@ export class BeatSaverApiService {
|
||||
|
||||
const params: Record<string, string> = this.objectToStringRecord({...filter, tags});
|
||||
|
||||
delete params.enabledTags;
|
||||
delete params.excludedTags;
|
||||
|
||||
return new URLSearchParams(params);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,11 +10,10 @@ import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { FolderLinkState } from "renderer/services/version-folder-linker.service";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { BsContentTabPanel } from "../shared/bs-content-tab-panel/bs-content-tab-panel.component";
|
||||
import { BsmButton } from "../shared/bsm-button.component";
|
||||
import { MapIcon } from "../svgs/icons/map-icon.component";
|
||||
import { PlaylistIcon } from "../svgs/icons/playlist-icon.component";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
import { BehaviorSubject, lastValueFrom, of } from "rxjs";
|
||||
import { LocalPlaylistsListPanel, LocalPlaylistsListRef } from "./playlists/local-playlists-list-panel.component";
|
||||
import { PlaylistsManagerService } from "renderer/services/playlists-manager.service";
|
||||
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
|
||||
@@ -23,6 +22,8 @@ import { LocalBPListsDetails } from "shared/models/playlists/local-playlist.mode
|
||||
import { PlaylistDownloaderService } from "renderer/services/playlist-downloader.service";
|
||||
import { LocalPlaylistFilter, LocalPlaylistFilterPanel } from "./playlists/local-playlist-filter-panel.component";
|
||||
import { noop } from "shared/helpers/function.helpers";
|
||||
import { Dropzone } from "../shared/dropzone.component";
|
||||
import { logRenderError } from "renderer";
|
||||
|
||||
type Props = {
|
||||
readonly version?: BSVersion;
|
||||
@@ -46,6 +47,9 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
const t = useTranslation();
|
||||
const [tabIndex, setTabIndex] = useState(0);
|
||||
|
||||
const [mapsDropZoneOpen, setMapsDropZoneOpen] = useState(false);
|
||||
const [playlistsDropZoneOpen, setPlaylistsDropZoneOpen] = useState(false);
|
||||
|
||||
const maps$ = useConstant(() => new BehaviorSubject<BsmLocalMap[]>(undefined));
|
||||
const playlists$ = useConstant(() => new BehaviorSubject<LocalBPListsDetails[]>(undefined));
|
||||
|
||||
@@ -73,11 +77,16 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
return playlistsManager.$playlistsFolderLinkState(version);
|
||||
}, FolderLinkState.Unlinked, [version]);
|
||||
|
||||
const handleAddClick = () => {
|
||||
const handleBrowse = () => {
|
||||
switch (tabIndex) {
|
||||
case 0: return mapsDownloader.openDownloadMapModal(version, maps$.value);
|
||||
case 1: return playlistsDownloader.openDownloadPlaylistModal(version, playlists$, maps$);
|
||||
default: return noop();
|
||||
case 0:
|
||||
mapsDownloader.openDownloadMapModal(version, maps$.value);
|
||||
return;
|
||||
case 1:
|
||||
playlistsDownloader.openDownloadPlaylistModal(version, playlists$, maps$);
|
||||
return;
|
||||
default:
|
||||
return noop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +110,31 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
return playlistsManager.unlinkVersion(version);
|
||||
}
|
||||
|
||||
const importMaps = async (paths: string[]) => {
|
||||
setMapsDropZoneOpen(() => false);
|
||||
return lastValueFrom(mapsManager.importMaps(paths, version)).catch(logRenderError);
|
||||
}
|
||||
|
||||
const importPlaylists = async (paths: string[]) => {
|
||||
setPlaylistsDropZoneOpen(() => false);
|
||||
return lastValueFrom(playlistsManager.importPlaylists({ version, paths })).catch(logRenderError);
|
||||
}
|
||||
|
||||
const addDropDownItems = ((): DropDownItem[] => {
|
||||
if(tabIndex === 0){
|
||||
return [
|
||||
{ icon: "browse", text: "pages.version-viewer.maps.tabs.maps.actions.drop-down.browse-maps", onClick: handleBrowse },
|
||||
{ icon: "download", text: "pages.version-viewer.maps.tabs.maps.actions.drop-down.import-maps", onClick: () => setMapsDropZoneOpen(true) }
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
{ icon: "browse", text: "pages.version-viewer.maps.tabs.playlists.drop-down.browse-playlists", onClick: handleBrowse },
|
||||
{ icon: "add-file", text: "pages.version-viewer.maps.tabs.playlists.drop-down.create-a-playlist", onClick: () => playlistsRef?.current?.createPlaylist?.() },
|
||||
{ icon: "download", text: "pages.version-viewer.maps.tabs.playlists.drop-down.import-playlists", onClick: () => setPlaylistsDropZoneOpen(true) }
|
||||
];
|
||||
})();
|
||||
|
||||
const dropDownItems = ((): DropDownItem[] => {
|
||||
if (tabIndex === 0) {
|
||||
return [
|
||||
@@ -111,7 +145,6 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
}
|
||||
|
||||
return [
|
||||
{ icon: "add", text: t("playlist.create-a-playlist"), onClick: () => playlistsRef?.current?.createPlaylist?.() },
|
||||
{ icon: "sync", text: t("playlist.synchronize-playlists"), onClick: () => playlistsRef?.current?.syncPlaylists?.() },
|
||||
{ icon: "export", text: t("playlist.export-playlists"), onClick: () => playlistsRef?.current?.exportPlaylists?.() },
|
||||
{ icon: "trash", text: t("playlist.delete-playlists"), onClick: () => playlistsRef?.current?.deletePlaylists?.() },
|
||||
@@ -121,13 +154,20 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
return (
|
||||
<>
|
||||
<nav className="w-full shrink-0 flex h-9 justify-center px-40 gap-2 text-main-color-1 dark:text-white mb-4">
|
||||
<BsmButton
|
||||
className="flex items-center justify-center w-fit rounded-full px-2 py-1 font-bold whitespace-nowrap"
|
||||
<BsmDropdownButton
|
||||
classNames={{
|
||||
mainContainer: "h-full relative z-[1] flex justify-center w-fit",
|
||||
button: "flex items-center justify-center h-full rounded-full px-2 py-1 font-bold whitespace-nowrap shadow-none",
|
||||
itemsContainer: "bg-theme-3",
|
||||
}}
|
||||
textClassName="whitespace-nowrap"
|
||||
icon="add"
|
||||
buttonColor="primary"
|
||||
text="misc.add"
|
||||
typeColor="primary"
|
||||
align="center"
|
||||
menuTranslationY="6px"
|
||||
withBar={false}
|
||||
onClick={handleAddClick}
|
||||
items={addDropDownItems}
|
||||
/>
|
||||
<div className="h-full rounded-full bg-light-main-color-2 dark:bg-main-color-2 grow p-[6px]">
|
||||
<input
|
||||
@@ -175,8 +215,37 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
]}
|
||||
>
|
||||
<InstalledMapsContext.Provider value={mapsContextValue}>
|
||||
<LocalMapsListPanel className="w-full h-full shrink-0" isActive={isActive && tabIndex === 0} ref={mapsRef} version={version} filter={mapFilter} search={search} linkedState={mapsLinkedState} />
|
||||
<LocalPlaylistsListPanel className="w-full h-full shrink-0" isActive={isActive && tabIndex === 1} ref={playlistsRef} version={version} linkedState={playlistLinkedState} filter={playlistFilter} search={search}/>
|
||||
|
||||
<Dropzone
|
||||
className="w-full h-full shrink-0"
|
||||
onFiles={importMaps}
|
||||
text={t("pages.version-viewer.maps.tabs.maps.drop-zone.text")}
|
||||
subtext={t("pages.version-viewer.maps.tabs.maps.drop-zone.subtext")}
|
||||
open={mapsDropZoneOpen}
|
||||
onClose={mapsDropZoneOpen ? () => setMapsDropZoneOpen(() => false) : undefined}
|
||||
filters={[{ name: ".zip", extensions: ["zip"] }]}
|
||||
dialogOptions={{ dialog: {
|
||||
properties: ["openFile", "multiSelections"],
|
||||
}}}
|
||||
>
|
||||
<LocalMapsListPanel className="w-full h-full shrink-0" isActive={isActive && tabIndex === 0} ref={mapsRef} version={version} filter={mapFilter} search={search} linkedState={mapsLinkedState} />
|
||||
</Dropzone>
|
||||
|
||||
<Dropzone
|
||||
className="w-full h-full shrink-0"
|
||||
onFiles={importPlaylists}
|
||||
text={t("pages.version-viewer.maps.tabs.playlists.drop-zone.text")}
|
||||
subtext={t("pages.version-viewer.maps.tabs.playlists.drop-zone.subtext")}
|
||||
open={playlistsDropZoneOpen}
|
||||
onClose={playlistsDropZoneOpen ? () => setPlaylistsDropZoneOpen(() => false) : undefined}
|
||||
filters={[{ name: ".bplist, .json", extensions: ["bplist", "json"] }]}
|
||||
dialogOptions={{ dialog: {
|
||||
properties: ["openFile", "multiSelections"],
|
||||
}}}
|
||||
>
|
||||
<LocalPlaylistsListPanel className="w-full h-full shrink-0" isActive={isActive && tabIndex === 1} ref={playlistsRef} version={version} linkedState={playlistLinkedState} filter={playlistFilter} search={search}/>
|
||||
</Dropzone>
|
||||
|
||||
</InstalledMapsContext.Provider>
|
||||
</BsContentTabPanel>
|
||||
</>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { GlowEffect } from "../../shared/glow-effect.component";
|
||||
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
|
||||
import { SongDetails } from "shared/models/maps";
|
||||
import formatDuration from "format-duration";
|
||||
import { MapInfo } from "shared/models/maps/info/map-info.model";
|
||||
|
||||
export type Props = {
|
||||
className?: string;
|
||||
@@ -302,9 +303,10 @@ function isFitVerified(filter: MapFilter, verified: boolean): boolean {
|
||||
return verified;
|
||||
}
|
||||
|
||||
function isFitSearch(search: string, {songName, songAuthorName, levelAuthorName}: {songName: string, songAuthorName: string, levelAuthorName: string}): boolean {
|
||||
function isFitSearch(search: string, {songName, songAuthorName, levelMappers}: {songName: MapInfo["songName"], songAuthorName: MapInfo["songAuthorName"], levelMappers: MapInfo["levelMappers"]}): boolean {
|
||||
if (!search) { return true; }
|
||||
return songName?.toLowerCase().includes(search.toLowerCase()) || songAuthorName?.toLowerCase().includes(search.toLowerCase()) || levelAuthorName?.toLowerCase().includes(search.toLowerCase());
|
||||
if(levelMappers?.some(mapper => mapper?.toLowerCase().includes(search.toLowerCase()))) { return true; }
|
||||
return songName?.toLowerCase().includes(search.toLowerCase()) || songAuthorName?.toLowerCase().includes(search.toLowerCase());
|
||||
}
|
||||
|
||||
export const isLocalMapFitMapFilter = ({filter, map, search}: { filter: MapFilter, map: BsmLocalMap, search: string }): boolean => {
|
||||
@@ -323,7 +325,7 @@ export const isLocalMapFitMapFilter = ({filter, map, search}: { filter: MapFilte
|
||||
if (!isFitRanked(filter, map.songDetails?.ranked || map.songDetails?.blRanked)) { return false; }
|
||||
if (!isFitCurated(filter, map.songDetails?.curated)) { return false; }
|
||||
if (!isFitVerified(filter, map.songDetails?.uploader.verified)) { return false; }
|
||||
if (!isFitSearch(search, {songName: map.rawInfo?._songName, songAuthorName: map.rawInfo?._songAuthorName, levelAuthorName: map.rawInfo?._levelAuthorName})) { return false; }
|
||||
if (!isFitSearch(search, {songName: map.mapInfo?.songName, songAuthorName: map.mapInfo?.songAuthorName, levelMappers: map.mapInfo?.levelMappers})) { return false; }
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -344,7 +346,7 @@ export const isBsvMapFitMapFilter = ({filter, map, search}: { filter: MapFilter,
|
||||
if (!isFitRanked(filter, map.ranked || map.blRanked)) { return false; }
|
||||
if (!isFitCurated(filter, !!map.curator)) { return false; }
|
||||
if (!isFitVerified(filter, !!map.curatedAt)) { return false; }
|
||||
if (!isFitSearch(search, {songName: map.name, songAuthorName: map.metadata.songAuthorName, levelAuthorName: map.metadata.levelAuthorName})) { return false; }
|
||||
if (!isFitSearch(search, {songName: map.name, songAuthorName: map.metadata.songAuthorName, levelMappers: [map.metadata.levelAuthorName]})) { return false; }
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -364,12 +366,12 @@ export const isSongDetailsFitMapFilter = ({filter, map, search}: { filter: MapFi
|
||||
if (!isFitRanked(filter, map.ranked || map.blRanked)) { return false; }
|
||||
if (!isFitCurated(filter, map.curated)) { return false; }
|
||||
if (!isFitVerified(filter, map.uploader.verified)) { return false; }
|
||||
if (!isFitSearch(search, {songName: map.name, songAuthorName: map.uploader.name, levelAuthorName: map.uploader.name})) { return false; }
|
||||
if (!isFitSearch(search, {songName: map.name, songAuthorName: map.uploader.name, levelMappers: [map.uploader.name]})) { return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
export const isMapFitFilter = ({filter, map, search}: { filter: MapFilter, map: BsmLocalMap | BsvMapDetail | SongDetails, search: string }): boolean => {
|
||||
if ((map as BsmLocalMap)?.rawInfo) { return isLocalMapFitMapFilter({filter, map: (map as BsmLocalMap), search}); }
|
||||
if ((map as BsmLocalMap)?.mapInfo) { return isLocalMapFitMapFilter({filter, map: (map as BsmLocalMap), search}); }
|
||||
if ((map as BsvMapDetail)?.metadata) { return isBsvMapFitMapFilter({filter, map: (map as BsvMapDetail), search}); }
|
||||
if ((map as SongDetails).hash) { return isSongDetailsFitMapFilter({filter, map: (map as SongDetails), search}); }
|
||||
return false;
|
||||
|
||||
+16
-10
@@ -5,7 +5,7 @@ import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
|
||||
import { Subscription, BehaviorSubject, lastValueFrom } from "rxjs";
|
||||
import { MapFilter } from "shared/models/maps/beat-saver.model";
|
||||
import { MapsDownloaderService } from "renderer/services/maps-downloader.service";
|
||||
import { last, tap } from "rxjs/operators";
|
||||
import { bufferTime, last, tap } from "rxjs/operators";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import BeatConflict from "../../../../../assets/images/apngs/beat-conflict.png";
|
||||
import { BsmImage } from "../../shared/bsm-image.component";
|
||||
@@ -104,19 +104,24 @@ export const LocalMapsListPanel = forwardRef<LocalMapsListPanelRef, Props>(({ ve
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
let sub: Subscription;
|
||||
const subs: Subscription[] = [];
|
||||
|
||||
if(isActiveOnce){
|
||||
sub = mapsDownloader.lastDownloadedMap$.subscribe({ next: ({map, version: targetVersion}) => {
|
||||
subs.push(mapsDownloader.lastDownloadedMap$.subscribe({ next: ({map, version: targetVersion}) => {
|
||||
if (!equal(targetVersion, version)) {
|
||||
return;
|
||||
}
|
||||
setMaps((maps$.value ? [map, ...maps$.value] : [map]));
|
||||
}});
|
||||
}}));
|
||||
|
||||
subs.push(mapsManager.$onMapImported(version).pipe(bufferTime(500)).subscribe({ next: newMaps => {
|
||||
const maps = (maps$.value ?? []).filter(map => !newMaps.some(newMap => newMap.path === map.path));
|
||||
setMaps([...newMaps, ...maps]);
|
||||
}}));
|
||||
}
|
||||
|
||||
return () => {
|
||||
sub?.unsubscribe();
|
||||
subs.forEach(s => s.unsubscribe());
|
||||
}
|
||||
|
||||
}, [isActiveOnce, version])
|
||||
@@ -176,17 +181,18 @@ export const LocalMapsListPanel = forwardRef<LocalMapsListPanelRef, Props>(({ ve
|
||||
<MapItem
|
||||
key={map.path}
|
||||
hash={map.hash}
|
||||
title={map.rawInfo._songName}
|
||||
title={map.mapInfo.songName}
|
||||
coverUrl={map.coverUrl}
|
||||
songUrl={map.songUrl}
|
||||
autor={map.rawInfo._levelAuthorName}
|
||||
songAutor={map.rawInfo._songAuthorName}
|
||||
bpm={map.rawInfo._beatsPerMinute}
|
||||
autor={map.mapInfo.levelMappers.at(0)}
|
||||
songAutor={map.mapInfo.songAuthorName}
|
||||
bpm={map.mapInfo.beatsPerMinute}
|
||||
duration={map.songDetails?.duration}
|
||||
selected={renderableMap.selected}
|
||||
diffs={MapItemComponentPropsMapper.extractMapDiffs({ rawMapInfo: map.rawInfo, songDetails: map.songDetails })}
|
||||
diffs={MapItemComponentPropsMapper.extractMapDiffs({ mapInfo: map.mapInfo, songDetails: map.songDetails })}
|
||||
mapId={map.songDetails?.id}
|
||||
ranked={map.songDetails?.ranked}
|
||||
blRanked={map.songDetails?.blRanked}
|
||||
autorId={map.songDetails?.uploader.id}
|
||||
likes={map.songDetails?.upVotes}
|
||||
createdAt={map.songDetails?.uploadedAt}
|
||||
|
||||
+18
-6
@@ -6,7 +6,7 @@ import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { PlaylistsManagerService } from "renderer/services/playlists-manager.service";
|
||||
import { FolderLinkState } from "renderer/services/version-folder-linker.service";
|
||||
import { BehaviorSubject, Observable, combineLatest, distinctUntilChanged, filter, finalize, lastValueFrom, map, tap } from "rxjs";
|
||||
import { BehaviorSubject, Observable, Subscription, bufferTime, combineLatest, distinctUntilChanged, filter, finalize, lastValueFrom, map, tap } from "rxjs";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { noop } from "shared/helpers/function.helpers";
|
||||
import { LocalBPList, LocalBPListsDetails } from "shared/models/playlists/local-playlist.models";
|
||||
@@ -52,7 +52,7 @@ type Props = {
|
||||
|
||||
export type LocalPlaylistsListRef = {
|
||||
createPlaylist: () => Promise<void>;
|
||||
syncPlaylists: () => Promise<void>;
|
||||
syncPlaylists: (playlists?: LocalBPList[]) => Promise<void>;
|
||||
deletePlaylists: () => Promise<void>;
|
||||
exportPlaylists: () => Promise<void>;
|
||||
}
|
||||
@@ -111,9 +111,9 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
|
||||
}
|
||||
|
||||
},
|
||||
syncPlaylists: async () => {
|
||||
syncPlaylists: async (playlists?: LocalBPList[]) => {
|
||||
if(!isOnline){ return; }
|
||||
const toSync = selectedPlaylists$.value?.length ? selectedPlaylists$.value : playlists$.value;
|
||||
const toSync = playlists ?? (selectedPlaylists$.value?.length ? selectedPlaylists$.value : playlists$.value);
|
||||
if(!toSync.length){ return; }
|
||||
|
||||
const modalRes = await modals.openModal(SyncPlaylistModal, { data: toSync });
|
||||
@@ -211,10 +211,22 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
|
||||
setPlaylists(newPlaylist);
|
||||
}
|
||||
|
||||
const sub = playlistDownloader.currentDownload$.pipe(filter(download => download?.downloaded && equal(download?.info.version, version))).subscribe(download => onPlaylistDownloadedCB(download.downloaded));
|
||||
const subs: Subscription[] = [];
|
||||
|
||||
subs.push(playlistDownloader.currentDownload$.pipe(
|
||||
filter(download => download?.downloaded && equal(download?.info.version, version))
|
||||
).subscribe(download => onPlaylistDownloadedCB(download.downloaded)));
|
||||
|
||||
subs.push(playlistService.$onPlaylistImported(version).pipe(bufferTime(500)).subscribe({
|
||||
next: newPlaylists => {
|
||||
if(!newPlaylists.length){ return; }
|
||||
const playlists = (playlists$.value ?? []).filter(p => !newPlaylists.some(np => np.path === p.path));
|
||||
setPlaylists([...newPlaylists, ...playlists]);
|
||||
}
|
||||
}));
|
||||
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
subs.forEach(s => s.unsubscribe());
|
||||
}
|
||||
|
||||
}, [isActiveOnce, version, linked]);
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { ModalComponent, ModalExitCode, ModalService } from "renderer/services/modale.service";
|
||||
import { WhySteamCredentialsModal } from "./why-steam-credentials-modal.component";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { Observable } from "rxjs";
|
||||
import { catchError, Observable, of } from "rxjs";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { QRCodeSVG } from "qrcode.react";
|
||||
import { BsmCheckbox } from "renderer/components/shared/bsm-checkbox.component";
|
||||
@@ -21,7 +21,7 @@ export const LoginToSteamModal: ModalComponent<
|
||||
const [password, setPassword] = useState("");
|
||||
const [stay, setStay] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const qrCodeUrl = useObservable(() => data.qrCode$);
|
||||
const qrCodeUrl = useObservable(() => data.qrCode$.pipe(catchError(() => of(null))));
|
||||
const t = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -18,7 +18,7 @@ export const DeleteDuplicateMapsModal: ModalComponent<void, { maps: BsmLocalMap[
|
||||
<p>{
|
||||
multiple
|
||||
? t("modals.maps-actions.delete-duplicate-maps.desc-plural", { nb: `${maps.length}` })
|
||||
: t("modals.maps-actions.delete-duplicate-maps.desc", { map: `${maps.at(0).rawInfo._songName}` })
|
||||
: t("modals.maps-actions.delete-duplicate-maps.desc", { map: `${maps.at(0).mapInfo.songName}` })
|
||||
}</p>
|
||||
<div className="grid grid-flow-col grid-cols-2 gap-4 mt-4 h-8">
|
||||
<BsmButton typeColor="cancel" className="rounded-md flex justify-center items-center transition-all" onClick={() => resolver({ exitCode: ModalExitCode.CANCELED })} withBar={false} text="misc.cancel" />
|
||||
|
||||
@@ -34,7 +34,7 @@ export const DeleteMapsModal: ModalComponent<void, { linked: boolean; maps: BsmL
|
||||
<form className="text-gray-800 dark:text-gray-200">
|
||||
<h1 className="text-3xl uppercase tracking-wide w-full text-center">{t(titleText)}</h1>
|
||||
<BsmImage className="mx-auto h-24" image={BeatConflict} />
|
||||
<p className="max-w-sm w-full">{t(descText, multiple ? { nb: maps.length.toString() } : { name: maps.at(0).rawInfo._songName })}</p>
|
||||
<p className="max-w-sm w-full">{t(descText, multiple ? { nb: maps.length.toString() } : { name: maps.at(0).mapInfo.songName })}</p>
|
||||
{linked && (
|
||||
<p className="text-sm italic mt-2 cursor-help w-fit" title={t(infoTitleText)}>
|
||||
{t(infoText)}
|
||||
|
||||
@@ -22,9 +22,11 @@ import { useConstant } from "renderer/hooks/use-constant.hook";
|
||||
import { getLocalTimeZone, parseAbsolute, toCalendarDateTime } from "@internationalized/date";
|
||||
import { VirtualScroll } from "renderer/components/shared/virtual-scroll/virtual-scroll.component";
|
||||
import { MapItemComponentPropsMapper } from "shared/mappers/map/map-item-component-props.mapper";
|
||||
import { ConfigurationService } from "renderer/services/configuration.service";
|
||||
|
||||
export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; ownedMaps: BsmLocalMap[] }> = ({ options: {data : { ownedMaps, version }} }) => {
|
||||
const beatSaver = useService(BeatSaverService);
|
||||
const config = useService(ConfigurationService);
|
||||
const mapsDownloader = useService(MapsDownloaderService);
|
||||
const progressBar = useService(ProgressBarService);
|
||||
const os = useService(OsDiagnosticService);
|
||||
@@ -37,12 +39,11 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
|
||||
const [query, setQuery] = useState("");
|
||||
const [maps, setMaps] = useState<BsvMapDetail[]>([]);
|
||||
const [downloadbleMaps, setDownloadbleMaps] = useState<DownloadableMap[]>([]);
|
||||
const [sortOrder, setSortOrder] = useState<BsvSearchOrder>(BsvSearchOrder.Latest);
|
||||
const [ownedMapHashs, setOwnedMapHashs] = useState<string[]>(ownedMaps?.map(map => map.hash) ?? []);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const isOnline = useObservable(() => os.isOnline$);
|
||||
const [searchParams, setSearchParams] = useState<SearchParams>({
|
||||
sortOrder,
|
||||
sortOrder: config.get("map-sort-order"),
|
||||
filter,
|
||||
page: 0,
|
||||
q: query,
|
||||
@@ -145,7 +146,9 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
|
||||
duration={map.metadata.duration}
|
||||
hash={map.versions.at(0).hash}
|
||||
likes={map.stats.upvotes}
|
||||
mapId={map.id} ranked={map.ranked}
|
||||
mapId={map.id}
|
||||
ranked={map.ranked}
|
||||
blRanked={map.blRanked}
|
||||
title={map.name}
|
||||
songAutor={map.metadata.songAuthorName}
|
||||
diffs={MapItemComponentPropsMapper.extractMapDiffs({ bsvMap: map })}
|
||||
@@ -168,21 +171,21 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
|
||||
}, []);
|
||||
|
||||
const handleSortChange = (newSort: BsvSearchOrder) => {
|
||||
setSortOrder(() => newSort);
|
||||
config.set("map-sort-order", newSort);
|
||||
setMaps(() => []);
|
||||
setSearchParams(() => ({ ...searchParams, sortOrder: newSort }));
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
const searchParams: SearchParams = {
|
||||
sortOrder,
|
||||
const searchParamsLocal: SearchParams = {
|
||||
sortOrder: searchParams.sortOrder,
|
||||
filter,
|
||||
q: query.trim(),
|
||||
page: 0,
|
||||
};
|
||||
|
||||
setMaps(() => []);
|
||||
setSearchParams(() => searchParams);
|
||||
setSearchParams(() => searchParamsLocal);
|
||||
};
|
||||
|
||||
const handleLoadMore = () => {
|
||||
@@ -217,7 +220,7 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
|
||||
handleSearch();
|
||||
}}
|
||||
/>
|
||||
<BsmSelect className="bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-1 pb-0.5 text-center" options={sortOptions} onChange={sort => handleSortChange(sort)} />
|
||||
<BsmSelect className="bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-1 pb-0.5 text-center" options={sortOptions} selected={searchParams.sortOrder} onChange={sort => handleSortChange(sort)} />
|
||||
</div>
|
||||
|
||||
{(() => {
|
||||
|
||||
+6
-3
@@ -5,7 +5,7 @@ import { BSVersion } from "shared/bs-version.interface"
|
||||
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"
|
||||
import { LocalBPListsDetails } from "shared/models/playlists/local-playlist.models"
|
||||
import { DownloadPlaylistModalHeader } from "./download-playlist-modal-header.component"
|
||||
import { BsvPlaylist, BsvSearchOrder, PlaylistSearchParams } from "shared/models/maps/beat-saver.model"
|
||||
import { BsvPlaylist, PlaylistSearchParams } from "shared/models/maps/beat-saver.model"
|
||||
import { useCallback, useState } from "react"
|
||||
import { useOnUpdate } from "renderer/hooks/use-on-update.hook"
|
||||
import { useService } from "renderer/hooks/use-service.hook"
|
||||
@@ -20,6 +20,7 @@ import BeatConflict from "../../../../../../../assets/images/apngs/beat-conflict
|
||||
import { cn } from "renderer/helpers/css-class.helpers"
|
||||
import { VirtualScroll } from "renderer/components/shared/virtual-scroll/virtual-scroll.component"
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook"
|
||||
import { ConfigurationService } from "renderer/services/configuration.service"
|
||||
|
||||
export const DownloadPlaylistModal: ModalComponent<void, {version: BSVersion, ownedPlaylists$: Observable<LocalBPListsDetails[]>, ownedMaps$: Observable<BsmLocalMap[]>}> = (
|
||||
{ options: { data: { version, ownedPlaylists$, ownedMaps$ }} }
|
||||
@@ -27,8 +28,9 @@ export const DownloadPlaylistModal: ModalComponent<void, {version: BSVersion, ow
|
||||
|
||||
const t = useTranslation();
|
||||
|
||||
const modal = useService(ModalService);
|
||||
const beatSaver = useService(BeatSaverService);
|
||||
const config = useService(ConfigurationService);
|
||||
const modal = useService(ModalService);
|
||||
const playlistDownloader = useService(PlaylistDownloaderService);
|
||||
|
||||
const [playlists, setPlaylists] = useState<BsvPlaylist[]>(null);
|
||||
@@ -41,7 +43,7 @@ export const DownloadPlaylistModal: ModalComponent<void, {version: BSVersion, ow
|
||||
const [error, setError] = useState(false);
|
||||
const [searchParams, setSearchParams] = useState<PlaylistSearchParams>({
|
||||
q: "",
|
||||
sortOrder: BsvSearchOrder.Relevance,
|
||||
sortOrder: config.get("playlist-sort-order"),
|
||||
page: 0,
|
||||
});
|
||||
|
||||
@@ -63,6 +65,7 @@ export const DownloadPlaylistModal: ModalComponent<void, {version: BSVersion, ow
|
||||
|
||||
const handleNewSearch = (value: Omit<PlaylistSearchParams, "page">) => {
|
||||
setPlaylists(() => undefined);
|
||||
config.set("playlist-sort-order", value.sortOrder);
|
||||
setSearchParams(() => ({ ...value, page: 0}));
|
||||
};
|
||||
|
||||
|
||||
+3
-3
@@ -303,7 +303,7 @@ export const EditPlaylistModal: ModalComponent<BPList, Props> = ({ resolver, opt
|
||||
availabledHashsSelected$.value.map(hash => tmpLocalMaps.find(map => getHashOfMap(map) === hash)).filter(Boolean)
|
||||
) : tmpLocalMaps;
|
||||
|
||||
const newPlaylistMaps = {...playlistMaps$.value} ?? {};
|
||||
const newPlaylistMaps = {...playlistMaps$.value ?? {}};
|
||||
mapsToAdd.forEach(map =>{ newPlaylistMaps[getHashOfMap(map)] = { map }; });
|
||||
|
||||
playlistMaps$.next(newPlaylistMaps);
|
||||
@@ -348,8 +348,8 @@ export const EditPlaylistModal: ModalComponent<BPList, Props> = ({ resolver, opt
|
||||
|
||||
const { map } = playlistMap;
|
||||
|
||||
if((map as BsmLocalMap).rawInfo?._levelAuthorName){
|
||||
mappersSet.add((map as BsmLocalMap).rawInfo._levelAuthorName);
|
||||
if((map as BsmLocalMap).mapInfo?.levelMappers){
|
||||
(map as BsmLocalMap).mapInfo.levelMappers.forEach(mapper => mappersSet.add(mapper));
|
||||
}
|
||||
else if((map as SongDetails).uploader?.name){
|
||||
mappersSet.add((map as SongDetails).uploader.name);
|
||||
|
||||
+3
-1
@@ -80,7 +80,9 @@ export const BsvPlaylistDetailsModal: ModalComponent<void, Props> = ({ resolver,
|
||||
duration={map.metadata.duration}
|
||||
hash={map.versions.at(0).hash}
|
||||
likes={map.stats.upvotes}
|
||||
mapId={map.id} ranked={map.ranked}
|
||||
mapId={map.id}
|
||||
ranked={map.ranked}
|
||||
blRanked={map.blRanked}
|
||||
title={map.name}
|
||||
songAutor={map.metadata.songAuthorName}
|
||||
diffs={MapItemComponentPropsMapper.extractMapDiffs({ bsvMap: map })}
|
||||
|
||||
+6
-5
@@ -62,16 +62,17 @@ export const LocalPlaylistDetailsModal: ModalComponent<void, Props> = ({resolver
|
||||
<MapItem
|
||||
key={map.path}
|
||||
hash={map.hash}
|
||||
title={map.rawInfo._songName}
|
||||
title={map.mapInfo.songName}
|
||||
coverUrl={map.coverUrl}
|
||||
songUrl={map.songUrl}
|
||||
autor={map.rawInfo._levelAuthorName}
|
||||
songAutor={map.rawInfo._songAuthorName}
|
||||
bpm={map.rawInfo._beatsPerMinute}
|
||||
autor={map.mapInfo.levelMappers.at(0)}
|
||||
songAutor={map.mapInfo.songAuthorName}
|
||||
bpm={map.mapInfo.beatsPerMinute}
|
||||
duration={map.songDetails?.duration}
|
||||
diffs={MapItemComponentPropsMapper.extractMapDiffs({ rawMapInfo: map.rawInfo, songDetails: map.songDetails })}
|
||||
diffs={MapItemComponentPropsMapper.extractMapDiffs({ mapInfo: map.mapInfo, songDetails: map.songDetails })}
|
||||
mapId={map.songDetails?.id}
|
||||
ranked={map.songDetails?.ranked}
|
||||
blRanked={map.songDetails?.blRanked}
|
||||
autorId={map.songDetails?.uploader.id}
|
||||
likes={map.songDetails?.upVotes}
|
||||
createdAt={map.songDetails?.uploadedAt}
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { DragEvent, ReactNode, useCallback, useMemo, useState } from "react";
|
||||
import { BsmImage } from "./bsm-image.component";
|
||||
import BeatImpatient from "../../../../assets/images/apngs/beat-impatient.png";
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
import Color from "color";
|
||||
import { cn } from "renderer/helpers/css-class.helpers";
|
||||
import { CloseIcon } from "../svgs/icons/close-icon.component";
|
||||
import { OpenDialogOptions } from "electron";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { IpcService } from "renderer/services/ipc.service";
|
||||
import { NotificationService } from "renderer/services/notification.service";
|
||||
import { lastValueFrom } from "rxjs";
|
||||
|
||||
type Props = Readonly<{
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
open?: boolean;
|
||||
text?: string;
|
||||
subtext?: string;
|
||||
onFiles?: (paths: string[]) => void;
|
||||
onClose?: () => void;
|
||||
filters?: OpenDialogOptions["filters"];
|
||||
dialogOptions?: {
|
||||
text?: string;
|
||||
dialog?: Exclude<OpenDialogOptions, "filters">;
|
||||
}
|
||||
}>;
|
||||
|
||||
// Supports drop and drop functionality for files.
|
||||
export function Dropzone({
|
||||
children,
|
||||
className,
|
||||
open,
|
||||
text,
|
||||
subtext,
|
||||
filters,
|
||||
dialogOptions,
|
||||
onFiles,
|
||||
onClose
|
||||
}: Props) {
|
||||
|
||||
const ipc = useService(IpcService);
|
||||
const notification = useService(NotificationService);
|
||||
|
||||
const t = useTranslation();
|
||||
|
||||
const [dragCount, setDragCount] = useState(0);
|
||||
const [dropZoneDragging, setDropZoneDragging] = useState(false);
|
||||
const themeColor = useThemeColor("first-color");
|
||||
const color = new Color(themeColor).lighten(.25).saturate(.8).hex();
|
||||
|
||||
const supportedExtensions = useMemo(() => {
|
||||
if(!filters?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return filters.flatMap(filter => filter.extensions ?? []);
|
||||
}, [filters]);
|
||||
|
||||
const handleDrop = useCallback((event: DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
setDragCount(() => 0);
|
||||
setDropZoneDragging(() => false);
|
||||
|
||||
if (!onFiles || !event.dataTransfer?.files?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const paths = Array.from(event.dataTransfer.files).reduce<string[]>((acc, file) => {
|
||||
const path = window.electron.webUtils.getPathForFile(file);
|
||||
|
||||
if(supportedExtensions.some(ext => path.endsWith(ext))) {
|
||||
acc.push(path);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
if(!paths.length) {
|
||||
notification.notifyError({
|
||||
title: t("notifications.shared.errors.titles.file-not-supported"),
|
||||
desc: t("notifications.shared.errors.msg.file-not-supported", { types: supportedExtensions.join(", ") }),
|
||||
duration: 8000
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
onFiles(paths);
|
||||
}, [supportedExtensions]);
|
||||
|
||||
const onDragOver = useCallback((event: DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}, []);
|
||||
|
||||
const onDragEnter = useCallback((event: DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setDragCount(prev => prev + 1);
|
||||
}, []);
|
||||
|
||||
const onDragLeave = useCallback((event: DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setDragCount(prev => prev - 1);
|
||||
}, []);
|
||||
|
||||
const openDialog = useCallback(async () => {
|
||||
if(!dialogOptions?.dialog) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await lastValueFrom(ipc.sendV2("open-dialog", { ...dialogOptions.dialog, filters }));
|
||||
|
||||
if(res?.canceled || !res?.filePaths?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
onFiles?.(res.filePaths);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={cn("relative", className)} onDragEnter={onDragEnter} onDragLeave={onDragLeave}>
|
||||
<AnimatePresence>
|
||||
{(dragCount || open) ? (
|
||||
<div onDragEnter={onDragEnter} onDragLeave={onDragLeave} onDrop={onDragLeave} className="absolute size-full top-0 right-0 z-10">
|
||||
<motion.div className="absolute size-full top-0 right-0 bg-black" initial={{ opacity: 0 }} animate={{ opacity: .6 }} exit={{ opacity: 0 }}/>
|
||||
<motion.div className="absolute size-full top-0 right-0 flex justify-center items-center p-5" initial={{ y: "100%" }} animate={{ y: "0" }} exit={{ y: "100%" }}>
|
||||
<div className="relative h-72 max-h-full w-full max-w-lg sm:h-96 sm:max-w-2xl bg-theme-3 rounded-md shadow-black shadow-lg p-7">
|
||||
{onClose && (
|
||||
<button className="absolute top-0.5 right-0.5 size-7" onClick={onClose}>
|
||||
<CloseIcon className="size-full"/>
|
||||
</button>
|
||||
)}
|
||||
<div
|
||||
onDragOver={onDragOver}
|
||||
onDrop={handleDrop}
|
||||
onDragEnter={() => setDropZoneDragging(true)}
|
||||
onDragLeave={() => setDropZoneDragging(false)}
|
||||
className="relative size-full z-[1] border-dashed border-3 rounded-md flex flex-col gap-1 justify-center items-center"
|
||||
style={{ borderColor: color }}
|
||||
>
|
||||
<div className="absolute size-full top-0 right-0 pointer-events-none -z-[1] transition-opacity duration-150" style={{
|
||||
backgroundColor: "transparent",
|
||||
backgroundImage: `radial-gradient(${color} 1px, transparent 0)`,
|
||||
backgroundSize: "60px 60px",
|
||||
backgroundPosition: "5px",
|
||||
opacity: dropZoneDragging ? .7 : .2,
|
||||
}}/>
|
||||
<BsmImage className="size-28 pointer-events-none" image={BeatImpatient}/>
|
||||
<span className="text-2xl font-bold pointer-events-none">{text}</span>
|
||||
<span className="pointer-events-none">{subtext}</span>
|
||||
{dialogOptions?.dialog && (
|
||||
<button className="underline hover:no-underline" onClick={openDialog}>{dialogOptions?.text ?? t("drop-zone.or-browse-files")}</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
): undefined}
|
||||
</AnimatePresence>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -62,9 +62,11 @@ import { ShortcutIcon } from "./icons/shortcut-icon.component";
|
||||
import { BackupRestoreIcon } from "./icons/backup-restore-icon.component";
|
||||
import { SongDetailDiffCharactertistic } from "shared/models/maps/song-details-cache/song-details-cache.model";
|
||||
import { CleanIcon } from "./icons/clean-icon.component";
|
||||
import { BrowseIcon } from "./icons/browse-icon.component";
|
||||
import { AddFileIcon } from "./icons/add-file-icon.component";
|
||||
|
||||
|
||||
export type BsmIconType = SongDetailDiffCharactertistic | ("settings" | "trash" | "favorite" | "folder" | "bsNote" | "check" | "three-dots" | "twitch" | "eye" | "play" | "checkCircleIcon" | "discord" | "info" | "eye-cross" | "terminal" | "desktop" | "oculus" | "add" | "cross" | "task" | "github" | "close" | "thumbUpFill" | "timerFill" | "pause" | "twitter" | "sync" | "chevron-top" | "copy" | "steam" | "edit" | "export" | "patreon" | "search" | "bsMapDifficulty" | "link" | "unlink" | "download" | "filter" | "mee6" | "volume-up" | "volume-off" | "volume-down" | "shortcut" | "backup-restore" | "web-site" | "clean" | "fr-FR-flag" | "es-ES-flag" | "en-US-flag" | "en-EN-flag" | "de-DE-flag" | "ru-RU-flag" | "zh-CN-flag" | "zh-TW-flag" | "ja-JP-flag");
|
||||
export type BsmIconType = SongDetailDiffCharactertistic | ("settings" | "trash" | "favorite" | "folder" | "bsNote" | "check" | "three-dots" | "twitch" | "eye" | "play" | "checkCircleIcon" | "discord" | "info" | "eye-cross" | "terminal" | "desktop" | "oculus" | "add" | "cross" | "task" | "github" | "close" | "thumbUpFill" | "timerFill" | "pause" | "twitter" | "sync" | "chevron-top" | "copy" | "steam" | "edit" | "export" | "patreon" | "search" | "bsMapDifficulty" | "link" | "unlink" | "download" | "filter" | "mee6" | "volume-up" | "volume-off" | "volume-down" | "shortcut" | "backup-restore" | "web-site" | "clean" | "browse" | "add-file" | "fr-FR-flag" | "es-ES-flag" | "en-US-flag" | "en-EN-flag" | "de-DE-flag" | "ru-RU-flag" | "zh-CN-flag" | "zh-TW-flag" | "ja-JP-flag");
|
||||
|
||||
export const BsmIcon = memo(({ className, icon, style }: { className?: string; icon: BsmIconType; style?: CSSProperties }) => {
|
||||
// TODO : Very ugly very messy, need to find a better way to do this
|
||||
@@ -262,6 +264,14 @@ export const BsmIcon = memo(({ className, icon, style }: { className?: string; i
|
||||
return <CleanIcon className={className} style={style} />;
|
||||
}
|
||||
|
||||
if (icon === "browse") {
|
||||
return <BrowseIcon className={className} style={style} />;
|
||||
}
|
||||
|
||||
if(icon === "add-file") {
|
||||
return <AddFileIcon className={className} style={style} />;
|
||||
}
|
||||
|
||||
return <TrashIcon className={className} style={style} />;
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createSvgIcon } from "../svg-icon.type";
|
||||
|
||||
export const AddFileIcon = createSvgIcon(( props, ref ) => {
|
||||
return (
|
||||
<svg ref={ref} {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor">
|
||||
<path d="M440-357.37v80q0 17 11.5 28.5t28.5 11.5q17 0 28.5-11.5t11.5-28.5v-80h80q17 0 28.5-11.5t11.5-28.5q0-17-11.5-28.5t-28.5-11.5h-80v-80q0-17-11.5-28.5t-28.5-11.5q-17 0-28.5 11.5t-11.5 28.5v80h-80q-17 0-28.5 11.5t-11.5 28.5q0 17 11.5 28.5t28.5 11.5h80ZM242.87-71.87q-37.78 0-64.39-26.61t-26.61-64.39v-634.26q0-37.78 26.61-64.39t64.39-26.61H525.8q18.22 0 34.72 6.84 16.5 6.83 29.18 19.51L781.78-669.7q12.68 12.68 19.51 29.18 6.84 16.5 6.84 34.72v442.93q0 37.78-26.61 64.39t-64.39 26.61H242.87Zm274.26-570.76v-154.5H242.87v634.26h474.26v-434.26h-154.5q-19.15 0-32.33-13.17-13.17-13.18-13.17-32.33Zm-274.26-154.5V-597.13v-200 634.26-634.26Z"/>
|
||||
</svg>
|
||||
);
|
||||
})
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createSvgIcon } from "../svg-icon.type";
|
||||
|
||||
export const BrowseIcon = createSvgIcon(( props, ref ) => {
|
||||
|
||||
return (
|
||||
<svg ref={ref} {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor">
|
||||
<path d="M73.38-480q0-84.36 32.02-158.55 32.02-74.2 87.06-129.15 55.03-54.96 129.23-87.06 74.19-32.1 158.55-32.1 134.3 0 238.05 76.51t145.17 196.84q5.8 15.77-1.12 30.78-6.93 15.01-22.85 19.75-14.93 4.2-28.19-4.11-13.25-8.3-18.95-22.99-22.75-67.42-71.58-119.41-48.83-52-116.66-79.69v17.01q0 34.1-23.83 59.02-23.84 24.93-57.93 24.93h-83.87v84.11q0 16.64-12.84 27.87-12.83 11.23-29.75 11.23h-81.28v84.53h60.85q16 0 26.94 11.02 10.93 11.02 10.93 27.02v84.42h-57.2L157.96-556.35q-4.33 19.26-6.5 38.13-2.16 18.87-2.16 37.74 0 122.43 77.84 214.51 77.85 92.08 197.26 111.19 14.87 2.74 23.8 14.97 8.92 12.22 7.57 27.5-1.6 15.83-13.52 25.68-11.92 9.84-27.64 6.72-145.92-23.35-243.57-135.47Q73.38-327.5 73.38-480Zm749.37 355.88L717.04-229.17q-20.76 13.44-44.66 21.04-23.89 7.6-49.14 7.6-73.46 0-125.14-51.53-51.67-51.54-51.67-125.07 0-73.37 51.61-124.79 51.62-51.41 124.99-51.41 73.53 0 124.94 51.55 51.42 51.56 51.42 125.02 0 25.4-7.93 49.26-7.94 23.86-20.71 45.28l105.13 105.05q11.26 11.03 11.47 26.53.22 15.5-10.81 26.52-11.1 11.26-26.89 11.26-15.79 0-26.9-11.26ZM622.86-276.28q42.13 0 71.37-29.27 29.25-29.28 29.25-71.41 0-42.13-29.15-71.37-29.16-29.25-71.29-29.25-42.13 0-71.41 29.15-29.29 29.16-29.29 71.29 0 42.13 29.19 71.49 29.2 29.37 71.33 29.37Z"/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
});
|
||||
@@ -1,4 +1,3 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { ChangeEvent, useEffect, useState } from "react";
|
||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
@@ -15,6 +14,7 @@ import { lastValueFrom } from "rxjs";
|
||||
import { BsDownloaderService } from "renderer/services/bs-version-download/bs-downloader.service";
|
||||
import equal from "fast-deep-equal";
|
||||
import { GlowEffect } from "renderer/components/shared/glow-effect.component";
|
||||
import { cn } from "renderer/helpers/css-class.helpers";
|
||||
|
||||
type Props = { version: BSVersion };
|
||||
|
||||
@@ -44,6 +44,7 @@ export function LaunchSlide({ version }: Props) {
|
||||
}, [desktopMode]);
|
||||
|
||||
const setMode = (mode: LaunchMods, value: boolean) => {
|
||||
|
||||
if (mode === LaunchMods.DEBUG_MOD) {
|
||||
setDebugMode(value);
|
||||
} else if (mode === LaunchMods.OCULUS_MOD) {
|
||||
@@ -64,7 +65,7 @@ export function LaunchSlide({ version }: Props) {
|
||||
oculus: version.oculus ? false : oculusMode,
|
||||
desktop: desktopMode,
|
||||
debug: debugMode,
|
||||
additionalArgs,
|
||||
additionalArgs: advancedLaunch ? additionalArgs : [],
|
||||
protonPath: bsLauncherService.getProtonPath(),
|
||||
});
|
||||
|
||||
@@ -84,9 +85,9 @@ export function LaunchSlide({ version }: Props) {
|
||||
</div>
|
||||
<div className="pt-4 w-2/3 flex flex-col items-center gap-3">
|
||||
<div className="relative">
|
||||
<GlowEffect className="!rounded-full" visible={!!additionalArgsString}/>
|
||||
<GlowEffect className="!rounded-full" visible={!!(advancedLaunch && additionalArgsString)}/>
|
||||
<BsmButton
|
||||
className="rounded-full w-fit text-lg py-1 px-7 shadow-md shadow-black bg-light-main-color-2 dark:bg-main-color-2 text-gray-800 dark:text-white"
|
||||
className={cn("rounded-full w-fit text-lg py-1 px-7 bg-theme-2 text-gray-800 dark:text-white", (advancedLaunch && additionalArgsString) ? "" : "shadow-md shadow-black")}
|
||||
text="pages.version-viewer.launch-mods.advanced-launch.button"
|
||||
withBar={false}
|
||||
onClick={e => {
|
||||
@@ -95,9 +96,9 @@ export function LaunchSlide({ version }: Props) {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<motion.div className="bg-light-main-color-2 dark:bg-main-color-2 h-9 rounded-full overflow-hidden flex items-center justify-center" initial={{ width: "0px" }} animate={{ width: advancedLaunch ? "100%" : "0px" }}>
|
||||
<div className="bg-light-main-color-2 dark:bg-main-color-2 h-9 rounded-full overflow-hidden flex items-center justify-center transition-all duration-100 ease-in-out w-full origin-top shadow-black shadow-sm" style={{ scale: advancedLaunch ? "100% 100%" : "0 0" }}>
|
||||
<input className="w-[calc(100%-12px)] h-[calc(100%-12px)] bg-light-main-color-1 dark:bg-main-color-1 text-black dark:text-white rounded-full outline-none text-center" type="text" placeholder={t("pages.version-viewer.launch-mods.advanced-launch.placeholder")} value={additionalArgsString} onChange={handleAdditionalArgsChange} />
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='grow flex justify-center items-center'>
|
||||
<BsmButton
|
||||
|
||||
@@ -1,4 +1,24 @@
|
||||
export const defaultConfiguration: { [key in DefaultConfigKey]: any } = {
|
||||
import { BsvSearchOrder } from "shared/models/maps/beat-saver.model";
|
||||
|
||||
// NOTE: To refactor. Rename to LocalStorageConfigKeyValues since these are stored in the
|
||||
// localStorage in the browser, and for readability
|
||||
export interface DefaultConfigKeyValues {
|
||||
"first-color": string;
|
||||
"second-color": string;
|
||||
theme: ThemeConfig;
|
||||
language: string;
|
||||
supported_languages: string[];
|
||||
default_mods: string[];
|
||||
"default-shared-folders": string[];
|
||||
"playlist-sort-order": BsvSearchOrder;
|
||||
"map-sort-order": BsvSearchOrder;
|
||||
};
|
||||
|
||||
export type DefaultConfigKey = keyof DefaultConfigKeyValues;
|
||||
|
||||
export const defaultConfiguration: {
|
||||
[key in DefaultConfigKey]: DefaultConfigKeyValues[key]
|
||||
} = {
|
||||
"first-color": "#3b82ff",
|
||||
"second-color": "#ff4444",
|
||||
theme: "os",
|
||||
@@ -8,10 +28,9 @@ export const defaultConfiguration: { [key in DefaultConfigKey]: any } = {
|
||||
"default-shared-folders": [
|
||||
window.electron.path.join("Beat Saber_Data", "CustomLevels"),
|
||||
window.electron.path.join("Beat Saber_Data", "CustomWIPLevels"),
|
||||
"DLC"
|
||||
],
|
||||
"playlist-sort-order": BsvSearchOrder.Relevance,
|
||||
"map-sort-order": BsvSearchOrder.Latest,
|
||||
};
|
||||
|
||||
export type DefaultConfigKey = "first-color" | "second-color" | "theme" | "language" | "supported_languages" | "default_mods" | "default-shared-folders";
|
||||
|
||||
export type ThemeConfig = "dark" | "light" | "os";
|
||||
|
||||
Vendored
+5
@@ -1,3 +1,5 @@
|
||||
import { webUtils } from "electron";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electron: {
|
||||
@@ -13,6 +15,9 @@ declare global {
|
||||
basename: (path: string) => string;
|
||||
join: (...args: string[]) => string;
|
||||
};
|
||||
webUtils: {
|
||||
getPathForFile: typeof webUtils.getPathForFile;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { SteamMobileApproveModal } from "renderer/components/modal/modal-types/b
|
||||
import { DownloaderServiceInterface } from "./bs-store-downloader.interface";
|
||||
import { AbstractBsDownloaderService } from "./abstract-bs-downloader.service";
|
||||
import { addFilterStringLog } from "renderer";
|
||||
import { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||
|
||||
export class SteamDownloaderService extends AbstractBsDownloaderService implements DownloaderServiceInterface{
|
||||
|
||||
@@ -120,11 +121,11 @@ export class SteamDownloaderService extends AbstractBsDownloaderService implemen
|
||||
return subs;
|
||||
}
|
||||
|
||||
private hanndleErrorEvent(errorEvent: DepotDownloaderEvent) {
|
||||
private hanndleErrorEvent(errorEvent: CustomError) {
|
||||
const handledErrors = Object.values(DepotDownloaderErrorEvent);
|
||||
|
||||
if(handledErrors.includes(errorEvent?.subType as DepotDownloaderErrorEvent)){
|
||||
return this.notificationService.notifyError({title: "notifications.types.error", desc: `notifications.bs-download.steam-download.errors.msg.${errorEvent.subType}`, duration: 10_000});
|
||||
if(handledErrors.includes(errorEvent?.code as DepotDownloaderErrorEvent)){
|
||||
return this.notificationService.notifyError({title: "notifications.types.error", desc: `notifications.bs-download.steam-download.errors.msg.${errorEvent.code}`, duration: 10_000});
|
||||
}
|
||||
|
||||
return this.notificationService.notifyError({title: "notifications.types.error", desc: `notifications.bs-download.steam-download.errors.msg.${DepotDownloaderErrorEvent.Unknown}`, duration: 10_000});
|
||||
|
||||
@@ -30,7 +30,7 @@ export class ConfigurationService {
|
||||
public get<Type>(key: string | DefaultConfigKey): Type {
|
||||
const rawValue = (window.sessionStorage.getItem(key) ?? window.localStorage.getItem(key));
|
||||
const tryParse = tryit<Type>(() => JSON.parse(rawValue));
|
||||
|
||||
|
||||
const res = (tryParse.error ? rawValue : tryParse.result) as Type;
|
||||
|
||||
if(!res && Object.keys(defaultConfiguration).includes(key)){
|
||||
@@ -40,7 +40,7 @@ export class ConfigurationService {
|
||||
return res;
|
||||
}
|
||||
|
||||
public set(key: string, value: unknown, persistant = true) {
|
||||
public set(key: string | DefaultConfigKey, value: unknown, persistant = true) {
|
||||
|
||||
if(value != null){
|
||||
this.getPropperStorage(persistant).setItem(key, JSON.stringify(value));
|
||||
@@ -51,7 +51,7 @@ export class ConfigurationService {
|
||||
this.emitChange(key);
|
||||
}
|
||||
|
||||
public delete(key: string) {
|
||||
public delete(key: string | DefaultConfigKey) {
|
||||
window.localStorage.removeItem(key);
|
||||
window.sessionStorage.removeItem(key);
|
||||
this.emitChange(key);
|
||||
|
||||
@@ -71,7 +71,7 @@ export class MapsDownloaderService {
|
||||
if (this.os.isOffline) {
|
||||
return null;
|
||||
}
|
||||
return this.ipc.sendV2("download-map", { map, version });
|
||||
return this.ipc.sendV2("bs-maps.download-map", { map, version });
|
||||
}
|
||||
|
||||
public async openDownloadMapModal(version?: BSVersion, ownedMaps: BsmLocalMap[] = []): Promise<ModalResponse<void>> {
|
||||
|
||||
@@ -9,12 +9,13 @@ import { DeleteMapsModal } from "renderer/components/modal/modal-types/delete-ma
|
||||
import { ProgressBarService } from "./progress-bar.service";
|
||||
import { NotificationService } from "./notification.service";
|
||||
import { ConfigurationService } from "./configuration.service";
|
||||
import { map, last, catchError } from "rxjs/operators";
|
||||
import { map, last, catchError, filter, distinctUntilChanged } from "rxjs/operators";
|
||||
import { ProgressionInterface } from "shared/models/progress-bar";
|
||||
import { FolderLinkState, VersionFolderLinkerService } from "./version-folder-linker.service";
|
||||
import { SongDetails } from "shared/models/maps";
|
||||
import { Progression } from "main/helpers/fs.helpers";
|
||||
import { DeleteDuplicateMapsModal } from "renderer/components/modal/modal-types/delete-duplicate-maps-modal.component";
|
||||
import equal from "fast-deep-equal";
|
||||
|
||||
export class MapsManagerService {
|
||||
private static instance: MapsManagerService;
|
||||
@@ -39,6 +40,8 @@ export class MapsManagerService {
|
||||
private readonly lastLinkedVersion$: Subject<BSVersion> = new Subject();
|
||||
private readonly lastUnlinkedVersion$: Subject<BSVersion> = new Subject();
|
||||
|
||||
private readonly lastImportedMap$: Subject<{ version: BSVersion, map: BsmLocalMap }> = new Subject();
|
||||
|
||||
private constructor() {
|
||||
this.ipcService = IpcService.getInstance();
|
||||
this.modal = ModalService.getInstance();
|
||||
@@ -196,6 +199,64 @@ export class MapsManagerService {
|
||||
})
|
||||
}
|
||||
|
||||
public importMaps(paths: string[], version?: BSVersion): Observable<Progression<BsmLocalMap>> {
|
||||
if(!this.progressBar.require()){
|
||||
return throwError(() => new Error("Another operation is already in progress (importMaps)"));
|
||||
}
|
||||
|
||||
return new Observable<Progression<BsmLocalMap>>(obs => {
|
||||
const subs: Subscription[] = [];
|
||||
(async () => {
|
||||
const import$ = this.ipcService.sendV2("bs-maps.import-maps", { paths, version });
|
||||
|
||||
this.progressBar.show(import$.pipe(
|
||||
catchError(() => of()),
|
||||
map(progress => ({
|
||||
progression: (progress.current / progress.total) * 100,
|
||||
label: progress.data?.songDetails?.name
|
||||
} as ProgressionInterface))
|
||||
));
|
||||
|
||||
subs.push(import$.subscribe(obs));
|
||||
|
||||
subs.push(import$.pipe(catchError(() => of()), map(progress => progress?.data), filter(Boolean), distinctUntilChanged((a, b) => equal(a, b))).subscribe({ next: map => {
|
||||
this.lastImportedMap$.next({ version, map });
|
||||
}}));
|
||||
|
||||
return lastValueFrom(import$);
|
||||
})()
|
||||
.then(progress => {
|
||||
if (progress.current === progress.total) {
|
||||
this.notifications.notifySuccess({
|
||||
title: "notifications.maps.import-map.titles.success",
|
||||
desc: "notifications.maps.import-map.msgs.success",
|
||||
});
|
||||
} else if (progress.current > 0) {
|
||||
this.notifications.notifySuccess({
|
||||
title: "notifications.maps.import-map.titles.success",
|
||||
desc: "notifications.maps.import-map.msgs.some-success",
|
||||
});
|
||||
}
|
||||
}).catch(err => {
|
||||
this.notifications.notifyError({
|
||||
title: "notifications.maps.import-map.titles.error",
|
||||
desc: ["invalid-zip"].includes(err?.code)
|
||||
? `notifications.maps.import-map.msgs.${err.code}`
|
||||
: "notifications.maps.import-map.msgs.unknown"
|
||||
});
|
||||
obs.error(err);
|
||||
})
|
||||
.finally(() => obs.complete());
|
||||
|
||||
return () => {
|
||||
if(subs.length){
|
||||
this.progressBar.hide();
|
||||
subs.forEach(s => s.unsubscribe());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public getMapsInfoFromHashs(hashs: string[]): Observable<SongDetails[]> {
|
||||
return this.ipcService.sendV2("get-maps-info-from-cache", hashs);
|
||||
}
|
||||
@@ -220,6 +281,10 @@ export class MapsManagerService {
|
||||
return this.lastUnlinkedVersion$.asObservable();
|
||||
}
|
||||
|
||||
public $onMapImported(version: BSVersion): Observable<BsmLocalMap> {
|
||||
return this.lastImportedMap$.pipe(filter(newMap => equal(newMap.version, version)), map(m => m.map));
|
||||
}
|
||||
|
||||
public $mapsFolderLinkState(version: BSVersion): Observable<FolderLinkState> {
|
||||
return this.linker.$folderLinkedState(version, MapsManagerService.RELATIVE_MAPS_FOLDER);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { IpcService } from "./ipc.service";
|
||||
import { Observable, lastValueFrom } from "rxjs";
|
||||
import { Observable, Subject, Subscription, catchError, distinctUntilChanged, filter, lastValueFrom, map, of } from "rxjs";
|
||||
import { FolderLinkState, VersionFolderLinkerService } from "./version-folder-linker.service";
|
||||
import { Progression } from "main/helpers/fs.helpers";
|
||||
import { LocalBPList, LocalBPListsDetails } from "shared/models/playlists/local-playlist.models";
|
||||
@@ -8,6 +8,11 @@ import { ModalExitCode, ModalService } from "./modale.service";
|
||||
import { UnlinkPlaylistModal } from "renderer/components/modal/modal-types/unlink-playlist-modal.component";
|
||||
import { LinkPlaylistModal } from "renderer/components/modal/modal-types/link-playlist-modal.component";
|
||||
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
|
||||
import { ProgressBarService } from "./progress-bar.service";
|
||||
import equal from "fast-deep-equal";
|
||||
import { noop } from "shared/helpers/function.helpers";
|
||||
import { NotificationService } from "./notification.service";
|
||||
import { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||
|
||||
export class PlaylistsManagerService {
|
||||
private static instance: PlaylistsManagerService;
|
||||
@@ -21,14 +26,20 @@ export class PlaylistsManagerService {
|
||||
|
||||
public static readonly RELATIVE_PLAYLISTS_FOLDER = "Playlists";
|
||||
|
||||
private readonly lastPlaylistImported$ = new Subject<{ version: BSVersion, playlist: LocalBPListsDetails }>();
|
||||
|
||||
private readonly ipc: IpcService;
|
||||
private readonly linker: VersionFolderLinkerService;
|
||||
private readonly modal: ModalService;
|
||||
private readonly progressBar: ProgressBarService;
|
||||
private readonly notification: NotificationService;
|
||||
|
||||
private constructor() {
|
||||
this.ipc = IpcService.getInstance();
|
||||
this.linker = VersionFolderLinkerService.getInstance();
|
||||
this.modal = ModalService.getInstance();
|
||||
this.progressBar = ProgressBarService.getInstance();
|
||||
this.notification = NotificationService.getInstance();
|
||||
}
|
||||
|
||||
public getVersionPlaylistsDetails(version: BSVersion): Observable<Progression<LocalBPListsDetails[]>> {
|
||||
@@ -48,6 +59,69 @@ export class PlaylistsManagerService {
|
||||
});
|
||||
}
|
||||
|
||||
public importPlaylists(opt: { version?: BSVersion, paths: string[] }): Observable<Progression<LocalBPListsDetails>> {
|
||||
return new Observable<Progression<LocalBPListsDetails>>(obs => {
|
||||
if(!this.progressBar.require()){
|
||||
obs.error("A progression is already running");
|
||||
return noop;
|
||||
}
|
||||
|
||||
const subs: Subscription[] = [];
|
||||
|
||||
const import$ = this.ipc.sendV2("import-playlists", opt);
|
||||
subs.push(import$.subscribe(obs));
|
||||
|
||||
subs.push(import$.pipe(catchError(() => of()), map(progress => progress?.data), filter(Boolean), distinctUntilChanged((a, b) => equal(a, b))).subscribe({
|
||||
next: playlist => {
|
||||
this.lastPlaylistImported$.next({ version: opt.version, playlist });
|
||||
}
|
||||
}));
|
||||
|
||||
this.progressBar.show(import$);
|
||||
|
||||
const knownErrors = ["INVALID_SOURCE", "INVALID_PLAYLIST_FILE", "CANNOT_PARSE_PLAYLIST"];
|
||||
|
||||
lastValueFrom(import$).then(res => {
|
||||
if(res.lastError && !res.current){
|
||||
throw res.lastError;
|
||||
}
|
||||
|
||||
if(!res.total){
|
||||
return this.notification.notifyWarning({
|
||||
title: "playlist.no-playlist-found",
|
||||
desc: "playlist.no-playlist-found-in-selected-files"
|
||||
});
|
||||
}
|
||||
|
||||
if(res.current < res.total){
|
||||
return this.notification.notifyWarning({
|
||||
title: "playlist.some-playlists-not-imported",
|
||||
desc: knownErrors.includes(res.lastError?.code) ? `playlist.some-playlists-have-been-imported.${res.lastError.code}` : "playlist.some-playlists-have-been-imported.unknown"
|
||||
});
|
||||
}
|
||||
|
||||
return this.notification.notifySuccess({
|
||||
title: "playlist.playlists-imported",
|
||||
desc: "playlist.all-playlists-have-been-successfully-imported",
|
||||
});
|
||||
}).catch((err: CustomError) => {
|
||||
this.notification.notifyError({
|
||||
title: "playlist.no-playlists-imported",
|
||||
desc: knownErrors.includes(err.code) ? `playlist.no-playlists-imported-errors.${err.code}` : "playlist.no-playlists-imported-errors.unknown"
|
||||
});
|
||||
}).finally(() => {
|
||||
obs.complete();
|
||||
})
|
||||
|
||||
|
||||
return () => {
|
||||
subs.forEach(s => s.unsubscribe());
|
||||
this.progressBar.hide();
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
public async linkVersion(version: BSVersion): Promise<boolean> {
|
||||
const modalRes = await this.modal.openModal(LinkPlaylistModal);
|
||||
|
||||
@@ -91,4 +165,11 @@ export class PlaylistsManagerService {
|
||||
public $playlistsFolderLinkState(version: BSVersion): Observable<FolderLinkState> {
|
||||
return this.linker.$folderLinkedState(version, PlaylistsManagerService.RELATIVE_PLAYLISTS_FOLDER);
|
||||
}
|
||||
|
||||
public $onPlaylistImported(version?: BSVersion): Observable<LocalBPListsDetails> {
|
||||
return this.lastPlaylistImported$.pipe(
|
||||
filter((val) => val?.playlist && equal(val.version, version)),
|
||||
map((val) => val.playlist)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,10 @@ export default function App() {
|
||||
const appVersion = await lastValueFrom(autoUpdater.getAppVersion());
|
||||
const lastAppVersion = autoUpdater.getLastAppVersion();
|
||||
|
||||
if(!lastAppVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastAppVersion.toLowerCase().includes("alpha")) {
|
||||
autoUpdater.setLastAppVersion("0"); // Reset last app version if it's an alpha version for users with alpha versions in the var stored.
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { getLocalTimeZone, parseAbsolute, toCalendarDateTime } from "@internationalized/date";
|
||||
import { MapItemComponentProps } from "renderer/components/maps-playlists-panel/maps/map-item.component";
|
||||
import { BsvMapDetail, RawMapInfoData, SongDetailDiffCharactertistic, SongDetails, SongDiffName } from "shared/models/maps";
|
||||
import { BsvMapDetail, SongDetailDiffCharactertistic, SongDetails, SongDiffName } from "shared/models/maps";
|
||||
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
|
||||
import { MapInfo } from "shared/models/maps/info/map-info.model";
|
||||
|
||||
export type ParsedMapDiff = { name: SongDiffName; libelle: string; stars: number, nps: number, njs: number };
|
||||
|
||||
export abstract class MapItemComponentPropsMapper {
|
||||
|
||||
public static extractMapDiffs({rawMapInfo, songDetails, bsvMap}: {rawMapInfo?: RawMapInfoData, songDetails?: SongDetails, bsvMap?: BsvMapDetail}): Map<SongDetailDiffCharactertistic, ParsedMapDiff[]> {
|
||||
public static extractMapDiffs({mapInfo, songDetails, bsvMap}: {mapInfo?: MapInfo, songDetails?: SongDetails, bsvMap?: BsvMapDetail}): Map<SongDetailDiffCharactertistic, ParsedMapDiff[]> {
|
||||
const res = new Map<SongDetailDiffCharactertistic, ParsedMapDiff[]>();
|
||||
|
||||
if (bsvMap?.versions?.at(0)?.diffs) {
|
||||
@@ -22,20 +23,18 @@ export abstract class MapItemComponentPropsMapper {
|
||||
if (songDetails?.difficulties) {
|
||||
songDetails?.difficulties.forEach(diff => {
|
||||
const arr = res.get(diff.characteristic) || [];
|
||||
const diffName = rawMapInfo?._difficultyBeatmapSets?.find(set => set._beatmapCharacteristicName === diff.characteristic)._difficultyBeatmaps.find(rawDiff => rawDiff._difficulty === diff.difficulty)?._customData?._difficultyLabel || diff.difficulty;
|
||||
const diffName = mapInfo?.difficulties?.find(mapDiff => mapDiff.characteristic === diff.characteristic && mapDiff.difficulty === diff.difficulty)?.difficultyLabel || diff.difficulty;
|
||||
arr.push({ libelle: diffName, name: diff.difficulty, stars: diff.stars, nps: diff.nps, njs: diff.njs });
|
||||
res.set(diff.characteristic, arr);
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
if(rawMapInfo?._difficultyBeatmapSets){
|
||||
rawMapInfo._difficultyBeatmapSets.forEach(set => {
|
||||
set._difficultyBeatmaps.forEach(diff => {
|
||||
const arr = res.get(set._beatmapCharacteristicName) || [];
|
||||
arr.push({ libelle: diff._customData?._difficultyLabel || diff._difficulty, name: diff._difficulty, stars: null, nps: null, njs: diff._noteJumpMovementSpeed });
|
||||
res.set(set._beatmapCharacteristicName, arr);
|
||||
});
|
||||
if(mapInfo?.difficulties){
|
||||
mapInfo.difficulties.forEach(diff => {
|
||||
const arr = res.get(diff.characteristic) || [];
|
||||
arr.push({ libelle: diff.difficultyLabel || diff.difficulty, name: diff.difficulty, stars: null, nps: null, njs: diff.noteJumpMovementSpeed });
|
||||
res.set(diff.characteristic, arr);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -45,14 +44,14 @@ export abstract class MapItemComponentPropsMapper {
|
||||
public static fromBsmLocalMap(map: BsmLocalMap): MapItemComponentProps<BsmLocalMap> {
|
||||
return {
|
||||
hash: map.hash,
|
||||
title: map.rawInfo._songName,
|
||||
title: map.mapInfo.songName,
|
||||
coverUrl: map.coverUrl,
|
||||
songUrl: map.songUrl,
|
||||
autor: map.rawInfo._levelAuthorName,
|
||||
songAutor: map.rawInfo._songAuthorName,
|
||||
bpm: map.rawInfo._beatsPerMinute,
|
||||
autor: map.mapInfo.levelMappers.at(0),
|
||||
songAutor: map.mapInfo.songAuthorName,
|
||||
bpm: map.mapInfo.beatsPerMinute,
|
||||
duration: map.songDetails?.duration,
|
||||
diffs: MapItemComponentPropsMapper.extractMapDiffs({ rawMapInfo: map.rawInfo, songDetails: map.songDetails }),
|
||||
diffs: MapItemComponentPropsMapper.extractMapDiffs({ mapInfo: map.mapInfo, songDetails: map.songDetails }),
|
||||
mapId: map.songDetails?.id,
|
||||
ranked: map.songDetails?.ranked,
|
||||
blRanked: map.songDetails?.blRanked,
|
||||
@@ -102,7 +101,7 @@ export abstract class MapItemComponentPropsMapper {
|
||||
}
|
||||
|
||||
public static from(mapDetails: BsmLocalMap|BsvMapDetail|SongDetails): MapItemComponentProps<BsmLocalMap|BsvMapDetail|SongDetails> {
|
||||
if ((mapDetails as BsmLocalMap).rawInfo) {
|
||||
if ((mapDetails as BsmLocalMap).mapInfo) {
|
||||
return MapItemComponentPropsMapper.fromBsmLocalMap(mapDetails as BsmLocalMap) as MapItemComponentProps<BsmLocalMap|BsvMapDetail|SongDetails>;
|
||||
}
|
||||
if ((mapDetails as BsvMapDetail).metadata) {
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
export class CustomError extends Error {
|
||||
|
||||
private readonly _code: string;
|
||||
private readonly _data?: unknown;
|
||||
public readonly code: string;
|
||||
public readonly data?: unknown;
|
||||
|
||||
constructor(message: string, code: string, data?: unknown){
|
||||
super(message);
|
||||
this._code = code;
|
||||
this._data = data;
|
||||
this.code = code;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public get code(): string { return this._code; }
|
||||
public get data(): unknown { return this._data; }
|
||||
|
||||
public static fromError(error: Error, code?: string, data?: unknown): CustomError{
|
||||
|
||||
if(error instanceof CustomError){
|
||||
@@ -33,4 +30,4 @@ export class CustomError extends Error {
|
||||
|
||||
export enum CustomErrorCodes {
|
||||
UNKNOWN_ERROR = "UNKNOWN_ERROR",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { BsmLocalModel } from "../models/bsm-local-model.interface";
|
||||
import { Mod } from "../mods";
|
||||
import { BPList, DownloadPlaylistProgressionData } from "../playlists/playlist.interface";
|
||||
import { VersionLinkerAction } from "renderer/services/version-folder-linker.service";
|
||||
import { FileFilter, OpenDialogReturnValue } from "electron";
|
||||
import { FileFilter, OpenDialogOptions, OpenDialogReturnValue } from "electron";
|
||||
import { SystemNotificationOptions } from "../notification/system-notification.model";
|
||||
import { Supporter } from "../supporters";
|
||||
import { AppWindow } from "../window-manager/app-window.model";
|
||||
@@ -58,7 +58,8 @@ export interface IpcChannelMapping {
|
||||
"load-version-maps": { request: BSVersion, response: BsmLocalMapsProgress};
|
||||
"delete-maps": { request: BsmLocalMap[], response: DeleteMapsProgress };
|
||||
"export-maps": { request: { version: BSVersion; maps: BsmLocalMap[]; outPath: string }, response: Progression };
|
||||
"download-map": { request: { map: BsvMapDetail; version: BSVersion }, response: BsmLocalMap };
|
||||
"bs-maps.import-maps": { request: { paths: string[]; version: BSVersion | undefined }, response: Progression<BsmLocalMap> };
|
||||
"bs-maps.download-map": { request: { map: BsvMapDetail; version: BSVersion | undefined }, response: BsmLocalMap };
|
||||
"last-downloaded-map": { request: void, response: { version?: BSVersion, map: BsmLocalMap } };
|
||||
"one-click-install-map": { request: BsvMapDetail, response: void };
|
||||
"register-maps-deep-link": { request: void, response: boolean };
|
||||
@@ -92,6 +93,7 @@ export interface IpcChannelMapping {
|
||||
"get-version-playlists-details": {request: BSVersion, response: Progression<LocalBPListsDetails[]>};
|
||||
"delete-playlist": {request: {version: BSVersion, bpList: LocalBPList, deleteMaps?: boolean}, response: Progression};
|
||||
"export-playlists": {request: {version?: BSVersion, bpLists: LocalBPList[], dest: string, playlistsMaps?: BsmLocalMap[]}, response: Progression<string>};
|
||||
"import-playlists": {request: {version?: BSVersion, paths: string[]}, response: Progression<LocalBPListsDetails>};
|
||||
"install-playlist-file": {request: {bplist: BPList, version?: BSVersion, dest?: string}, response: LocalBPListsDetails};
|
||||
|
||||
/* ** bs-uninstall-ipcs ** */
|
||||
@@ -121,6 +123,7 @@ export interface IpcChannelMapping {
|
||||
|
||||
/* ** os-controls-ipcs ** */
|
||||
"new-window": { request: string, response: void };
|
||||
"open-dialog": { request: OpenDialogOptions, response: OpenDialogReturnValue };
|
||||
"choose-folder": { request: string, response: OpenDialogReturnValue };
|
||||
"choose-file": { request: string, response: OpenDialogReturnValue }
|
||||
"choose-image": { request: { multiple?: boolean, base64?: boolean }, response: string[] }
|
||||
|
||||
@@ -147,42 +147,42 @@ export interface BsvMapParitySummary {
|
||||
}
|
||||
|
||||
export enum MapStyle {
|
||||
Dance = "dance",
|
||||
Swing = "swing",
|
||||
Nightcore = "nightcore",
|
||||
Folk = "folk",
|
||||
Family = "family",
|
||||
Alt = "alternative",
|
||||
Ambient = "ambient",
|
||||
Funk = "funk",
|
||||
Anime = "anime",
|
||||
ClassicalOrchestral = "classical-orchestral",
|
||||
Comedy = "comedy-meme",
|
||||
Dance = "dance",
|
||||
DrumBass = "drum-and-bass",
|
||||
Dubstep = "dubstep",
|
||||
Electronic = "electronic",
|
||||
Folk = "folk-acoustic",
|
||||
Funk = "funk-disco",
|
||||
Hardcore = "hardcore",
|
||||
Hiphop = "hip-hop-rap",
|
||||
Holiday = "holiday",
|
||||
House = "house",
|
||||
Indie = "indie",
|
||||
Instrumental = "instrumental",
|
||||
JPop = "j-pop",
|
||||
JRock = "j-rock",
|
||||
Jazz = "jazz",
|
||||
Soul = "soul",
|
||||
Speedcore = "speedcore",
|
||||
KPop = "k-pop",
|
||||
Family = "kids-family",
|
||||
Metal = "metal",
|
||||
Nightcore = "nightcore",
|
||||
Pop = "pop",
|
||||
Punk = "punk",
|
||||
Rb = "rb",
|
||||
Holiday = "holiday",
|
||||
Vocaloid = "vocaloid",
|
||||
JRock = "j-rock",
|
||||
Trance = "trance",
|
||||
DrumBass = "drumbass",
|
||||
Comedy = "comedy",
|
||||
Instrumental = "instrumental",
|
||||
Hardcore = "hardcore",
|
||||
KPop = "k-pop",
|
||||
Indie = "indie",
|
||||
Techno = "techno",
|
||||
House = "house",
|
||||
Game = "game",
|
||||
Film = "film",
|
||||
Alt = "alt",
|
||||
Dubstep = "dubstep",
|
||||
Metal = "metal",
|
||||
Anime = "anime",
|
||||
Hiphop = "hiphop",
|
||||
JPop = "j-pop",
|
||||
Rock = "rock",
|
||||
Pop = "pop",
|
||||
Electronic = "electronic",
|
||||
ClassicalOrchestral = "classical-orchestral"
|
||||
Soul = "soul",
|
||||
Speedcore = "speedcore",
|
||||
Swing = "swing",
|
||||
Film = "tv-movie-soundtrack",
|
||||
Techno = "techno",
|
||||
Trance = "trance",
|
||||
Game = "video-game-soundtrack",
|
||||
Vocaloid = "vocaloid",
|
||||
}
|
||||
|
||||
// MapType Enum
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { RawMapInfoData } from "./raw-map.model";
|
||||
import { MapInfo } from "./info/map-info.model";
|
||||
import { SongDetails } from "./song-details-cache/song-details-cache.model";
|
||||
|
||||
export interface BsmLocalMap {
|
||||
hash: string;
|
||||
coverUrl: string;
|
||||
songUrl: string;
|
||||
rawInfo: RawMapInfoData;
|
||||
mapInfo: MapInfo;
|
||||
songDetails?: SongDetails;
|
||||
path: string;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export { RawMapInfoData, RawMapDifficulty, RawDifficultySet } from "./raw-map.model";
|
||||
export { BsvInstant, BsvMapDetail, BsvMapDetailMetadata, BsvMapDifficulty, BsvMapParitySummary, BsvMapStats, BsvMapTestplay, BsvMapVersion, BsvUserDetail } from "./beat-saver.model";
|
||||
export { SongDetails, SongDifficulty, SongUploader, SongDetailDiffCharactertistic, SongDiffName } from "./song-details-cache/song-details-cache.model";
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { SongDetailDiffCharactertistic, SongDiffName } from "../song-details-cache/song-details-cache.model";
|
||||
import { RawMapInfoDataV2 } from "./raw-map-info-v2.model";
|
||||
import { RawMapInfoDataV4 } from "./raw-map-info-v4.model";
|
||||
|
||||
export type AnyRawMapInfo = RawMapInfoDataV2 | RawMapInfoDataV4;
|
||||
|
||||
// interfaces/mapInfo.ts
|
||||
export interface MapInfo {
|
||||
version: string;
|
||||
songName: string;
|
||||
songSubName?: string;
|
||||
songAuthorName: string;
|
||||
levelMappers: string[];
|
||||
levelLighters: string[];
|
||||
beatsPerMinute: number;
|
||||
shuffle?: number;
|
||||
shufflePeriod?: number;
|
||||
previewStartTime: number;
|
||||
previewDuration: number;
|
||||
songFilename: string;
|
||||
songPreviewFilename: string;
|
||||
coverImageFilename: string;
|
||||
environmentNames: string[];
|
||||
difficulties: MapDifficulty[];
|
||||
}
|
||||
|
||||
export interface MapDifficulty {
|
||||
characteristic: SongDetailDiffCharactertistic;
|
||||
difficulty: SongDiffName;
|
||||
difficultyLabel?: string;
|
||||
beatmapFilename: string;
|
||||
noteJumpMovementSpeed: number;
|
||||
noteJumpStartBeatOffset: number;
|
||||
beatmapColorSchemeIdx?: number;
|
||||
environmentNameIdx?: number;
|
||||
// Additional fields from version 4.0.0
|
||||
beatmapAuthors?: {
|
||||
mappers: string[];
|
||||
lighters: string[];
|
||||
};
|
||||
lightshowDataFilename?: string;
|
||||
}
|
||||
+13
-13
@@ -1,6 +1,6 @@
|
||||
import { SongDetailDiffCharactertistic, SongDiffName } from "./song-details-cache/song-details-cache.model";
|
||||
import { SongDetailDiffCharactertistic, SongDiffName } from "../song-details-cache/song-details-cache.model";
|
||||
|
||||
export interface RawMapInfoData<T = unknown> {
|
||||
export interface RawMapInfoDataV2 {
|
||||
_version: string;
|
||||
_songName: string;
|
||||
_songSubName: string;
|
||||
@@ -16,24 +16,24 @@ export interface RawMapInfoData<T = unknown> {
|
||||
_environmentName: string;
|
||||
_allDirectionsEnvironmentName: string;
|
||||
_songTimeOffset: number;
|
||||
_customData: T;
|
||||
_difficultyBeatmapSets: RawDifficultySet[];
|
||||
_difficultyBeatmapSets: RawDifficultySetV2[];
|
||||
// Additional fields for 2.1.0
|
||||
_environmentNames?: string[];
|
||||
_colorSchemes?: unknown[];
|
||||
}
|
||||
|
||||
export interface RawDifficultySet {
|
||||
interface RawDifficultySetV2 {
|
||||
_beatmapCharacteristicName: SongDetailDiffCharactertistic;
|
||||
_difficultyBeatmaps: RawMapDifficulty[];
|
||||
_difficultyBeatmaps: RawMapDifficultyV2[];
|
||||
}
|
||||
|
||||
export interface RawMapDifficulty {
|
||||
interface RawMapDifficultyV2 {
|
||||
_difficulty: SongDiffName;
|
||||
_difficultyRank: string;
|
||||
_difficultyRank: number;
|
||||
_beatmapFilename: string;
|
||||
_noteJumpMovementSpeed: number;
|
||||
_noteJumpStartBeatOffset: number;
|
||||
_customData?: RawMapDifficultyCustomData;
|
||||
}
|
||||
|
||||
export interface RawMapDifficultyCustomData {
|
||||
_difficultyLabel?: string;
|
||||
_beatmapColorSchemeIdx?: number;
|
||||
_environmentNameIdx?: number;
|
||||
_customData?: { _difficultyLabel?: string; };
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { SongDetailDiffCharactertistic, SongDiffName } from "../song-details-cache/song-details-cache.model";
|
||||
|
||||
// interfaces/version4.ts
|
||||
export interface RawMapInfoDataV4 {
|
||||
version: string;
|
||||
song: {
|
||||
title: string;
|
||||
subTitle: string;
|
||||
author: string;
|
||||
};
|
||||
audio: {
|
||||
songFilename: string;
|
||||
songDuration: number;
|
||||
audioDataFilename: string;
|
||||
bpm: number;
|
||||
lufs: number;
|
||||
previewStartTime: number;
|
||||
previewDuration: number;
|
||||
};
|
||||
songPreviewFilename: string;
|
||||
coverImageFilename: string;
|
||||
environmentNames: string[];
|
||||
colorSchemes: unknown[];
|
||||
difficultyBeatmaps: RawMapDifficultyV4[];
|
||||
}
|
||||
|
||||
interface RawMapDifficultyV4 {
|
||||
characteristic: SongDetailDiffCharactertistic;
|
||||
difficulty: SongDiffName;
|
||||
beatmapAuthors: {
|
||||
mappers: string[];
|
||||
lighters: string[];
|
||||
};
|
||||
environmentNameIdx: number;
|
||||
beatmapColorSchemeIdx: number;
|
||||
noteJumpMovementSpeed: number;
|
||||
noteJumpStartBeatOffset: number;
|
||||
beatmapDataFilename: string;
|
||||
lightshowDataFilename: string;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { MapDifficulty, MapInfo, AnyRawMapInfo } from "shared/models/maps/info/map-info.model";
|
||||
import { RawMapInfoDataV2 } from "shared/models/maps/info/raw-map-info-v2.model";
|
||||
import { RawMapInfoDataV4 } from "shared/models/maps/info/raw-map-info-v4.model";
|
||||
|
||||
function parseVersion2(data: RawMapInfoDataV2): MapInfo {
|
||||
return {
|
||||
version: data._version,
|
||||
songName: data._songName,
|
||||
songSubName: data._songSubName,
|
||||
songAuthorName: data._songAuthorName,
|
||||
levelMappers: [data._levelAuthorName],
|
||||
levelLighters: [],
|
||||
beatsPerMinute: data._beatsPerMinute,
|
||||
shuffle: data._shuffle,
|
||||
shufflePeriod: data._shufflePeriod,
|
||||
previewStartTime: data._previewStartTime,
|
||||
previewDuration: data._previewDuration,
|
||||
songFilename: data._songFilename,
|
||||
songPreviewFilename: data._songFilename,
|
||||
coverImageFilename: data._coverImageFilename,
|
||||
environmentNames: data._environmentNames || [data._environmentName],
|
||||
difficulties: data._difficultyBeatmapSets.flatMap(set =>
|
||||
set._difficultyBeatmaps.map<MapDifficulty>((diff) => ({
|
||||
characteristic: set._beatmapCharacteristicName,
|
||||
difficulty: diff._difficulty,
|
||||
difficultyLabel: diff._customData?._difficultyLabel,
|
||||
beatmapFilename: diff._beatmapFilename,
|
||||
noteJumpMovementSpeed: diff._noteJumpMovementSpeed,
|
||||
noteJumpStartBeatOffset: diff._noteJumpStartBeatOffset,
|
||||
beatmapColorSchemeIdx: diff._beatmapColorSchemeIdx,
|
||||
environmentNameIdx: diff._environmentNameIdx,
|
||||
}))
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function parseVersion4(data: RawMapInfoDataV4): MapInfo {
|
||||
return {
|
||||
version: data.version,
|
||||
songName: data.song.title,
|
||||
songSubName: data.song.subTitle,
|
||||
songAuthorName: data.song.author,
|
||||
levelMappers: data.difficultyBeatmaps.flatMap(diff => diff.beatmapAuthors.mappers),
|
||||
levelLighters: data.difficultyBeatmaps.flatMap(diff => diff.beatmapAuthors.lighters),
|
||||
beatsPerMinute: data.audio.bpm,
|
||||
previewStartTime: data.audio.previewStartTime,
|
||||
previewDuration: data.audio.previewDuration,
|
||||
songFilename: data.audio.songFilename,
|
||||
songPreviewFilename: data.songPreviewFilename,
|
||||
coverImageFilename: data.coverImageFilename,
|
||||
environmentNames: data.environmentNames,
|
||||
difficulties: data.difficultyBeatmaps.map<MapDifficulty>(diff => ({
|
||||
characteristic: diff.characteristic,
|
||||
difficulty: diff.difficulty,
|
||||
beatmapFilename: diff.beatmapDataFilename,
|
||||
noteJumpMovementSpeed: diff.noteJumpMovementSpeed,
|
||||
noteJumpStartBeatOffset: diff.noteJumpStartBeatOffset,
|
||||
beatmapColorSchemeIdx: diff.beatmapColorSchemeIdx,
|
||||
environmentNameIdx: diff.environmentNameIdx,
|
||||
beatmapAuthors: diff.beatmapAuthors,
|
||||
lightshowDataFilename: diff.lightshowDataFilename,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function parseMapInfoDat(info: AnyRawMapInfo): MapInfo | never {
|
||||
const version = (info as RawMapInfoDataV2)?._version || (info as RawMapInfoDataV4)?.version;
|
||||
|
||||
if(!version) {
|
||||
throw new Error('Cannot determine info.dat version');
|
||||
}
|
||||
|
||||
if (version.startsWith('2.')) {
|
||||
return parseVersion2(info as RawMapInfoDataV2);
|
||||
}
|
||||
|
||||
if (version.startsWith('4.')) {
|
||||
return parseVersion4(info as RawMapInfoDataV4);
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported info.dat version: ${version}`);
|
||||
}
|
||||
Reference in New Issue
Block a user