Compare commits

...

16 Commits

Author SHA1 Message Date
MathieuG-P 992f41238c Update to 1.4.14 2024-12-02 20:26:03 +01:00
MathieuG-P 17d7b4ee31 Fix depotdownloader file validation broken 2024-12-02 20:25:05 +01:00
MathieuG-P 0e7ae4dd99 Merge pull request #679 from beatmaps-io/signed-playlists
Pass query parameters to playlist downloader

(cherry picked from commit 127a538509)
2024-12-02 20:15:08 +01:00
MathieuG-P ca1049977e change certificate sha 2024-11-26 20:01:46 +01:00
MathieuG-P 46684fb757 Merge pull request #673 from Zagrios/chore/add-bs-version-outated-alert-in-version-view
[chore] added a beat saber version outdated warning in the version view

(cherry picked from commit 877879f335)
2024-11-25 21:20:53 +01:00
MathieuG-P b620cf19b5 Merge pull request #671 from Zagrios/bugfix/close-btn-supporters-view-wrong-color
[bugfix] Close button in the supporters view had the wrong color in the white theme

(cherry picked from commit 229a149f00)
2024-11-25 21:04:08 +01:00
MathieuG-P e40e010ab5 Merge pull request #667 from Zagrios/bugfix/do-no-apply-no-yeet-on-original-copies-of-bs
[bugfix] Do not launch BS with the --no-yeet arg when launching original copies of BS (Steam/Oculus)

(cherry picked from commit fd30709af9)
2024-11-25 21:03:37 +01:00
MathieuG-P efebd221c9 Merge pull request #666 from Zagrios/feature/add-unselect-all-mods-button
[feature] add "unselect all" action in mods panel

(cherry picked from commit c73bde2cd9)
2024-11-25 21:03:04 +01:00
MathieuG-P 4805e74a2a Merge pull request #662 from beatmaps-io/default-bsv-map-sort
Set default beat saver map sort order to Relevance

(cherry picked from commit 39739dfb49)
2024-11-25 20:52:41 +01:00
MathieuG-P 52d9fe4cf2 Merge pull request #661 from beatmaps-io/master
Use correct beatsaver api url

(cherry picked from commit c9286c5419)
2024-11-25 20:45:48 +01:00
MathieuG-P dff827a06c Merge pull request #660 from chk1/bugfix/658
[bugfix-658] Change background color of Mods search box for light mode

(cherry picked from commit 578e79edac)
2024-11-25 20:45:26 +01:00
MathieuG-P e6ee58b560 Merge pull request #657 from LiamillionSS/bugfix/527
[bugfix/527] Prevents oculus version from launching if the original oculus version is not present

(cherry picked from commit e1f421831b)
2024-11-25 20:44:54 +01:00
MathieuG-P 7c871f84a4 Merge pull request #654 from Zagrios/chore/add-outdate-bs-version-warn-modal
[chore] Added a pre-download warning modal for outdated BS versions

(cherry picked from commit e17b7f70fd)
2024-11-25 20:42:20 +01:00
MathieuG-P 6aed7810d8 Merge pull request #653 from Zagrios/chore/add-beatleader-icon-to-playlist-oneclickt-setting
[chore] Added beatleader icon to playlist oneclick setting + change tippys color

(cherry picked from commit 6c39e5f43d)
2024-11-25 20:31:09 +01:00
MathieuG-P 6f332f66bc Merge pull request #649 from silentrald/bugfix/non-standard-zip-extraction
[bugfix] handle extracting non-conformant zip files

(cherry picked from commit f7f5f768fc)
2024-11-25 20:30:29 +01:00
MathieuG-P 6dde2fa5d0 update to v1.4.13 2024-11-25 19:46:57 +01:00
62 changed files with 12100 additions and 5264 deletions
+13
View File
@@ -52,6 +52,19 @@ module.exports = {
Function: false,
}
}]
"import/extensions": "off",
"lines-between-class-members": "off",
"no-throw-literal": "warn",
"no-use-before-define": "off",
"no-useless-constructor": "off",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "error",
'react/jsx-filename-extension': [2, { 'extensions': ['.js', '.jsx', '.ts', '.tsx'] }],
"no-shadow": "off",
"react/function-component-definition": "off",
"jsx-a11y/control-has-associated-label": "off",
"react/button-has-type": "off",
"max-classes-per-file": "off",
},
parserOptions: {
ecmaVersion: 2020,
+2
View File
@@ -15,3 +15,5 @@
*.ttf binary
*.woff binary
*.woff2 binary
assets/scripts/* binary
*.zip binary
+5 -4
View File
@@ -13,7 +13,7 @@ jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
@@ -26,6 +26,7 @@ jobs:
cache: "npm"
- run: npm ci
- run: npm run build
- run: npm test
# Currently typescript, eslint, and prettier are unhappy
continue-on-error: true
- name: Unit tests
run: npm run test:unit
+17
View File
@@ -785,6 +785,23 @@
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/4669760442595134838",
"ReleaseImg": "https://clan.akamai.steamstatic.com/images/32055887/57cfa043407c091e67b7e0a8ae86fe5590deae33.png",
"ReleaseDate": "1728399815",
"year": "2024"
},
{
"BSVersion": "1.39.0",
"BSManifest": "5365094543246905054",
"OculusBinaryId": "7978663688900007",
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/4451339035198095638",
"ReleaseImg": "https://clan.cloudflare.steamstatic.com/images/32055887/fd8e2c783e46decdf5e10fed1e16f124616d9321.png",
"ReleaseDate": "1731426932",
"year": "2024"
},
{
"BSVersion": "1.39.1",
"BSManifest": "2179614296547473280",
"OculusBinaryId": "8189133517853022",
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/4451340304252928024",
"ReleaseDate": "1732107748",
"year": "2024",
"recommended": true
}
+32 -10
View File
@@ -14,7 +14,12 @@
"refuse": "Ablehnen",
"apply": "Übernehmen",
"copy": "Kopieren",
"copied": "Kopiert!"
"copied": "Kopiert!",
"confirm": "Bestätigen",
"choose-folder": "Ordner Wählen",
"unknown": "Ein unbekannter Fehler ist aufgetreten ¯\\_(ツ)_/¯",
"warning": "Achtung",
"continue": "Weiter"
},
"nav-bar": {
"add-version": "Version Hinzufügen",
@@ -32,7 +37,8 @@
"desktop": "FPFC Modus",
"desktop-description": "Auf diese Weise kannst du WASD und die Maus verwenden, um im Spiel durch das Menü zu navigieren. Dies erleichtert das Testen erheblich, da Du dein Headset nicht aufsetzen musst!",
"debug": "Debug Modus",
"debug-description": "Aktiviert das Ausgabeprotokollfenster für IPA. Dies zeigt die Debug-Konsole, die Mods verwenden.",
"debug-description": "Aktiviert das Ausgabeprotokollfenster für IPA. Dies zeigt die Debug-Konsole, welche die Mods verwenden.",
"outdated-tippy": "Diese Version ist veraltet, und einige Mods oder Funktionen funktionieren möglicherweise nicht wie erwartet. Es wird empfohlen, die empfohlene Version ({recommendedVersion}) von Beat Saber zu verwenden, um die neuesten Funktionen und Fehlerbehebungen zu genießen.",
"advanced-launch": {
"button": "Erweiterter Start",
"placeholder": "Weitere Argumente Bspw: --revert; --nowait"
@@ -82,7 +88,8 @@
"latest": "Neueste",
"description": "Beschreibung",
"dropdown": {
"uninstall-all": "Alle deinstallieren"
"uninstall-all": "Alle deinstallieren",
"unselect-all": "Alle abwählen"
}
}
}
@@ -380,16 +387,20 @@
"BS_ALREADY_RUNNING": "Beat Saber läuft bereits",
"EXE_NOT_FINDED": "Dateien Fehlen",
"EXIT": "Abrupter Stop",
"OCULUS_LIB_NOT_FOUND": "Oculus-Bibliothek nicht gefunden"
"OCULUS_LIB_NOT_FOUND": "Oculus-Bibliothek nicht gefunden",
"ORIGINAL_OCULUS_NOT_INSTALLED": "Original Oculus-Version nicht gefunden"
},
"msg": {
"UNKNOWN_ERROR": "Ein unbekannter Fehler ist aufgetreten.",
"STEAM_NOT_RUNNING": "Steam muss laufen, um Beat Saber zu starten.",
"OCULUS_NOT_RUNNING": "Oculus muss ausgeführt werden, um Beat Saber zu starten.",
"BS_ALREADY_RUNNING": "Schließe Beat Saber, bevor Du es erneut startest.",
"EXE_NOT_FINDED": "Einige Dateien scheinen zu fehlen, versuche die Dateien zu überprüfen.",
"EXIT": "Beat Saber stoppte abrupt, versuche die Dateien zu überprüfen.",
"OCULUS_LIB_NOT_FOUND": "Überprüfe, ob die Oculus-Anwendung korrekt installiert ist und dass die Bibliotheken in Oculus richtig definiert sind."
"BS_ALREADY_RUNNING": "Schließe Beat Saber, bevor du es erneut startest.",
"EXE_NOT_FINDED": "Einige Dateien scheinen zu fehlen. Versuche, die Dateien zu überprüfen.",
"EXIT": "Beat Saber stoppte abrupt. Versuche, die Dateien zu überprüfen.",
"OCULUS_LIB_NOT_FOUND": "Überprüfe, ob die Oculus-Anwendung korrekt installiert ist und dass die Bibliotheken in Oculus richtig definiert sind.",
"PROTON_NOT_SET": "Stellen Sie den Proton-Pfad in den Einstellungen ein.",
"PROTON_NOT_FOUND": "Stellen Sie den Proton-Pfad in den Einstellungen ein.",
"ORIGINAL_OCULUS_NOT_INSTALLED": "Beat Saber muss vor dem Öffnen über die Oculus-App installiert werden."
},
"actions": {
"STEAM_NOT_RUNNING": "Starte Steam"
@@ -500,6 +511,14 @@
"error": {
"msg": "Beim Erstellen der Verknüpfung ist ein Fehler aufgetreten."
}
},
"bs-version-oudated": {
"title": "Veraltete Version",
"msg": "Diese Version von Beat Saber ist veraltet. Verwenden Sie die empfohlene Version, um die neuesten Funktionen und Fehlerbehebungen zu nutzen.",
"actions": {
"do-not-remind": "Nicht mehr daran erinnern",
"ok": "Ok"
}
}
},
"modals": {
@@ -702,8 +721,8 @@
"original-version-backup-oculus": {
"title": "Achtung",
"body": {
"must-be-installed-once": "Sie müssen Beat Saber mindestens einmal aus dem Oculus Store installiert haben, sonst könnte sich Beat Saber nach dem Start automatisch schließen.",
"will-backup": "Um diese Version zu starten, wird der ursprüngliche Installationsordner von Beat Saber in Ihrer Oculus-Bibliothek umbenannt und beim Schließen von Beat Saber automatisch wiederhergestellt."
"must-be-installed-once": "Sie müssen Beat Saber aus dem Oculus Store auf diesem Gerät installiert haben, andernfalls wird Beat Saber nach dem Start automatisch geschlossen.",
"will-backup": "Um diese Version zu starten, wird der ursprüngliche Installationsordner von Beat Saber in deiner Oculus-Bibliothek umbenannt und beim Schließen von Beat Saber automatisch wiederhergestellt."
},
"not-remind-me": "Nicht mehr erinnern",
"understood": "Verstanden"
@@ -735,6 +754,9 @@
},
"launch-as-admin": "Als Administrator starten",
"not-remind-me": "Nicht mehr erinnern"
},
"bs-version-outdated": {
"body": "Die Version {outdatedVersion} ist veraltet, und einige Mods oder Funktionen funktionieren möglicherweise nicht mehr wie erwartet. Bitte lade die neueste empfohlene Version ({recommendedVersion}) von Beat Saber herunter, um die neuesten Funktionen und Fehlerbehebungen zu nutzen."
}
},
"maps": {
+27 -5
View File
@@ -14,7 +14,12 @@
"refuse": "Refuse",
"apply": "Apply",
"copy": "Copy",
"copied": "Copied!"
"copied": "Copied!",
"confirm": "Confirm",
"choose-folder": "Choose folder",
"unknown": "An unknown error occurred ¯\\_(ツ)_/¯",
"warning": "Warning",
"continue": "Continue"
},
"nav-bar": {
"add-version": "Add a version",
@@ -33,6 +38,7 @@
"desktop-description": "First Person Flying Controller mode allows you to use WASD and the mouse to navigate around the menu in game. This makes testing much easier, because you don't have to put on your headset!",
"debug": "Debug Mode",
"debug-description": "Enables the output log window for IPA. This will show the debug console that mods use.",
"outdated-tippy": "This version is outdated, and some mods or features may no longer work as expected. Prefer using the recommended version ({recommendedVersion}) of Beat Saber to enjoy the latest features and bugfixes.",
"advanced-launch": {
"button": "Advanced launch",
"placeholder": "Additional arguments ex: --revert; --nowait"
@@ -82,7 +88,8 @@
"latest": "Latest",
"description": "Description",
"dropdown": {
"uninstall-all": "Uninstall all"
"uninstall-all": "Uninstall all",
"unselect-all": "Unselect all"
}
}
}
@@ -380,7 +387,8 @@
"BS_ALREADY_RUNNING": "Beat Saber already running",
"EXE_NOT_FINDED": "Missing files",
"EXIT": "Abrupt stop",
"OCULUS_LIB_NOT_FOUND": "Oculus library not found"
"OCULUS_LIB_NOT_FOUND": "Oculus library not found",
"ORIGINAL_OCULUS_NOT_INSTALLED": "Original Oculus version not found"
},
"msg": {
"UNKNOWN_ERROR": "An unknown error has occurred.",
@@ -389,7 +397,10 @@
"BS_ALREADY_RUNNING": "Close BeatSaber before launching it again.",
"EXE_NOT_FINDED": "Some files seem to be missing, try to verify the files.",
"EXIT": "BeatSaber stopped abruptly, tries to check the files.",
"OCULUS_LIB_NOT_FOUND": "Check that the Oculus application is properly installed, and that the libraries are correctly defined in Oculus."
"OCULUS_LIB_NOT_FOUND": "Check that the Oculus application is properly installed, and that the libraries are correctly defined in Oculus.",
"PROTON_NOT_SET": "Set the Proton path in settings.",
"PROTON_NOT_FOUND": "Set the Proton path in settings.",
"ORIGINAL_OCULUS_NOT_INSTALLED": "Beat Saber must be installed from the Oculus app before opening."
},
"actions": {
"STEAM_NOT_RUNNING": "Launch Steam"
@@ -500,6 +511,14 @@
"error": {
"msg": "An error occurred while creating the shortcut."
}
},
"bs-version-oudated": {
"title": "Version Outdated",
"msg": "This Beat Saber version is outdated, use the recommended version to enjoy the latest features and bugfixes",
"actions": {
"do-not-remind": "Do not remind me",
"ok": "Ok"
}
}
},
"modals": {
@@ -702,7 +721,7 @@
"original-version-backup-oculus": {
"title": "Warning",
"body": {
"must-be-installed-once": "You must have installed Beat Saber at least once from the Oculus Store, otherwise Beat Saber might automatically close after launching.",
"must-be-installed-once": "You must have Beat Saber installed from the Oculus Store on this device, otherwise Beat Saber might automatically close after launching.",
"will-backup": "To launch this version, the original installation folder of Beat Saber located in your Oculus library will be renamed and will be automatically restored when Beat Saber is closed."
},
"not-remind-me": "Do not remind me",
@@ -735,6 +754,9 @@
},
"launch-as-admin": "Launch as Administrator",
"not-remind-me": "Do not remind me"
},
"bs-version-outdated": {
"body": "The version {outdatedVersion} is outdated, and some mods or features may no longer work as expected. Please download the latest recommended version ({recommendedVersion}) of Beat Saber to enjoy the latest features and bugfixes."
}
},
"maps": {
+27 -5
View File
@@ -14,7 +14,12 @@
"refuse": "Rechazar",
"apply": "Aplicar",
"copy": "Copiar",
"copied": "¡Copiado!"
"copied": "¡Copiado!",
"confirm": "Confirmar",
"choose-folder": "Elige la carpeta",
"unknown": "Se ha producido un error desconocido ¯\\_(ツ)_/¯",
"warning": "Advertencia",
"continue": "Continuar"
},
"nav-bar": {
"add-version": "Agregar una versión",
@@ -33,6 +38,7 @@
"desktop-description": "El modo FPFC te permite usar WASD y el mouse para navegar por el menú en juego. Esto hace que las pruebas sean mucho más fáciles, ¡porque no tienes que ponerte las gafas VR!",
"debug": "Modo Depuración",
"debug-description": "Habilita la ventana de registro de salida para IPA. Esto mostrará la consola de depuración que usan los mods.",
"outdated-tippy": "Esta versión está desactualizada, y algunos mods o funciones pueden no funcionar como se espera. Es mejor usar la versión recomendada ({recommendedVersion}) de Beat Saber para disfrutar de las últimas características y correcciones.",
"advanced-launch": {
"button": "Opciones avanzadas",
"placeholder": "Argumentos adicionales ej: --revert; --no-wait"
@@ -82,7 +88,8 @@
"latest": "Último",
"description": "Descripción",
"dropdown": {
"uninstall-all": "Desinstalar todos"
"uninstall-all": "Desinstalar todos",
"unselect-all": "Deseleccionar todo"
}
}
}
@@ -380,7 +387,8 @@
"BS_ALREADY_RUNNING": "Beat Saber ya está en marcha",
"EXE_NOT_FINDED": "Archivos no encontrados",
"EXIT": "Parada abrupta",
"OCULUS_LIB_NOT_FOUND": "Librería Oculus no encontrada"
"OCULUS_LIB_NOT_FOUND": "Librería Oculus no encontrada",
"ORIGINAL_OCULUS_NOT_INSTALLED": "No se encontró la versión original de Oculus"
},
"msg": {
"UNKNOWN_ERROR": "Se ha producido un error desconocido.",
@@ -389,7 +397,10 @@
"BS_ALREADY_RUNNING": "Cierra Beat Saber antes de volver a iniciarlo.",
"EXE_NOT_FINDED": "Parece que faltan algunos archivos, intenta verificar los archivos.",
"EXIT": "Beat Saber se detuvo abruptamente, trata de verificar los archivos.",
"OCULUS_LIB_NOT_FOUND": "Verifica que la aplicación Oculus esté correctamente instalada y que las librerías estén bien definidas en Oculus."
"OCULUS_LIB_NOT_FOUND": "Verifica que la aplicación Oculus esté correctamente instalada y que las librerías estén bien definidas en Oculus.",
"PROTON_NOT_SET": "Establece la ruta de Proton en la configuración.",
"PROTON_NOT_FOUND": "Establece la ruta de Proton en la configuración.",
"ORIGINAL_OCULUS_NOT_INSTALLED": "Beat Saber debe instalarse desde la aplicación de Oculus antes de abrirlo."
},
"actions": {
"STEAM_NOT_RUNNING": "Iniciar Steam"
@@ -500,6 +511,14 @@
"error": {
"msg": "Se produjo un error al crear el acceso directo."
}
},
"bs-version-oudated": {
"title": "Versión desactualizada",
"msg": "Esta versión de Beat Saber está desactualizada. Usa la versión recomendada para disfrutar de las últimas características y correcciones.",
"actions": {
"do-not-remind": "No volver a recordármelo",
"ok": "Ok"
}
}
},
"modals": {
@@ -702,7 +721,7 @@
"original-version-backup-oculus": {
"title": "Atención",
"body": {
"must-be-installed-once": "Debes haber instalado Beat Saber al menos una vez desde la Oculus Store, de lo contrario, Beat Saber podría cerrarse automáticamente después de iniciar.",
"must-be-installed-once": "Debes tener Beat Saber instalado desde la tienda de Oculus en este dispositivo, de lo contrario, Beat Saber se cerrará automáticamente después de iniciarse.",
"will-backup": "Para lanzar esta versión, la carpeta de instalación original de Beat Saber ubicada en tu biblioteca de Oculus será renombrada y se restaurará automáticamente al cerrar Beat Saber."
},
"not-remind-me": "No volver a recordármelo",
@@ -735,6 +754,9 @@
},
"launch-as-admin": "Iniciar como Administrador",
"not-remind-me": "No volver a recordármelo"
},
"bs-version-outdated": {
"body": "La versión {outdatedVersion} está obsoleta, y algunos mods o funciones pueden no funcionar como se espera. Por favor, descarga la última versión recomendada ({recommendedVersion}) de Beat Saber para disfrutar de las últimas funciones y correcciones de errores."
}
},
"maps": {
+28 -6
View File
@@ -14,7 +14,12 @@
"refuse": "Refuser",
"apply": "Appliquer",
"copy": "Copier",
"copied": "Copié !"
"copied": "Copié !",
"confirm": "Confirmer",
"choose-folder": "Choisir un dossier",
"unknown": "Une erreur inconnue s'est produite ¯\\_(ツ)_/¯",
"warning": "Attention",
"continue": "Continuer"
},
"nav-bar": {
"add-version": "Ajouter une version",
@@ -33,6 +38,7 @@
"desktop-description": "Cela vous permet d'utiliser WASD et la souris pour naviguer dans le menu en jeu. Cela rend les tests beaucoup plus faciles, car vous n'avez pas à mettre votre casque!",
"debug": "Mode Debug",
"debug-description": "Active la fenêtre de log pour IPA. Cela affichera la console de débogage utilisée par les mods.",
"outdated-tippy": "Cette version est obsolète et certains mods ou fonctionnalités peuvent ne plus fonctionner comme prévu. Préférez utiliser la version recommandée ({recommendedVersion}) de Beat Saber pour profiter des dernières fonctionnalités et correctifs.",
"advanced-launch": {
"button": "Lancement avancé",
"placeholder": "Arguments supplémentaires ex: --revert; --nowait"
@@ -82,7 +88,8 @@
"latest": "Récent",
"description": "Description",
"dropdown": {
"uninstall-all": "Tout désinstaller"
"uninstall-all": "Tout désinstaller",
"unselect-all": "Tout désélectionner"
}
}
}
@@ -380,7 +387,8 @@
"BS_ALREADY_RUNNING": "Beat Saber est déjà lancé",
"EXE_NOT_FINDED": "Fichiers manquants",
"EXIT": "Arrêt brutal",
"OCULUS_LIB_NOT_FOUND": "Librairie Oculus introuvable"
"OCULUS_LIB_NOT_FOUND": "Librairie Oculus introuvable",
"ORIGINAL_OCULUS_NOT_INSTALLED": "Version originale Oculus non trouvée"
},
"msg": {
"UNKNOWN_ERROR": "Une erreur inconnue s'est produite.",
@@ -389,7 +397,10 @@
"BS_ALREADY_RUNNING": "Ferme Beat Saber avant de le lancer à nouveau.",
"EXE_NOT_FINDED": "Quelques fichiers semblent manquants. Essaye de vérifier les fichiers.",
"EXIT": "Beat Saber s'est arrêté brusquement, essaye de vérifier les fichiers.",
"OCULUS_LIB_NOT_FOUND": "Vérifie que l'application Oculus est correctement installée, et que les librairies sont bien définies dans Oculus."
"OCULUS_LIB_NOT_FOUND": "Vérifie que l'application Oculus est correctement installée, et que les librairies sont bien définies dans Oculus.",
"PROTON_NOT_SET": "Définissez le chemin de Proton dans les paramètres.",
"PROTON_NOT_FOUND": "Définissez le chemin de Proton dans les paramètres.",
"ORIGINAL_OCULUS_NOT_INSTALLED": "Beat Saber doit être installé depuis l'application Oculus avant de l'ouvrir."
},
"actions": {
"STEAM_NOT_RUNNING": "Lancer Steam"
@@ -501,6 +512,14 @@
"error": {
"msg": "Une erreur s'est produite lors de la création du raccourci."
}
},
"bs-version-oudated": {
"title": "Version obsolète",
"msg": "Cette version de Beat Saber est obsolète. Utilisez la version recommandée pour profiter des dernières fonctionnalités et correctifs.",
"actions": {
"do-not-remind": "Ne plus me rappeler",
"ok": "Ok"
}
}
},
"modals": {
@@ -684,7 +703,7 @@
},
"create-launch-shortcut": {
"title": "Créer un raccourci",
"desc": "Créer un raccourci te permettra de démarrer Beat Saber avec les options choisi sans passé par BSManager.",
"desc": "Créer un raccourci te permettra de démarrer Beat Saber avec les options choisies sans passer par BSManager.",
"launch-options": "Options de lancement",
"advanced-launch": "Lancement avancé",
"valid-btn": "Créer le raccourci"
@@ -703,7 +722,7 @@
"original-version-backup-oculus": {
"title": "Attention",
"body": {
"must-be-installed-once": "Vous devez avoir installé au moins une fois Beat Saber depuis l'Oculus Store, sinon Beat Saber pourrait se fermer automatiquement après son lancement.",
"must-be-installed-once": "Vous devez avoir Beat Saber installé depuis le Oculus Store sur cet appareil, sinon Beat Saber se fermera automatiquement après le lancement.",
"will-backup": "Afin de lancer cette version, le dossier d'installation original de Beat Saber se trouvant dans votre bibliothèque Oculus va être renommé et sera automatiquement restauré à l'arrêt de Beat Saber."
},
"not-remind-me": "Ne plus me rappeler",
@@ -736,6 +755,9 @@
},
"launch-as-admin": "Lancer en administateur",
"not-remind-me": "Ne plus me rappeler"
},
"bs-version-outdated": {
"body": "La version {outdatedVersion} est obsolète, et certains mods ou fonctionnalités peuvent ne plus fonctionner comme prévu. Veuillez télécharger la dernière version recommandée ({recommendedVersion}) de Beat Saber pour profiter des dernières fonctionnalités et corrections de bugs."
}
},
"maps": {
+27 -5
View File
@@ -14,7 +14,12 @@
"refuse": "拒否する",
"apply": "適用",
"copy": "コピー",
"copied": "コピー済み!"
"copied": "コピー済み!",
"confirm": "確認",
"choose-folder": "フォルダーを選択",
"unknown": "不明なエラーが発生しました (´・ω・`)",
"warning": "警告",
"continue": "続ける"
},
"nav-bar": {
"add-version": "バージョンを追加",
@@ -33,6 +38,7 @@
"desktop-description": "ファーストパーソン・フライング・コントローラー・モードでは、WASDとマウスを使ってゲーム内のメニューを操作できます。ヘッドセットを装着する必要がないので、テストがより簡単になります!",
"debug": "デバックモード",
"debug-description": "IPAの出力ログウィンドウを有効にします。これはMODのデバッグコンソールを表示します。",
"outdated-tippy": "このバージョンは古いため、一部のMODや機能が期待通りに動作しない可能性があります。最新の機能やバグ修正を楽しむには、推奨バージョン ({recommendedVersion}) のBeat Saberを使用することをお勧めします。",
"advanced-launch": {
"button": "高度な起動",
"placeholder": "追加引数 例:--revert; --nowait"
@@ -82,7 +88,8 @@
"latest": "最新",
"description": "說明",
"dropdown": {
"uninstall-all": "全てアンインストールする"
"uninstall-all": "全てアンインストールする",
"unselect-all": "すべて選択解除"
}
}
}
@@ -380,7 +387,8 @@
"BS_ALREADY_RUNNING": "Beat Saberはすでに起動しています",
"EXE_NOT_FINDED": "ファイルが見つかりません",
"EXIT": "緊急停止",
"OCULUS_LIB_NOT_FOUND": "Oculusライブラリが見つかりません"
"OCULUS_LIB_NOT_FOUND": "Oculusライブラリが見つかりません",
"ORIGINAL_OCULUS_NOT_INSTALLED": "オリジナルのOculusバージョンが見つかりませんでした"
},
"msg": {
"UNKNOWN_ERROR": "不明なエラーが発生しました。",
@@ -389,7 +397,10 @@
"BS_ALREADY_RUNNING": "再度起動する前にBeatSaberを閉じてください。",
"EXE_NOT_FINDED": "いくつかのファイルが見つからないようです。",
"EXIT": "BeatSaberが緊急停止したため、ファイルをチェックします。",
"OCULUS_LIB_NOT_FOUND": "Oculusアプリケーションが正しくインストールしていることと、Oculusライブラリが正しく定義されていることを確認してください。"
"OCULUS_LIB_NOT_FOUND": "Oculusアプリケーションが正しくインストールしていることと、Oculusライブラリが正しく定義されていることを確認してください。",
"PROTON_NOT_SET": "設定でProtonのパスを設定してください。",
"PROTON_NOT_FOUND": "設定でProtonのパスを設定してください。",
"ORIGINAL_OCULUS_NOT_INSTALLED": "Beat Saberは、開く前にOculusアプリからインストールする必要があります。"
},
"actions": {
"STEAM_NOT_RUNNING": "Steamを起動"
@@ -500,6 +511,14 @@
"error": {
"msg": "ショートカットの作成中にエラーが発生しました。"
}
},
"bs-version-oudated": {
"title": "バージョンが古い",
"msg": "このBeat Saberのバージョンは古いため、推奨バージョンを使用して最新の機能やバグ修正をお楽しみください。",
"actions": {
"do-not-remind": "再度通知しない",
"ok": "OK"
}
}
},
"modals": {
@@ -702,7 +721,7 @@
"original-version-backup-oculus": {
"title": "注意",
"body": {
"must-be-installed-once": "少なくとも一度はOculus StoreからBeat Saberをインストールしている必要があります。そうしないとBeat Saberは起動後に自動的に終了する可能性があります。",
"must-be-installed-once": "このデバイスにOculusストアからBeat Saberをインストールしておく必要があります。そうしないとBeat Saberは起動後に自動的に閉じます。",
"will-backup": "このバージョンを起動するために、OculusライブラリにあるBeat Saberの元のインストールフォルダは名前が変更され、Beat Saberの終了時に自動的に復元されます。"
},
"not-remind-me": "二度と表示しないでください",
@@ -735,6 +754,9 @@
},
"launch-as-admin": "管理者として起動",
"not-remind-me": "Больше не напоминать"
},
"bs-version-outdated": {
"body": "バージョン {outdatedVersion} は古いため、一部のモッドや機能が期待通りに動作しない場合があります。最新の機能やバグ修正を楽しむために、推奨される最新バージョンの Beat Saber ({recommendedVersion}) をダウンロードしてください。"
}
},
"maps": {
+27 -5
View File
@@ -14,7 +14,12 @@
"refuse": "Отказаться",
"apply": "Применить",
"copy": "Скопировать",
"copied": "Скопировано!"
"copied": "Скопировано!",
"confirm": "Подтвердить",
"choose-folder": "Изменить папку",
"unknown": "Неизвестная ошибка ¯\\_(ツ)_/¯",
"warning": "Внимание",
"continue": "Продолжить"
},
"nav-bar": {
"add-version": "Добавить версию игры",
@@ -33,6 +38,7 @@
"desktop-description": "Режим Летающих контроллеров от первого лица(FPFC) позволит вам использовать меню с помощью WASD и мышки. Это облегчает тестирование модов, потому что вам не нужно подключать шлем!",
"debug": "Режим отладки",
"debug-description": "Включает окно логов для IPA. В окне будут отображаться логи модов.",
"outdated-tippy": "Эта версия устарела, и некоторые моды или функции могут работать не так, как ожидалось. Рекомендуется использовать рекомендуемую версию ({recommendedVersion}) Beat Saber, чтобы воспользоваться последними функциями и исправлениями ошибок.",
"advanced-launch": {
"button": "Расширенный запуск",
"placeholder": "Параметры запуска, например: --revert; --nowait"
@@ -82,7 +88,8 @@
"latest": "Latest",
"description": "Описание",
"dropdown": {
"uninstall-all": "Удалить всё"
"uninstall-all": "Удалить всё",
"unselect-all": "Снять все выделения"
}
}
}
@@ -380,7 +387,8 @@
"BS_ALREADY_RUNNING": "Beat Saber уде запущен",
"EXE_NOT_FINDED": "Отсутствуют файлы",
"EXIT": "Вылет",
"OCULUS_LIB_NOT_FOUND": "Библиотека Oculus не найдена"
"OCULUS_LIB_NOT_FOUND": "Библиотека Oculus не найдена",
"ORIGINAL_OCULUS_NOT_INSTALLED": "Оригинальная версия Oculus не найдена"
},
"msg": {
"UNKNOWN_ERROR": "Возникла неизвестная ошибка.",
@@ -389,7 +397,10 @@
"BS_ALREADY_RUNNING": "Закройте Beat Saber, чтобы запустить его.",
"EXE_NOT_FINDED": "Некоторые файлы отсутствуют, пробуем их проверить.",
"EXIT": "Beat Saber вылетел, пытаемся проверить файлы.",
"OCULUS_LIB_NOT_FOUND": "Проверьте, правильно ли установлено приложение Oculus, и правильно ли определены библиотеки в Oculus."
"OCULUS_LIB_NOT_FOUND": "Проверьте, правильно ли установлено приложение Oculus, и правильно ли определены библиотеки в Oculus.",
"PROTON_NOT_SET": "Установите путь к Proton в настройках.",
"PROTON_NOT_FOUND": "Установите путь к Proton в настройках.",
"ORIGINAL_OCULUS_NOT_INSTALLED": "Beat Saber должен быть установлен из приложения Oculus перед открытием."
},
"actions": {
"STEAM_NOT_RUNNING": "Запустить Steam"
@@ -500,6 +511,14 @@
"error": {
"msg": "Ошибка создания ярлыка."
}
},
"bs-version-oudated": {
"title": "Устаревшая версия",
"msg": "Эта версия Beat Saber устарела. Используйте рекомендуемую версию, чтобы воспользоваться последними функциями и исправлениями ошибок.",
"actions": {
"do-not-remind": "Больше не напоминать",
"ok": "Ок"
}
}
},
"modals": {
@@ -702,7 +721,7 @@
"original-version-backup-oculus": {
"title": "Внимание",
"body": {
"must-be-installed-once": "Вы должны были установить Beat Saber хотя бы один раз из Oculus Store, иначе Beat Saber может автоматически закрыться после запуска.",
"must-be-installed-once": "Вы должны установить Beat Saber из магазина Oculus на этом устройстве, в противном случае Beat Saber автоматически закроется после запуска.",
"will-backup": "Для запуска этой версии исходная папка установки Beat Saber, находящаяся в вашей библиотеке Oculus, будет переименована и автоматически восстановлена при выходе из Beat Saber."
},
"not-remind-me": "Больше не напоминать",
@@ -734,6 +753,9 @@
"info-3": "Обратите внимание, что не рекомендуется предоставлять права администратора Steam, так как это также влияет на установленные игры и моды, представляя угрозу безопасности."
},
"launch-as-admin": "Запустить от имени администратора"
},
"bs-version-outdated": {
"body": "Версия {outdatedVersion} устарела, и некоторые моды или функции могут работать некорректно. Пожалуйста, скачайте последнюю рекомендуемую версию ({recommendedVersion}) Beat Saber, чтобы воспользоваться новейшими функциями и исправлениями ошибок."
}
},
"maps": {
+27 -5
View File
@@ -14,7 +14,12 @@
"refuse": "拒絕",
"apply": "應用",
"copy": "複製",
"copied": "已複製!"
"copied": "已複製!",
"confirm": "確認",
"choose-folder": "選擇文件夾",
"unknown": "發生了一個錯誤 ¯\\_(ツ)_/¯",
"warning": "警告",
"continue": "繼續"
},
"nav-bar": {
"add-version": "新增版本",
@@ -33,6 +38,7 @@
"desktop-description": "第一人稱飛控模式允許你使用鍵盤 WASD 和滑鼠在遊戲中進行導航。你無需帶上頭顯即可進行測試!",
"debug": "Debug 模式",
"debug-description": "為 IPA 啟用輸出日誌窗口。 會顯示所用 Mod 的 debug 輸出",
"outdated-tippy": "此版本已過時,某些模組或功能可能無法按預期運作。建議使用推薦版本的 Beat Saber ({recommendedVersion}),以享受最新功能和修復。",
"advanced-launch": {
"button": "高級啟動",
"placeholder": "額外啟動參數,例如: --revert; --nowait"
@@ -82,7 +88,8 @@
"latest": "最新",
"description": "描述",
"dropdown": {
"uninstall-all": "全部移除"
"uninstall-all": "全部移除",
"unselect-all": "取消全選"
}
}
}
@@ -380,7 +387,8 @@
"BS_ALREADY_RUNNING": "Beat Saber 已在運行",
"EXE_NOT_FINDED": "缺少文件",
"EXIT": "突然停止",
"OCULUS_LIB_NOT_FOUND": "未找到 Oculus 庫"
"OCULUS_LIB_NOT_FOUND": "未找到 Oculus 庫",
"ORIGINAL_OCULUS_NOT_INSTALLED": "找不到原始Oculus版本"
},
"msg": {
"UNKNOWN_ERROR": "發生未知錯誤。",
@@ -389,7 +397,10 @@
"BS_ALREADY_RUNNING": "再次啟動前請關閉 BeatSaber。",
"EXE_NOT_FINDED": "某些文件似乎遺失了,嘗試驗證文件。",
"EXIT": "BeatSaber 突然停止,請檢查文件。",
"OCULUS_LIB_NOT_FOUND": "檢查 Oculus 應用是否正確安裝,以及庫是否在 Oculus 中正確定義。"
"OCULUS_LIB_NOT_FOUND": "檢查 Oculus 應用是否正確安裝,以及庫是否在 Oculus 中正確定義。",
"PROTON_NOT_SET": "在設定中設置Proton路徑。",
"PROTON_NOT_FOUND": "在設定中設置Proton路徑。",
"ORIGINAL_OCULUS_NOT_INSTALLED": "在打開之前,必須從Oculus應用程式安裝Beat Saber。"
},
"actions": {
"STEAM_NOT_RUNNING": "啟動 Steam"
@@ -500,6 +511,14 @@
"error": {
"msg": "創建捷徑時發生錯誤"
}
},
"bs-version-oudated": {
"title": "版本過時",
"msg": "此 Beat Saber 版本已過時,請使用推薦版本以享受最新功能和修復。",
"actions": {
"do-not-remind": "不要再提醒我",
"ok": "確定"
}
}
},
"modals": {
@@ -702,7 +721,7 @@
"original-version-backup-oculus": {
"title": "警告",
"body": {
"must-be-installed-once": "必須至少從 Oculus 商店安裝過一次 BeatSaber,否則 BeatSaber 可能會在啟動後自動關閉。",
"must-be-installed-once": "必須在此設備上從Oculus商店安裝Beat Saber,否則Beat Saber在啟動後自動關閉。",
"will-backup": "為了啟動這個版本,位於您的Oculus庫中的Beat Saber的原始安裝文件夾將被重新命名,並且在Beat Saber關閉時會自動恢復。"
},
"not-remind-me": "不再提醒我",
@@ -735,6 +754,9 @@
},
"launch-as-admin": "以管理員身份啟動",
"not-remind-me": "不再提醒我"
},
"bs-version-outdated": {
"body": "版本 {outdatedVersion} 已過時,某些模組或功能可能無法正常運作。請下載最新推薦的 Beat Saber 版本 ({recommendedVersion}),以享受最新功能和修正。"
}
},
"maps": {
+27 -5
View File
@@ -14,7 +14,12 @@
"refuse": "拒绝",
"apply": "应用",
"copy": "复制",
"copied": "已复制!"
"copied": "已复制!",
"confirm": "确认",
"choose-folder": "选择文件夹",
"Unknown": "发生了一个错误 ¯\\_(ツ)_/¯",
"warning": "警告",
"continue": "继续"
},
"nav-bar": {
"add-version": "添加版本",
@@ -33,6 +38,7 @@
"desktop-description": "第一人称飞控模式允许你使用键盘 WASD 和鼠标在游戏中进行导航。你无需带上头显即可进行测试!",
"debug": "Debug 模式",
"debug-description": "为 IPA 启用输出日志窗口。 会显示所用 Mod 的 debug 输出",
"outdated-tippy": "此版本已过时,某些模组或功能可能无法按预期运行。建议使用推荐版本的 Beat Saber ({recommendedVersion}),以享受最新功能和修复。",
"advanced-launch": {
"button": "高级启动",
"placeholder": "额外启动参数,例如: --revert; --nowait"
@@ -82,7 +88,8 @@
"latest": "最新",
"description": "描述",
"dropdown": {
"uninstall-all": "全部卸载"
"uninstall-all": "全部卸载",
"unselect-all": "取消全选"
}
}
}
@@ -380,7 +387,8 @@
"BS_ALREADY_RUNNING": "Beat Saber 已在运行",
"EXE_NOT_FINDED": "缺少文件",
"EXIT": "突然停止",
"OCULUS_LIB_NOT_FOUND": "未找到 Oculus 库"
"OCULUS_LIB_NOT_FOUND": "未找到 Oculus 库",
"ORIGINAL_OCULUS_NOT_INSTALLED": "未找到原始Oculus版本"
},
"msg": {
"UNKNOWN_ERROR": "发生未知错误。",
@@ -389,7 +397,10 @@
"BS_ALREADY_RUNNING": "再次启动前请关闭 BeatSaber。",
"EXE_NOT_FINDED": "某些文件似乎丢失了,尝试验证文件。",
"EXIT": "BeatSaber 突然停止,请检查文件。",
"OCULUS_LIB_NOT_FOUND": "检查 Oculus 应用是否正确安装,以及库是否在 Oculus 中正确定义。"
"OCULUS_LIB_NOT_FOUND": "检查 Oculus 应用是否正确安装,以及库是否在 Oculus 中正确定义。",
"PROTON_NOT_SET": "在设置中设置Proton路径。",
"PROTON_NOT_FOUND": "在设置中设置Proton路径。",
"ORIGINAL_OCULUS_NOT_INSTALLED": "在打开之前,必须从Oculus应用程序安装Beat Saber。"
},
"actions": {
"STEAM_NOT_RUNNING": "启动 Steam"
@@ -500,6 +511,14 @@
"error": {
"msg": "创建快捷方式时发生错误"
}
},
"bs-version-oudated": {
"title": "版本过时",
"msg": "此 Beat Saber 版本已过时,请使用推荐版本以享受最新功能和修复。",
"actions": {
"do-not-remind": "不再提醒",
"ok": "确定"
}
}
},
"modals": {
@@ -702,7 +721,7 @@
"original-version-backup-oculus": {
"title": "警告",
"body": {
"must-be-installed-once": "必须至少从 Oculus 商店安装过一次 BeatSaber,否则 BeatSaber 可能会在启动后自动关闭。",
"must-be-installed-once": "必须在此设备上从Oculus商店安装Beat Saber,否则Beat Saber在启动后自动关闭。",
"will-backup": "为了启动这个版本,位于您的Oculus库中的Beat Saber的原始安装文件夹将被重命名,并且在Beat Saber关闭时会自动恢复。"
},
"not-remind-me": "不再提醒我",
@@ -735,6 +754,9 @@
},
"launch-as-admin": "以管理员身份启动",
"not-remind-me": "不再提醒我"
},
"bs-version-outdated": {
"body": "版本 {outdatedVersion} 已过时,某些模组或功能可能无法正常工作。请下载最新推荐的 Beat Saber 版本 ({recommendedVersion}),以享受最新功能和修复。"
}
},
"maps": {
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -7,8 +7,6 @@
"version": "8.0.0"
},
"configProperties": {
"System.Globalization.Invariant": true,
"System.Globalization.PredefinedCulturesOnly": true,
"System.Reflection.Metadata.MetadataUpdater.IsSupported": false,
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
}
+19
View File
@@ -0,0 +1,19 @@
import type { Config } from "jest";
const config: Config = {
// testURL: "http://localhost/",
// testEnvironment: "jsdom",
transform: {
"\\.(ts|tsx|js|jsx)$": "ts-jest",
},
moduleNameMapper: {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/.erb/mocks/fileMock.js",
"\\.(css|less|sass|scss)$": "identity-obj-proxy",
},
moduleFileExtensions: ["js", "jsx", "ts", "tsx", "json"],
moduleDirectories: ["node_modules", "src"],
testPathIgnorePatterns: ["release/app/dist"],
setupFiles: ["./.erb/scripts/check-build-exists.ts"],
};
export default config;
+10875 -5023
View File
File diff suppressed because it is too large Load Diff
+12 -39
View File
@@ -14,6 +14,7 @@
"start:preload": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.preload.dev.ts",
"start:renderer": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./.erb/configs/webpack.config.renderer.dev.ts",
"test": "jest",
"test:unit": "jest ./src/__tests__/unit",
"publish": "npm run build && electron-builder -c.win.certificateSha1=206941d969c4fa8a0e04d9427def361e13b02fd0 --publish always --win --x64"
},
"lint-staged": {
@@ -122,45 +123,17 @@
"beat-saber"
],
"homepage": "https://github.com/Zagrios/bs-manager#readme",
"jest": {
"testURL": "http://localhost/",
"testEnvironment": "jsdom",
"transform": {
"\\.(ts|tsx|js|jsx)$": "ts-jest"
},
"moduleNameMapper": {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/.erb/mocks/fileMock.js",
"\\.(css|less|sass|scss)$": "identity-obj-proxy"
},
"moduleFileExtensions": [
"js",
"jsx",
"ts",
"tsx",
"json"
],
"moduleDirectories": [
"node_modules",
"src"
],
"testPathIgnorePatterns": [
"release/app/dist"
],
"setupFiles": [
"./.erb/scripts/check-build-exists.ts"
]
},
"devDependencies": {
"@electron/rebuild": "^3.2.13",
"@pmmmwh/react-refresh-webpack-plugin": "0.5.5",
"@teamsupercell/typings-for-css-modules-loader": "^2.5.1",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/jest-dom": "^6.4.1",
"@testing-library/react": "^13.3.0",
"@types/archiver": "^5.3.1",
"@types/color": "^3.0.3",
"@types/crypto-js": "^4.2.1",
"@types/dateformat": "^5.0.0",
"@types/jest": "^27.5.2",
"@types/jest": "^29.5.11",
"@types/node": "^20",
"@types/node-fetch": "^2.6.3",
"@types/pako": "^2.0.1",
@@ -199,7 +172,7 @@
"eslint-import-resolver-webpack": "^0.13.2",
"eslint-plugin-compat": "^4.0.2",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-jest": "^26.8.7",
"eslint-plugin-jest": "^27.6.3",
"eslint-plugin-jsx-a11y": "^6.6.1",
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-react": "^7.30.0",
@@ -207,7 +180,7 @@
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.5.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^27.5.1",
"jest": "^29.7.0",
"lint-staged": "^12.5.0",
"mini-css-extract-plugin": "^2.6.1",
"opencollective-postinstall": "^2.0.3",
@@ -223,9 +196,9 @@
"sass-loader": "^12.6.0",
"style-loader": "^3.3.1",
"tailwind-scrollbar": "^2.0.1",
"tailwindcss": "^3.3.1",
"tailwindcss": "^3.4.15",
"terser-webpack-plugin": "^5.3.5",
"ts-jest": "^27.1.5",
"ts-jest": "^29.1.2",
"ts-loader": "^9.3.0",
"ts-node": "^10.8.2",
"typescript": "^4.7.4",
@@ -240,6 +213,7 @@
"@electron/fuses": "^1.6.2",
"@node-steam/vdf": "^2.2.0",
"@tippyjs/react": "^4.2.6",
"@nextui-org/react": "^2.3.6",
"archiver": "^6.0.1",
"color": "^4.2.3",
"crypto-js": "^4.2.0",
@@ -250,15 +224,13 @@
"electron-store": "^8.1.0",
"electron-updater": "^6.1.7",
"fast-deep-equal": "^3.1.3",
"framer-motion": "^10.16.16",
"framer-motion": "^11.2.6",
"fs-extra": "^11.2.0",
"history": "^5.3.0",
"is-elevated": "^3.0.0",
"jszip": "^3.10.1",
"md5-file": "^5.0.0",
"node-abi": "^3.47.0",
"node-abi": "^3.65.0",
"node-fetch": "^2.6.7",
"node-stream-zip": "^1.15.0",
"pako": "^2.1.0",
"ps-list": "^7.2.0",
"qrcode.react": "^3.1.0",
@@ -281,7 +253,8 @@
"tippy.js": "^6.3.7",
"to-ico": "^1.1.5",
"use-double-click": "^1.0.5",
"use-fit-text": "^2.4.0"
"use-fit-text": "^2.4.0",
"yauzl": "^3.2.0"
},
"devEngines": {
"node": ">=14.x",
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "bs-manager",
"version": "1.4.12",
"version": "1.4.14",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "bs-manager",
"version": "1.4.12",
"version": "1.4.14",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "bs-manager",
"version": "1.4.12",
"version": "1.4.14",
"description": "BSManager",
"main": "./dist/main/main.js",
"author": {
-9
View File
@@ -1,9 +0,0 @@
import "@testing-library/jest-dom";
import { render } from "@testing-library/react";
import App from "../renderer/windows/App";
describe("App", () => {
it("should render", () => {
expect(render(<App />)).toBeTruthy();
});
});
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+329
View File
@@ -0,0 +1,329 @@
import path from "path";
import { mkdir, pathExistsSync, readFile, rm } from "fs-extra";
import { BsmZipExtractor } from "main/models/bsm-zip-extractor.class";
const TEST_FOLDER = path.resolve(__dirname, "..", "assets", "zip");
const STANDARD_ZIP = path.join(TEST_FOLDER, "standard.zip");
const WINDOWS_LEGACY_MAP_ZIP = path.join(TEST_FOLDER, "windows_legacy.zip");
const SUBFOLDERS_ZIP = path.join(TEST_FOLDER, "subfolders.zip");
const MANIFEST_ZIP = path.join(TEST_FOLDER, "manifest.zip");
const EMPTY_ZIP = path.join(TEST_FOLDER, "empty.zip");
const UNICODE_ZIP = path.join(TEST_FOLDER, "unicode.zip");
const DESTINATION_FOLDER = path.join(TEST_FOLDER, "out");
describe("Test BsmZipExtractor class", () => {
let zip: BsmZipExtractor;
beforeAll(async () => {
if (pathExistsSync(DESTINATION_FOLDER)) {
await rm(DESTINATION_FOLDER, { recursive: true, force: true });
}
await mkdir(DESTINATION_FOLDER);
});
afterEach(async () => {
if (zip) {
zip.close();
}
if (pathExistsSync(DESTINATION_FOLDER)) {
await rm(DESTINATION_FOLDER, { recursive: true, force: true });
}
await mkdir(DESTINATION_FOLDER);
});
afterAll(async () => {
if (pathExistsSync(DESTINATION_FOLDER)) {
await rm(DESTINATION_FOLDER, { recursive: true, force: true });
}
});
it("Extract standard zip", async () => {
zip = await BsmZipExtractor.fromPath(STANDARD_ZIP);
const res = await zip.extract(DESTINATION_FOLDER);
expect(res.sort()).toEqual([
"file_1.2.txt",
"file_1.3.txt",
"folder_1.1/",
"folder_1.1/file_2.1.txt",
"folder_1.1/file_2.2.txt",
].sort())
for (const file of [
"file_1.2.txt",
"file_1.3.txt",
]) {
expect(pathExistsSync(path.join(DESTINATION_FOLDER, file)))
.toBe(true);
}
const SUBFOLDER_PATH = path.join(DESTINATION_FOLDER, "folder_1.1");
for (const file of [
"file_2.1.txt",
"file_2.2.txt",
]) {
expect(pathExistsSync(path.join(SUBFOLDER_PATH, file)))
.toBe(true);
}
});
it("Extract map zips using back slashes", async () => {
zip = await BsmZipExtractor.fromPath(WINDOWS_LEGACY_MAP_ZIP);
const res = await zip.extract(DESTINATION_FOLDER);
expect(res.sort()).toEqual([
"BPMInfo.dat",
"cover.jpg",
"ExpertLegacy.dat",
"ExpertPlusLawless.dat",
"ExpertPlusLegacy.dat",
"ExpertPlusStandard.dat",
"ExpertStandard.dat",
"Info.dat",
"song.egg",
"pfp/",
"pfp/galaxyCompressed.jpg",
"pfp/gojiCompressed.jpg",
].sort());
// Expect all the files to exists
for (const file of [
"BPMInfo.dat",
"cover.jpg",
"ExpertLegacy.dat",
"ExpertPlusLawless.dat",
"ExpertPlusLegacy.dat",
"ExpertPlusStandard.dat",
"ExpertStandard.dat",
"Info.dat",
"song.egg",
]) {
expect(pathExistsSync(path.join(DESTINATION_FOLDER, file)))
.toBe(true);
}
const SUBFOLDER_PATH = path.join(DESTINATION_FOLDER, "pfp");
for (const file of [
"galaxyCompressed.jpg",
"gojiCompressed.jpg",
]) {
expect(pathExistsSync(path.join(SUBFOLDER_PATH, file)))
.toBe(true);
}
});
it("Read Zip with multiple subfolders", async () => {
const zip = await BsmZipExtractor.fromPath(SUBFOLDERS_ZIP);
const res = await zip.extract(DESTINATION_FOLDER);
expect(res.sort()).toEqual([
"1/",
"1/2/",
"1/2/3/",
"1/2/3/4/",
"1/2/3/4/5.txt",
].sort());
for (const folder of [
"1/",
"1/2/",
"1/2/3/",
"1/2/3/4/",
]) {
expect(pathExistsSync(path.join(DESTINATION_FOLDER, folder)))
.toBe(true);
}
const file = "1/2/3/4/5.txt";
expect(pathExistsSync(path.join(DESTINATION_FOLDER, file)))
.toBe(true);
});
it("Read manifest.json from zip file", async () => {
const zipBuffer = await readFile(MANIFEST_ZIP);
zip = await BsmZipExtractor.fromBuffer(zipBuffer);
const buffer = await (await zip.findEntry(entry => entry.fileName === "manifest.json")).read();
const manifest = JSON.parse(buffer.toString());
expect(manifest).toBeTruthy();
// Check some of the fields if they are correct
expect(manifest.appId).toBe("some-app-id");
expect(manifest.canonicalName).toBe("some-canonical-name");
expect(manifest.isCore).toBe(true);
});
it("Should find an entry using findEntry", async () => {
zip = await BsmZipExtractor.fromPath(STANDARD_ZIP);
const entry = await zip.findEntry(entry => entry.fileName === "file_1.2.txt");
expect(entry).toBeTruthy();
expect(entry.fileName).toBe("file_1.2.txt");
const contentBuffer = await entry.read();
const content = contentBuffer.toString();
expect(content).toContain("This is file 1.2"); // Assurez-vous que le contenu correspond
});
it("Should filter entries using filterEntries", async () => {
zip = await BsmZipExtractor.fromPath(STANDARD_ZIP);
const entries = await zip.filterEntries(entry => !entry.isDirectory && entry.fileName.startsWith("folder_1.1/"));
expect(entries.length).toBe(2);
const fileNames = entries.map(entry => entry.fileName).sort();
expect(fileNames).toEqual([
"folder_1.1/file_2.1.txt",
"folder_1.1/file_2.2.txt",
]);
});
it("Should get an entry by file name using getEntry", async () => {
zip = await BsmZipExtractor.fromPath(STANDARD_ZIP);
const entry = await zip.getEntry("file_1.3.txt");
expect(entry).toBeTruthy();
expect(entry.fileName).toBe("file_1.3.txt");
const contentBuffer = await entry.read();
const content = contentBuffer.toString();
expect(content).toContain("This is file 1.3"); // Assurez-vous que le contenu correspond
});
it("Should extract only specified entries using entriesNames option", async () => {
zip = await BsmZipExtractor.fromPath(STANDARD_ZIP);
const res = await zip.extract(DESTINATION_FOLDER, { entriesNames: ["file_1.2.txt", "folder_1.1/file_2.1.txt"] });
expect(res.sort()).toEqual([
"file_1.2.txt",
"folder_1.1/",
"folder_1.1/file_2.1.txt",
].sort());
for (const file of [
"file_1.2.txt",
"folder_1.1/file_2.1.txt",
]) {
expect(pathExistsSync(path.join(DESTINATION_FOLDER, file)))
.toBe(true);
}
// Vérifiez que les autres fichiers ne sont pas extraits
expect(pathExistsSync(path.join(DESTINATION_FOLDER, "file_1.3.txt")))
.toBe(false);
expect(pathExistsSync(path.join(DESTINATION_FOLDER, "folder_1.1/file_2.2.txt")))
.toBe(false);
});
it("Should not extract any files if abortToken is already aborted", async () => {
zip = await BsmZipExtractor.fromPath(STANDARD_ZIP);
const abortController = new AbortController();
// Abortez avant d'appeler extract
abortController.abort();
const res = await zip.extract(DESTINATION_FOLDER, { abortToken: abortController });
expect(res.length).toBe(0);
// Vérifiez qu'aucun fichier n'a été extrait
expect(pathExistsSync(path.join(DESTINATION_FOLDER, "file_1.2.txt")))
.toBe(false);
});
it("Should throw an error when opening a non-existent zip file", async () => {
await expect(BsmZipExtractor.fromPath("non_existent.zip"))
.rejects
.toThrow();
});
it("Should handle empty zip file correctly", async () => {
zip = await BsmZipExtractor.fromPath(EMPTY_ZIP);
const entries = [];
for await (const entry of zip.entries()) {
entries.push(entry);
}
expect(entries.length).toBe(0);
const res = await zip.extract(DESTINATION_FOLDER);
expect(res.length).toBe(0);
});
it("Should return the same entries on multiple calls to entries()", async () => {
zip = await BsmZipExtractor.fromPath(STANDARD_ZIP);
const entries1 = [];
for await (const entry of zip.entries()) {
entries1.push(entry);
}
expect(entries1.length).toBe(5); // Basé sur le contenu de STANDARD_ZIP
const entries2 = [];
for await (const entry of zip.entries()) {
entries2.push(entry);
}
expect(entries2.length).toBe(5);
expect(entries2.map(e => e.fileName).sort()).toEqual(entries1.map(e => e.fileName).sort());
});
it("Should not be able to read entries after zip is closed", async () => {
zip = await BsmZipExtractor.fromPath(STANDARD_ZIP);
zip.close();
await expect(zip.entries().next()).rejects.toThrow();
});
it("Should handle zip file from buffer", async () => {
const zipBuffer = await readFile(STANDARD_ZIP);
zip = await BsmZipExtractor.fromBuffer(zipBuffer);
const entries = [];
for await (const entry of zip.entries()) {
entries.push(entry.fileName);
}
expect(entries.sort()).toEqual([
"file_1.2.txt",
"file_1.3.txt",
"folder_1.1/",
"folder_1.1/file_2.1.txt",
"folder_1.1/file_2.2.txt",
].sort());
});
it("Should read and extract files with Unicode filenames", async () => {
zip = await BsmZipExtractor.fromPath(UNICODE_ZIP);
const entries = [];
for await (const entry of zip.entries()) {
entries.push(entry);
}
expect(entries.map(e => e.fileName)).toContain("こんにちは.txt"); // "Bonjour" en japonais
const res = await zip.extract(DESTINATION_FOLDER);
expect(res).toContain("こんにちは.txt");
expect(pathExistsSync(path.join(DESTINATION_FOLDER, "こんにちは.txt")))
.toBe(true);
});
it("Should throw an error when reading a corrupted zip file", async () => {
const CORRUPTED_ZIP = path.join(TEST_FOLDER, "corrupted.zip");
await expect(BsmZipExtractor.fromPath(CORRUPTED_ZIP)).rejects.toThrow(/.*[Error: Invalid comment length].*/)
});
});
-28
View File
@@ -1,28 +0,0 @@
import JSZip from "jszip";
import { pathExist } from "./fs.helpers";
import path from "path";
import { mkdir, writeFile } from "fs/promises";
export async function extractZip(zip: JSZip, dest: string): Promise<string[]> {
if (!(await pathExist(dest))) {
throw new Error(`Path ${dest} does not exist`);
}
const files: string[] = [];
for (const [relativePath, entry] of Object.entries(zip.files)) {
if (entry.dir) {
continue;
}
const content = await entry.async("nodebuffer");
const outPath = path.join(dest, relativePath);
const outDir = path.dirname(outPath);
await mkdir(outDir, { recursive: true });
await writeFile(outPath, content);
files.push(outPath);
}
return files;
}
+218
View File
@@ -0,0 +1,218 @@
import { createWriteStream, ensureDir } from "fs-extra";
import path from "path";
import { Readable } from "stream";
import yauzl, { ZipFile, Options, Entry } from "yauzl"
export class BsmZipExtractor {
private static readonly YAUZL_OPEN_OPTIONS: Options = {
lazyEntries: true,
decodeStrings: true,
autoClose: false,
};
public static fromPath(path: string): Promise<BsmZipExtractor> {
return new Promise((resolve, reject) => {
yauzl.open(path, BsmZipExtractor.YAUZL_OPEN_OPTIONS, (error: Error, zip: yauzl.ZipFile) => {
if (error) return reject(error);
resolve(new BsmZipExtractor(zip));
});
});
}
public static fromBuffer(buffer: Buffer): Promise<BsmZipExtractor> {
return new Promise((resolve, reject) => {
yauzl.fromBuffer(buffer, BsmZipExtractor.YAUZL_OPEN_OPTIONS, (error: Error, zip: yauzl.ZipFile) => {
if (error) return reject(error);
resolve(new BsmZipExtractor(zip));
});
});
}
private readonly zip: ZipFile;
private readonly entriesMap = new Map<string, BsmZipExtractorEntry>();
private constructor(zip: ZipFile) {
this.zip = zip;
}
private async readEntry(): Promise<BsmZipExtractorEntry> {
return new Promise((resolve, reject) => {
const onEntry = (entry: Entry) => {
cleanup();
resolve(new BsmZipExtractorEntry({ entry, zip: this.zip }));
};
const onEnd = () => {
cleanup();
resolve(null);
};
const onError = (err: Error) => {
cleanup();
reject(err);
};
const cleanup = () => {
this.zip.removeListener("entry", onEntry);
this.zip.removeListener("end", onEnd);
this.zip.removeListener("error", onError);
};
this.zip.once("entry", onEntry);
this.zip.once("end", onEnd);
this.zip.once("error", onError);
this.zip.readEntry();
});
}
// The first time this method is called, it will read all the entries and store them in a map
// The next times it will return the entries from the map to avoid reopening the zip file again
public async *entries(): AsyncGenerator<BsmZipExtractorEntry> {
for (const entry of Array.from(this.entriesMap.values())) {
yield entry;
}
let entry: BsmZipExtractorEntry = await this.readEntry();
while (entry) {
this.entriesMap.set(entry.fileName, entry);
yield entry;
entry = await this.readEntry();
}
}
public async findEntry(func: (entry: BsmZipExtractorEntry) => boolean): Promise<BsmZipExtractorEntry|null> {
for await (const entry of this.entries()) {
if (func(entry)) {
return entry;
}
}
return null;
}
public async filterEntries(func: (entry: BsmZipExtractorEntry) => boolean): Promise<BsmZipExtractorEntry[]> {
const filtered: BsmZipExtractorEntry[] = [];
for await (const entry of this.entries()) {
if (func(entry)) {
filtered.push(entry);
}
}
return filtered;
}
public async getEntry(fileName: string): Promise<BsmZipExtractorEntry> {
return this.entriesMap.get(fileName) ?? this.findEntry(entry => entry.fileName === fileName);
}
/**
* Extracts all entries from the zip file to the destination folder
* @param destination
* @param opt - { entriesNames?: string[], abortToken: AbortController }
* @returns {Promise<string[]>} The list of extracted files (relative paths in the destination folder)
*
* `opt` object:
* - `entriesNames` - The list of entries to extract (can be regexs or glob pattern). If not provided, all entries will be extracted
* - `abortToken` - The AbortController instance to abort the extraction
*
*/
public async extract(destination: string, opt?: { entriesNames?: (string|RegExp)[], abortToken?: AbortController }): Promise<string[]> {
const entriesNames = opt?.entriesNames;
const abortToken = opt?.abortToken;
if (abortToken?.signal?.aborted) {
return [];
}
await ensureDir(destination);
const extracted = new Set<string>()
for await (const entry of this.entries()) {
if (abortToken?.signal?.aborted) {
break;
}
if (entriesNames && !entriesNames.some(name => typeof name === "string" ? entry.fileName === name : name.test(entry.fileName))){
continue;
}
const extractedFile = await entry.extract(destination);
const dirname = path.dirname(extractedFile);
// Make zip that use backslash as separator have the same behavior as zip that use forward slash
if(dirname !== "."){
const split = dirname.split(path.posix.sep);
for (let i = 1; i <= split.length; i++) {
const folder = split.slice(0, i).join(path.posix.sep);
extracted.add(folder + path.posix.sep);
}
}
extracted.add(extractedFile);
}
return Array.from(extracted);
}
public close(): void {
this.zip.close();
}
}
class BsmZipExtractorEntry {
private readonly entry: Entry;
private readonly zip: ZipFile;
constructor(opt: { entry: Entry, zip: ZipFile }) {
this.entry = opt.entry;
this.zip = opt.zip;
}
public async read(): Promise<Buffer> {
return new Promise((resolve, reject) => {
this.zip.openReadStream(this.entry, (error: Error, stream: Readable) => {
if (error) return reject(error);
const buffers: Buffer[] = [];
stream.on("data", (data) => buffers.push(data as Buffer));
stream.on("end", () => resolve(Buffer.concat(buffers)));
stream.on("error", reject);
});
});
}
public async extract(destination: string): Promise<string> {
const destPath = path.join(destination, this.entry.fileName);
if (this.isDirectory) {
return ensureDir(destPath).then(() => this.entry.fileName);
}
return new Promise((resolve, reject) => {
this.zip.openReadStream(this.entry, async (error: Error, stream: Readable) => {
if (error) return reject(error);
await ensureDir(path.dirname(destPath));
const writeStream = createWriteStream(destPath);
stream.pipe(writeStream);
writeStream.on("finish", () => resolve(this.entry.fileName));
writeStream.on("error", reject);
});
});
}
public get isDirectory(): boolean {
return this.entry.fileName.endsWith("/");
}
public get fileName(): string {
return this.entry.fileName;
}
}
+27 -14
View File
@@ -1,4 +1,3 @@
import JSZip from "jszip";
import fetch from "node-fetch";
import { CustomError } from "../../shared/models/exceptions/custom-error.class";
import { mkdirs, createWriteStream, pathExists, WriteStream } from "fs-extra";
@@ -7,6 +6,8 @@ import { inflate } from "pako"
import { EMPTY, Observable, ReplaySubject, Subscriber, catchError, filter, from, lastValueFrom, mergeMap, scan, share, tap } from "rxjs";
import { Progression, hashFile } from "../helpers/fs.helpers";
import { OculusDownloaderErrorCodes } from "../../shared/models/bs-version-download/oculus-download.model";
import { BsmZipExtractor } from "./bsm-zip-extractor.class";
import { tryit } from "../../shared/helpers/error.helpers";
export class OculusDownloader {
@@ -23,26 +24,38 @@ export class OculusDownloader {
return `https://securecdn.oculus.com/binaries/segment/?access_token=${token}&binary_id=${binaryId}&segment_sha256=${segmentSha256}`;
}
private async downloadManifestZip(manifestUrl: string): Promise<JSZip> {
private async downloadManifestZip(manifestUrl: string): Promise<Buffer> {
const response = await fetch(manifestUrl);
const arrBuffer = await response.arrayBuffer();
return JSZip.loadAsync(arrBuffer);
return Buffer.from(arrBuffer);
}
private async getManifest(): Promise<OculusManifest> {
const downloadUrl = this.getDownloadManifestUrl(this.options.accessToken, this.options.binaryId);
const manifestZip = await this.downloadManifestZip(downloadUrl).catch(err => CustomError.throw(err, "DOWNLOAD_MANIFEST_FAILED"));
const manifestFile = manifestZip.file("manifest.json");
const buffer = await this.downloadManifestZip(downloadUrl)
.catch(err => CustomError.throw(err, "DOWNLOAD_MANIFEST_FAILED"));
if(!manifestFile){
const zip = await BsmZipExtractor.fromBuffer(buffer);
const entry = await zip.getEntry("manifest.json");
if(!entry) {
throw new CustomError("Manifest file not found", "MANIFEST_FILE_NOT_FOUND");
}
return manifestFile.async("text").then(JSON.parse).catch(err => CustomError.throw(err, "PARSE_MANIFEST_FILE_FAILED"));
const manifest = await entry.read();
zip.close();
const { result, error } = tryit(() => JSON.parse(manifest.toString()) as OculusManifest);
if(error){
throw CustomError.throw(error, "PARSE_MANIFEST_FILE_FAILED")
}
return result;
}
private downloadManifestFile(file: OculusManifestFile, destination: string): Observable<Progression<OculusManifestFile>> {
const downloadSegment = async (segment: OculusManifestFileSegment): Promise<ArrayBuffer> => {
const segmentUrl = this.getDownloadSegmentUrl(this.options.accessToken, this.options.binaryId, segment[1]);
const response = await fetch(segmentUrl);
@@ -66,11 +79,11 @@ export class OculusDownloader {
const arrBuffer = await downloadSegment(segment);
const inflated = inflate(arrBuffer);
await writeStream.write(inflated);
writeStream.write(inflated);
progress.current += inflated.byteLength;
progress.diff = inflated.byteLength;
sub.next(progress);
}
@@ -86,7 +99,7 @@ export class OculusDownloader {
});
}
private isFileIntegrityValid(file: OculusFileWithName, folder: string): Promise<boolean> {
private async isFileIntegrityValid(file: OculusFileWithName, folder: string): Promise<boolean> {
const [fileName, fileData] = file;
const destination = path.join(folder, fileName);
@@ -138,7 +151,7 @@ export class OculusDownloader {
if(this.isDownloading){
throw new CustomError("Already downloading", "ALREADY_DOWNLOADING");
}
this.options = options;
this.isDownloading = true;
@@ -182,7 +195,7 @@ export class OculusDownloader {
})));
const integrity = await lastValueFrom(this.verifyIntegrity(manifest, options.destination)).catch(err => CustomError.throw(err, "VERIFY_INTEGRITY_FAILED"));
if(integrity.data.length > 0){
throw new CustomError("Some files failed to download", "SOME_FILES_FAILED_TO_DOWNLOAD", integrity.data);
}
@@ -242,4 +255,4 @@ interface Logger {
info: (...args: unknown[]) => void;
warn: (...args: unknown[]) => void;
error: (...args: unknown[]) => void;
}
}
@@ -8,7 +8,6 @@ import { UtilsService } from "../utils.service";
import crypto from "crypto";
import { lstatSync } from "fs";
import { copy, createReadStream, ensureDir, pathExists, realpath, unlink } from "fs-extra";
import StreamZip from "node-stream-zip";
import { RequestService } from "../request.service";
import sanitize from "sanitize-filename";
import { DeepLinkService } from "../deep-link.service";
@@ -23,6 +22,7 @@ import { allSettled } from "../../../shared/helpers/promise.helpers";
import { splitIntoChunk } from "../../../shared/helpers/array.helpers";
import { IpcService } from "../ipc.service";
import { parseMapInfoDat } from "../../../shared/parsers/maps/map-info.parser";
import { BsmZipExtractor } from "../../models/bsm-zip-extractor.class";
export class LocalMapsManagerService {
private static instance: LocalMapsManagerService;
@@ -137,18 +137,15 @@ export class LocalMapsManagerService {
return { mapInfo, coverUrl, songUrl, hash, path: mapPath };
}
private async downloadMapZip(zipUrl: string): Promise<{ zip: StreamZip.StreamZipAsync; zipPath: string }> {
private async downloadMapZip(zipUrl: string): Promise<string> {
const fileName = `${path.basename(zipUrl, ".zip")}-${crypto.randomUUID()}.zip`;
const tempPath = this.utils.getTempPath();
await ensureFolderExist(this.utils.getTempPath());
const zipPath = (await lastValueFrom(this.reqService.downloadFile(zipUrl, {
return (await lastValueFrom(this.reqService.downloadFile(zipUrl, {
destFolder: tempPath,
filename: fileName
}))).data;
const zip = new StreamZip.async({ file: zipPath });
return { zip, zipPath };
}
private openOneClickDownloadMapWindow(mapId: string, isHash = false): void {
@@ -279,16 +276,15 @@ export class LocalMapsManagerService {
return installedMap;
}
const { zip, zipPath } = await this.downloadMapZip(zipUrl);
const zipPath = await this.downloadMapZip(zipUrl);
const zip = await BsmZipExtractor.fromPath(zipPath);
if (!zip) {
throw `Cannot download ${zipUrl}`;
}
await ensureFolderExist(mapPath);
await zip.extract(null, mapPath);
await zip.close();
await zip.extract(mapPath);
zip.close();
await unlink(zipPath);
const localMap = await this.loadMapInfoFromPath(mapPath);
@@ -12,7 +12,6 @@ import { readFileSync } from "fs";
import { BeatSaverService } from "../thrid-party/beat-saver/beat-saver.service";
import { copy, copyFile, pathExists, realpath } from "fs-extra";
import { Progression, ensureFolderExist, pathExist } from "../../helpers/fs.helpers";
import { IpcService } from "../ipc.service";
import sanitize from "sanitize-filename";
export class LocalPlaylistsManagerService {
@@ -36,7 +35,6 @@ export class LocalPlaylistsManagerService {
private readonly deepLink: DeepLinkService;
private readonly windows: WindowManagerService;
private readonly bsaver: BeatSaverService;
private readonly ipc: IpcService;
private constructor() {
this.maps = LocalMapsManagerService.getInstance();
@@ -45,13 +43,12 @@ export class LocalPlaylistsManagerService {
this.deepLink = DeepLinkService.getInstance();
this.windows = WindowManagerService.getInstance();
this.bsaver = BeatSaverService.getInstance();
this.ipc = IpcService.getInstance();
this.deepLink.addLinkOpenedListener(this.DEEP_LINKS.BeatSaver, link => {
log.info("DEEP-LINK RECEIVED FROM", this.DEEP_LINKS.BeatSaver, link);
const url = new URL(link);
const bplistUrl = url.host === "playlist" ? url.pathname.replace("/", "") : "";
this.openOneClickDownloadPlaylistWindow(bplistUrl);
const bplistUrl = url.host === "playlist" ? url.pathname.replace("/", "") + url.search : "";
this.windows.openWindow(`oneclick-download-playlist.html?playlistUrl=${encodeURIComponent(bplistUrl)}`);
});
}
@@ -97,10 +94,6 @@ export class LocalPlaylistsManagerService {
return JSON.parse(rawContent);
}
private openOneClickDownloadPlaylistWindow(downloadUrl: string): void {
this.windows.openWindow(`oneclick-download-playlist.html?playlistUrl=${downloadUrl}`);
}
public downloadPlaylist(bpListUrl: string, version: BSVersion): Observable<Progression<DownloadPlaylistProgressionData>> {
return new Observable<Progression<DownloadPlaylistProgressionData>>(obs => {
@@ -13,8 +13,11 @@ export abstract class AbstractLauncherService {
}
protected buildBsLaunchArgs(launchOptions: LaunchOption): string[]{
const launchArgs = ["--no-yeet"];
const launchArgs = [];
if(!launchOptions.version.steam && !launchOptions.version.oculus){
launchArgs.push("--no-yeet")
}
if (launchOptions.oculus) {
launchArgs.push("-vrmode");
launchArgs.push("oculus");
@@ -127,6 +127,11 @@ export class OculusLauncherService extends AbstractLauncherService implements St
const prepareDowngradedVersion: () => Promise<string> = async () => {
const originalVersionPath = await prepareOriginalVersion().catch(() => null);
if (!originalVersionPath) {
throw CustomError.fromError(new Error("Original Oculus Beat Saber not installed"), BSLaunchError.ORIGINAL_OCULUS_NOT_INSTALLED);
}
const oculusLib = await lastValueFrom(this.oculusLib$.pipe(take(1), timeout(sToMs(30)), catchError(() => of(null))));
if(!oculusLib){
@@ -11,11 +11,11 @@ import { BS_EXECUTABLE } from "../../constants";
import log from "electron-log";
import { deleteFolder, pathExist, unlinkPath } from "../../helpers/fs.helpers";
import { lastValueFrom } from "rxjs";
import JSZip from "jszip";
import { extractZip } from "../../helpers/zip.helpers";
import recursiveReadDir from "recursive-readdir";
import { sToMs } from "../../../shared/helpers/time.helpers";
import { ensureDir, pathExistsSync } from "fs-extra";
import { pathExistsSync } from "fs-extra";
import { BsmZipExtractor } from "../../models/bsm-zip-extractor.class";
import crypto from "crypto";
export class BsModsManagerService {
private static instance: BsModsManagerService;
@@ -45,6 +45,7 @@ export class BsModsManagerService {
this.bsLocalService = BSLocalVersionService.getInstance();
this.utilsService = UtilsService.getInstance();
this.requestService = RequestService.getInstance();
this.utilsService = UtilsService.getInstance();
}
private async getModFromHash(hash: string): Promise<Mod> {
@@ -123,7 +124,7 @@ export class BsModsManagerService {
return this.getIpaFromHash(injectorMd5);
}
private async downloadZip(zipUrl: string): Promise<JSZip> {
private async downloadZip(zipUrl: string): Promise<BsmZipExtractor> {
zipUrl = path.join(this.beatModsApi.BEAT_MODS_URL, zipUrl);
log.info("Download mod zip", zipUrl);
@@ -139,10 +140,7 @@ export class BsModsManagerService {
return null;
}
return JSZip.loadAsync(buffer).catch(e => {
log.error("ZIP", "Error while loading zip", e);
return null;
});
return BsmZipExtractor.fromBuffer(buffer);
}
private async executeBSIPA(version: BSVersion, args: string[]): Promise<boolean> {
@@ -209,43 +207,38 @@ export class BsModsManagerService {
log.info("Start download mod zip", mod.name, download.url);
const zip = await this.downloadZip(download.url);
log.info("Mod zip download end", mod.name, download.url, !!zip);
log.info("Mod zip download end", mod.name, download.url);
if (!zip) {
return false;
}
const crypto = require("crypto");
const files = await zip.files;
let hashCount = 0;
for await (const entry of zip.entries()) {
const buffer = await entry.read();
const md5Hash = crypto.createHash("md5")
.update(buffer)
.digest("hex");
hashCount += +download.hashMd5.some(md5 => md5.hash === md5Hash);
}
const checkedEntries = (
await Promise.all(
Object.values(files).map(async entry => {
const data = await entry.async("nodebuffer");
const entryMd5 = crypto.createHash("md5").update(data).digest("hex");
return download.hashMd5.some(md5 => md5.hash === entryMd5) ? entry : undefined;
})
).catch(e => {
log.error("Error while checking mod zip entries", mod.name, e);
throw e;
})
).filter(entry => !!entry);
if (checkedEntries.length !== download.hashMd5.length) {
if (hashCount !== download.hashMd5.length) {
return false;
}
const verionPath = await this.bsLocalService.getVersionPath(version);
const versionPath = await this.bsLocalService.getVersionPath(version);
const isBSIPA = mod.name.toLowerCase() === "bsipa";
const destDir = isBSIPA ? verionPath : path.join(verionPath, ModsInstallFolder.PENDING);
const destDir = isBSIPA ? versionPath : path.join(versionPath, ModsInstallFolder.PENDING);
await ensureDir(destDir);
log.info("Start extracting mod zip", mod.name, "to", destDir);
const extracted = await extractZip(zip, destDir)
const extracted = await zip.extract(destDir)
.then(() => true)
.catch(e => {
log.error("Error while extracting mod zip", e);
return false;
})
.finally(() => {
zip.close();
});
log.info("Mod zip extraction end", mod.name, "to", destDir, "success:", extracted);
@@ -278,7 +271,7 @@ export class BsModsManagerService {
return Array.from(
new Map<string, Mod>(
availableMods.reduce((res, mod) => {
if (this.isDependency(mod, mods, availableMods)) {
if (mod.required || this.isDependency(mod, mods, availableMods)) {
res.push([mod.name, mod]);
}
return res;
@@ -357,13 +350,13 @@ export class BsModsManagerService {
}
public async installMods(mods: Mod[], version: BSVersion): Promise<InstallModsResult> {
const deps = await this.resolveDependencies(mods, version);
mods.push(...deps);
if (!mods?.length) {
throw "no-mods";
}
const deps = await this.resolveDependencies(mods, version);
mods.push(...deps);
const bsipa = mods.find(mod => mod.name.toLowerCase() === "bsipa");
if (bsipa) {
mods = mods.filter(mod => mod.name.toLowerCase() !== "bsipa");
@@ -14,7 +14,7 @@ export class BeatSaverApiService {
private readonly request: RequestService;
private readonly bsaverApiUrl = "https://beatsaver.com/api";
private readonly bsaverApiUrl = "https://api.beatsaver.com";
private constructor() {
this.request = RequestService.getInstance();
@@ -0,0 +1,42 @@
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { BsmImage } from "renderer/components/shared/bsm-image.component";
import { ModalComponent, ModalExitCode } from "renderer/services/modale.service";
import BeatConflict from "../../../../../assets/images/apngs/beat-conflict.png";
import { BSVersion } from "shared/bs-version.interface";
import { BsmCheckbox } from "renderer/components/shared/bsm-checkbox.component";
import { useTranslationV2 } from "renderer/hooks/use-translation.hook";
import { useState } from "react";
import { ConfigurationService } from "renderer/services/configuration.service";
import { useService } from "renderer/hooks/use-service.hook";
export const BSVersionOutdatedModal: ModalComponent<void, { outdated: BSVersion, recommended: BSVersion }> = ({ resolver, data : { outdated, recommended }}) => {
const t = useTranslationV2();
const config = useService(ConfigurationService);
const [remember, setRemember] = useState(false);
const handleContinue = () => {
if (remember) {
config.set("not-show-bs-version-outdated-modal", true);
}
resolver({ exitCode: ModalExitCode.COMPLETED });
};
return (
<form className="text-gray-800 dark:text-gray-200 max-w-sm">
<h1 className="text-3xl uppercase tracking-wide w-full text-center">{t.text("misc.warning")}</h1>
<BsmImage className="mx-auto h-24" image={BeatConflict} />
<p>{t.element("modals.bs-version-outdated.body", { outdatedVersion: <b>{outdated.BSVersion}</b>, recommendedVersion: <b>{recommended.BSVersion}</b> })}</p>
<div className="flex items-center relative py-2 gap-1 mt-1">
<BsmCheckbox className="h-5 relative z-[1]" checked={remember} onChange={val => setRemember(() => val)} />
<span className="italic">{t.text("modals.misc.remember-my-choice")}</span>
</div>
<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" />
<BsmButton typeColor="primary" className="rounded-md flex justify-center items-center transition-all" onClick={handleContinue} withBar={false} text="misc.continue" />
</div>
</form>
);
};
@@ -34,7 +34,7 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
const [filter, setFilter] = useState<MapFilter>({});
const [query, setQuery] = useState("");
const [maps, setMaps] = useState<BsvMapDetail[]>([]);
const [sortOrder, setSortOrder] = useState<SearchOrder>(BSV_SORT_ORDER.at(0));
const [sortOrder, setSortOrder] = useState<SearchOrder>("Relevance");
const [ownedMapHashs, setOwnedMapHashs] = useState<string[]>(ownedMaps?.map(map => map.hash) ?? []);
const [loading, setLoading] = useState(false);
const isOnline = useObservable(() => os.isOnline$);
@@ -195,7 +195,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} onChange={sort => handleSortChange(sort)} selected={sortOrder}/>
</div>
<ul className="w-full grow flex content-start flex-wrap gap-2 pt-1.5 px-2 overflow-y-scroll overflow-x-hidden scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900 z-0">
{maps.length === 0 ? (
@@ -35,7 +35,7 @@ export function SupportersView({ isVisible, setVisible }: Props) {
(
{isVisible && (
<motion.div className="fixed top-0 left-0 w-full h-full bg-black bg-opacity-90 z-40 text-gray-200" transition={{ duration: 0.3 }} initial={{ opacity: 0, y: "-100%" }} animate={{ opacity: 1, y: "0%" }} exit={{ opacity: 0, y: "-100%" }}>
<BsmButton className="absolute right-10 top-10 !bg-transparent w-7 h-7" icon="cross" withBar={false} onClick={() => setVisible(false)} />
<BsmButton className="absolute right-10 top-10 !bg-transparent w-7 h-7" iconClassName="text-white size-full" icon="cross" withBar={false} onClick={() => setVisible(false)} />
{(!!sponsors.length || !!supporters.length) && <img className="absolute bottom-5 right-5 rotate-45 w-32 h-32" src={ManheraChanGif} />}
<div className="w-full h-full overflow-y-scroll">
{!!sponsors.length && <SupportersBlock className="mt-12" title="pages.settings.patreon.view.sponsors" supporters={sponsors} />}
@@ -60,8 +60,10 @@ import { ChevronTopIcon } from "./icons/chevron-top-icon.component";
import { EyeCrossIcon } from "./icons/eye-cross-icon.component";
import { ShortcutIcon } from "./icons/shortcut-icon.component";
import { BackupRestoreIcon } from "./icons/backup-restore-icon.component";
import { CancelIcon } from "./icons/cancel-icon.component";
import { WarningIcon } from "./icons/warning-icon.component";
export type BsmIconType = BsvMapCharacteristic | ("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" | "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 = BsvMapCharacteristic | ("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" | "cancel" | "warning" | "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
@@ -250,6 +252,15 @@ export const BsmIcon = memo(({ className, icon, style }: { className?: string; i
if (icon === "backup-restore") {
return <BackupRestoreIcon className={className} style={style} />;
}
if(icon === "cancel"){
return <CancelIcon className={className} style={style} />;
}
if(icon === "warning"){
return <WarningIcon className={className} style={style} />;
}
return <TrashIcon className={className} style={style} />;
};
@@ -0,0 +1,9 @@
import { createSvgIcon } from "../svg-icon.type";
export const CancelIcon = createSvgIcon((props, ref) => {
return (
<svg ref={ref} {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor">
<path d="M480-421.37 592.17-309.2q11.72 11.72 29.32 11.6 17.6-.12 29.31-11.83 11.72-11.72 11.72-29.32t-11.72-29.32L538.63-480 650.8-592.17q11.72-11.72 11.72-29.32T650.8-650.8q-11.71-11.72-29.31-11.72-17.6 0-29.32 11.72L480-538.63 367.83-650.8q-11.72-11.72-29.2-11.72-17.48 0-29.2 11.72-11.71 11.71-11.71 29.31 0 17.6 11.71 29.32L421.37-480 309.2-367.83q-11.72 11.72-11.6 29.2.12 17.48 11.83 29.2 11.72 11.71 29.32 11.71t29.32-11.71L480-421.37Zm0 349.5q-84.91 0-159.35-32.1-74.43-32.1-129.51-87.17-55.07-55.08-87.17-129.51-32.1-74.44-32.1-159.35t32.1-159.35q32.1-74.43 87.17-129.51 55.08-55.07 129.51-87.17 74.44-32.1 159.35-32.1t159.35 32.1q74.43 32.1 129.51 87.17 55.07 55.08 87.17 129.51 32.1 74.44 32.1 159.35t-32.1 159.35q-32.1 74.43-87.17 129.51-55.08 55.07-129.51 87.17-74.44 32.1-159.35 32.1Zm0-91q133.04 0 225.09-92.04 92.04-92.05 92.04-225.09 0-133.04-92.04-225.09-92.05-92.04-225.09-92.04-133.04 0-225.09 92.04-92.04 92.05-92.04 225.09 0 133.04 92.04 225.09 92.05 92.04 225.09 92.04ZM480-480Z"/>
</svg>
)
})
@@ -0,0 +1,9 @@
import { createSvgIcon } from "../svg-icon.type";
export const WarningIcon = createSvgIcon((props, ref) => {
return (
<svg ref={ref} {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor">
<path d="M80-78q-15 0-27-8t-19-19q-7-11-7.5-25t7.5-28l400-689q8-14 20-20t26-6q14 0 26 6t20 20l400 689q8 14 7.5 28t-7.5 25q-7 11-19 19t-27 8H80Zm401-135q22 0 37.5-15.5T534-266q0-22-15.5-37.5T481-319q-22 0-37.5 15.5T428-266q0 22 15.5 37.5T481-213Zm0-143q22 0 37.5-15.5T534-409v-98q0-22-15.5-37.5T481-560q-22 0-37.5 15.5T428-507v98q0 22 15.5 37.5T481-356Z"/>
</svg>
);
})
@@ -1,8 +1,7 @@
import { motion } from "framer-motion";
import { ChangeEvent, useEffect, useState } from "react";
import { ChangeEvent, useEffect, useMemo, useState } from "react";
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { useObservable } from "renderer/hooks/use-observable.hook";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { useTranslationV2 } from "renderer/hooks/use-translation.hook";
import { BSLauncherService, LaunchMods } from "renderer/services/bs-launcher.service";
import { ConfigurationService } from "renderer/services/configuration.service";
import { BSVersion } from "shared/bs-version.interface";
@@ -14,15 +13,21 @@ import { BsStore } from "shared/models/bs-store.enum";
import { lastValueFrom } from "rxjs";
import { BsDownloaderService } from "renderer/services/bs-version-download/bs-downloader.service";
import equal from "fast-deep-equal";
import { BSVersionManagerService } from "renderer/services/bs-version-manager.service";
import { safeLt } from "shared/helpers/semver.helpers";
import { WarningIcon } from "renderer/components/svgs/icons/warning-icon.component";
import Tippy from "@tippyjs/react";
import { motion } from "framer-motion"
type Props = { version: BSVersion };
export function LaunchSlide({ version }: Props) {
const t = useTranslation();
const { text: t, element: te } = useTranslationV2();
const configService = useService(ConfigurationService);
const bsLauncherService = useService(BSLauncherService);
const bsDownloader = useService(BsDownloaderService);
const versions = useService(BSVersionManagerService);
const [oculusMode, setOculusMode] = useState(!!configService.get<boolean>(LaunchMods.OCULUS_MOD));
const [desktopMode, setDesktopMode] = useState(!!configService.get<boolean>(LaunchMods.DESKTOP_MOD));
@@ -62,7 +67,7 @@ export function LaunchSlide({ version }: Props) {
.map(arg => arg.trim())
.filter(arg => arg.length > 0)
: undefined;
const launch$ = bsLauncherService.launch({
version,
oculus: version.oculus ? false : oculusMode,
@@ -70,15 +75,28 @@ export function LaunchSlide({ version }: Props) {
debug: debugMode,
additionalArgs
});
return lastValueFrom(launch$).catch(() => {});
};
const isOutdated = useMemo(() => {
return safeLt(version?.BSVersion, versions.getRecommendedVersion()?.BSVersion);
}, [version]);
return (
<div className="w-full shrink-0 items-center relative flex flex-col justify-start">
<div className="flex flex-col gap-3 justify-center items-center mb-5">
<BsmImage className="relative object-cover h-28" image={BSLogo} />
<h1 className="relative text-4xl font-bold italic -top-3">{version.name ? `${version.BSVersion} - ${version.name}` : version.BSVersion}</h1>
<h1 className="relative text-4xl font-bold italic -top-3">
{version.name ? `${version.BSVersion} - ${version.name}` : version.BSVersion}
{isOutdated && (
<Tippy theme="default" content={te("pages.version-viewer.launch-mods.outdated-tippy", { recommendedVersion: <b>{versions.getRecommendedVersion().BSVersion}</b> })}>
<span className="absolute bg-theme-1 size-7 rounded-full p-1.5 flex justify-center items-center -right-2 top-2 translate-x-full translate-y-px cursor-help">
<WarningIcon className="text-warning-400 animate-pulse"/>
</span>
</Tippy>
)}
</h1>
</div>
<div className="grid grid-flow-col gap-6">
{!(version.oculus || version.metadata?.store === BsStore.OCULUS) && <LaunchModToogle infoText="pages.version-viewer.launch-mods.oculus-description" icon="oculus" onClick={() => setMode(LaunchMods.OCULUS_MOD, !oculusMode)} active={oculusMode} text="pages.version-viewer.launch-mods.oculus" />}
@@ -100,10 +118,10 @@ export function LaunchSlide({ version }: Props) {
</motion.div>
</div>
<div className='grow flex justify-center items-center'>
<BsmButton
onClick={launch}
<BsmButton
onClick={launch}
active={JSON.stringify(version) === JSON.stringify(versionRunning)}
className='relative -translate-y-1/2 text-5xl text-gray-800 dark:text-gray-200 font-bold tracking-wide pt-1 pb-3 px-7 rounded-lg shadow-md italic shadow-black active:scale-90 transition-transform'
className='relative -translate-y-1/2 text-5xl text-gray-800 dark:text-gray-200 font-bold tracking-wide pt-1 pb-3 px-7 rounded-lg shadow-md italic shadow-black active:scale-90 transition-transform'
text="misc.launch"
disabled={equal(version, versionDownloading)}
/>
@@ -111,3 +129,5 @@ export function LaunchSlide({ version }: Props) {
</div>
);
}
@@ -9,10 +9,18 @@ import { Mod } from "shared/models/mods/mod.interface";
import { ModItem } from "./mod-item.component";
import { useService } from "renderer/hooks/use-service.hook";
type Props = { modsMap: Map<string, Mod[]>; installed: Map<string, Mod[]>; modsSelected: Mod[]; onModChange: (selected: boolean, mod: Mod) => void; moreInfoMod?: Mod; onWantInfos: (mod: Mod) => void };
type Props = {
modsMap: Map<string, Mod[]>;
installed: Map<string, Mod[]>;
modsSelected: Mod[];
onModChange: (selected: boolean, mod: Mod) => void;
moreInfoMod?: Mod;
onWantInfos: (mod: Mod) => void
unselectAllMods?: () => void;
};
export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreInfoMod, onWantInfos, unselectAllMods }: Props) {
export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreInfoMod, onWantInfos }: Props) {
const pageState = useService(PageStateService);
const modsManager = useService(BsModsManagerService);
@@ -65,12 +73,15 @@ export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreIn
<span className="z-10 sticky flex items-center justify-end top-0 bg-inherit border-b-2 border-main-color-1">
<BsmButton className="rounded-full h-6 w-6 p-[2px]" withBar={false} icon="search" onClick={handleToogleFilter} />
</span>
<span className="z-10 sticky top-0 flex items-center bg-inherit border-main-color-1 border-b-2 h-8 px-1 whitespace-nowrap">{filterEnabled ? <motion.input autoFocus className="bg-main-color-1 rounded-md h-6 px-2" initial={{ width: 0 }} animate={{ width: "250px" }} transition={{ ease: "easeInOut", duration: 0.15 }} onChange={e => handleInput(e.target.value)} /> : <span className="w-full text-center">{t("pages.version-viewer.mods.mods-grid.header-bar.name")}</span>}</span>
<span className="z-10 sticky top-0 flex items-center bg-inherit border-main-color-1 border-b-2 h-8 px-1 whitespace-nowrap">{filterEnabled ? <motion.input autoFocus className="bg-light-main-color-1 dark:bg-main-color-1 rounded-md h-6 px-2" initial={{ width: 0 }} animate={{ width: "250px" }} transition={{ ease: "easeInOut", duration: 0.15 }} onChange={e => handleInput(e.target.value)} /> : <span className="w-full text-center">{t("pages.version-viewer.mods.mods-grid.header-bar.name")}</span>}</span>
<span className="z-10 sticky flex items-center justify-center top-0 bg-inherit border-b-2 border-main-color-1 h-8 px-2 whitespace-nowrap">{t("pages.version-viewer.mods.mods-grid.header-bar.installed")}</span>
<span className="z-10 sticky flex items-center justify-center top-0 bg-inherit border-b-2 border-main-color-1 h-8 px-2 whitespace-nowrap">{t("pages.version-viewer.mods.mods-grid.header-bar.latest")}</span>
<span className="z-10 sticky flex items-center justify-center top-0 bg-inherit border-b-2 border-main-color-1 h-8 whitespace-nowrap">{t("pages.version-viewer.mods.mods-grid.header-bar.description")}</span>
<span className="z-10 sticky top-0 bg-inherit border-b-2 border-main-color-1 h-8 flex justify-start items-center py-1 pl-[3px] min-w-[50px]">
<BsmDropdownButton className="h-full aspect-square relative rounded-full bg-light-main-color-1 dark:bg-main-color-3" withBar={false} icon="three-dots" buttonClassName="!rounded-full !p-[2px] !bg-light-main-color-2 dark:!bg-main-color-2 hover:!bg-light-main-color-1 dark:hover:!bg-main-color-3" menuTranslationY="5px" items={[{ text: "pages.version-viewer.mods.mods-grid.header-bar.dropdown.uninstall-all", icon: "trash", onClick: handleUninstallAll }]} />
<BsmDropdownButton className="h-full aspect-square relative rounded-full bg-light-main-color-1 dark:bg-main-color-3" withBar={false} icon="three-dots" buttonClassName="!rounded-full !p-[2px] !bg-light-main-color-2 dark:!bg-main-color-2 hover:!bg-light-main-color-1 dark:hover:!bg-main-color-3" menuTranslationY="5px" items={[
{ text: "pages.version-viewer.mods.mods-grid.header-bar.dropdown.unselect-all", icon: "cancel", onClick: () => unselectAllMods?.() },
{ text: "pages.version-viewer.mods.mods-grid.header-bar.dropdown.uninstall-all", icon: "trash", onClick: () => handleUninstallAll?.() }
]} />
</span>
{Array.from(modsMap.keys()).map(
@@ -116,6 +116,10 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
});
};
const unselectAllMods = () => {
setModsSelected(() => []);
};
useEffect(() => {
const subs: Subscription[] = [];
@@ -181,7 +185,7 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
return (
<>
<div className="grow overflow-scroll w-full min-h-0 scrollbar-thin scrollbar-thumb-neutral-900 scrollbar-thumb-rounded-full">
<ModsGrid modsMap={modsAvailable} installed={modsInstalled} modsSelected={modsSelected} onModChange={handleModChange} moreInfoMod={moreInfoMod} onWantInfos={handleMoreInfo} />
<ModsGrid modsMap={modsAvailable} installed={modsInstalled} modsSelected={modsSelected} onModChange={handleModChange} moreInfoMod={moreInfoMod} onWantInfos={handleMoreInfo} unselectAllMods={unselectAllMods} />
</div>
<div className="h-10 shrink-0 flex items-center justify-between px-3">
<BsmButton className="text-center rounded-md px-2 py-[2px]" text="pages.version-viewer.mods.buttons.more-infos" typeColor="cancel" withBar={false} disabled={!moreInfoMod} onClick={handleOpenMoreInfo} style={{ width: downloadWith }} />
@@ -8,7 +8,7 @@ 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"),
],
]
};
export type DefaultConfigKey = "first-color" | "second-color" | "theme" | "language" | "supported_languages" | "default_mods" | "default-shared-folders";
@@ -1,7 +1,12 @@
import { I18nService } from "renderer/services/i18n.service";
import { useService } from "./use-service.hook";
import { useObservable } from "./use-observable.hook";
import { useConstant } from "./use-constant.hook";
import { createElement, Fragment } from "react";
/**
* @deprecated Use useTranslationV2 instead
*/
export function useTranslation(): (translationKey: string, args?: Record<string, string>) => string {
const i18nService = useService(I18nService);
@@ -16,3 +21,35 @@ export function useTranslation(): (translationKey: string, args?: Record<string,
return tranlatables.map(key => i18nService.translate(key, args)).join(" ");
};
}
type TranslateFunctions = {
text: (translationKey: string, args?: Record<string, string>) => string;
element: (translationKey: string, args?: Record<string, JSX.Element>) => JSX.Element;
}
export function useTranslationV2(): TranslateFunctions {
const i18nService = useService(I18nService);
useObservable(() => i18nService.currentLanguage$);
const text = useConstant(() => {
return (key: string, args?: Record<string, string>) => {
if (!key) {
return key;
}
return i18nService.translate(key, args);
};
});
const element = useConstant(() => {
return (key: string, args?: Record<string, JSX.Element>) => {
if (!key) {
return createElement(Fragment, null, key);
}
return i18nService.translateElement(key, args);
};
});
return { text, element };
}
+7
View File
@@ -96,6 +96,13 @@
@apply border-r-neutral-900
}
.bg-theme-1 { @apply bg-light-main-color-1 dark:bg-main-color-1; }
.bg-theme-2 { @apply bg-light-main-color-2 dark:bg-main-color-2; }
.bg-theme-3 { @apply bg-light-main-color-3 dark:bg-main-color-3; }
.\!bg-theme-1 { @apply !bg-light-main-color-1 dark:!bg-main-color-1; }
.\!bg-theme-2 { @apply !bg-light-main-color-2 dark:!bg-main-color-2; }
.\!bg-theme-3 { @apply !bg-light-main-color-3 dark:!bg-main-color-3; }
@keyframes glowing {
0% {
background-position: 0 0;
@@ -11,6 +11,11 @@ import { useService } from "renderer/hooks/use-service.hook";
import { BSVersion } from "shared/bs-version.interface";
import { useObservable } from "renderer/hooks/use-observable.hook";
import { BsDownloaderService } from "renderer/services/bs-version-download/bs-downloader.service";
import { logRenderError } from "renderer";
import { ModalExitCode, ModalService } from "renderer/services/modale.service";
import { BSVersionOutdatedModal } from "renderer/components/modal/modal-types/bs-version-outdated-modal.component";
import { ConfigurationService } from "renderer/services/configuration.service";
import { safeLt } from "shared/helpers/semver.helpers";
export const AvailableVersionsContext = createContext<{ selectedVersion: BSVersion; setSelectedVersion: (version: BSVersion) => void }>(null);
@@ -18,6 +23,8 @@ export function AvailableVersionsList() {
const bsDownloader = useService(BsDownloaderService);
const versionManager = useService(BSVersionManagerService);
const modals = useService(ModalService);
const config = useService(ConfigurationService);
const [selectedVersion, setSelectedVersion] = useState<BSVersion>(null);
const contextValue = useMemo(() => ({ selectedVersion, setSelectedVersion }), [selectedVersion]);
@@ -25,10 +32,23 @@ export function AvailableVersionsList() {
const downloading = useObservable(() => bsDownloader.downloadingVersion$.pipe(map(v => !!v)));
const t = useTranslation();
const startDownload = async () => {
const startDownload = async (version: BSVersion) => {
const showOutdated = !config.get("not-show-bs-version-outdated-modal");
const recommendedVersion = versionManager.getRecommendedVersion();
return bsDownloader.downloadVersion(selectedVersion)
.catch(console.log)
const isVersionOutdated = safeLt(version.BSVersion, recommendedVersion?.BSVersion);
if (showOutdated && isVersionOutdated) {
const res = await modals.openModal(BSVersionOutdatedModal, { outdated: version, recommended: recommendedVersion });
if (res.exitCode !== ModalExitCode.COMPLETED) {
return;
}
}
return bsDownloader.downloadVersion(version)
.catch(logRenderError)
.finally(() => setSelectedVersion(null));
};
@@ -45,17 +65,17 @@ export function AvailableVersionsList() {
return (
<div className="relative h-full w-full flex items-center flex-col pt-2">
<Slideshow className="absolute w-full h-full top-0" />
<h1 className="text-gray-100 text-2xl mb-4 z-[1]">{t("pages.available-versions.title")}</h1>
<AvailableVersionsContext.Provider value={contextValue}>
<AvailableVersionsSlider />
</AvailableVersionsContext.Provider>
<AnimatePresence>
{selectedVersion && !downloading && (
<motion.div initial={{ y: "150%" }} animate={{ y: "0%" }} exit={{ y: "150%" }} className="absolute bottom-5" onClick={startDownload}>
<motion.div initial={{ y: "150%" }} animate={{ y: "0%" }} exit={{ y: "150%" }} className="absolute bottom-5" onClick={() => startDownload(selectedVersion)}>
<BsmButton text="misc.download" className="relative text-gray-800 dark:text-gray-100 rounded-md text-3xl font-bold italic tracking-wide px-3 pb-2 pt-1 shadow-md shadow-black" />
</motion.div>
)}
+22 -12
View File
@@ -273,7 +273,7 @@ export function SettingsPage() {
<span className="font-extrabold">{t("notifications.settings.additional-content.deep-link.select-all")}</span>
</div>
<div className="flex h-full gap-2">
<Tippy content="BeatSaver" placement="top" className="font-bold bg-main-color-3" arrow={false} duration={[200, 0]}>
<Tippy content="BeatSaver" placement="top" className="font-bold bg-main-color-3" theme="default" arrow={false} duration={[200, 0]}>
<BsmImage
className="h-8 cursor-pointer"
image={beatSaverIcon}
@@ -283,7 +283,7 @@ export function SettingsPage() {
}}
/>
</Tippy>
<Tippy content="BeastSaber" placement="top" className="font-bold bg-main-color-3" arrow={false} duration={[200, 0]}>
<Tippy content="BeastSaber" placement="top" className="font-bold bg-main-color-3" theme="default" arrow={false} duration={[200, 0]}>
<BsmImage
className="h-8 rounded-md cursor-pointer"
image={beastSaberIcon}
@@ -293,7 +293,7 @@ export function SettingsPage() {
}}
/>
</Tippy>
<Tippy content="ScoreSaber" placement="top" className="font-bold bg-main-color-3" arrow={false} duration={[200, 0]}>
<Tippy content="ScoreSaber" placement="top" className="font-bold bg-main-color-3" theme="default" arrow={false} duration={[200, 0]}>
<BsmImage
className="h-8 cursor-pointer"
image={scoreSaberIcon}
@@ -303,7 +303,7 @@ export function SettingsPage() {
}}
/>
</Tippy>
<Tippy content="BeatLeader" placement="top" className="font-bold bg-main-color-3" arrow={false} duration={[200, 0]}>
<Tippy content="BeatLeader" placement="top" className="font-bold bg-main-color-3" theme="default" arrow={false} duration={[200, 0]}>
<BsmImage
className="h-8 cursor-pointer"
image={beatleaderIcon}
@@ -313,7 +313,7 @@ export function SettingsPage() {
}}
/>
</Tippy>
<Tippy content="ModelSaber" placement="top" className="font-bold bg-main-color-3" arrow={false} duration={[200, 0]}>
<Tippy content="ModelSaber" placement="top" className="font-bold bg-main-color-3" theme="default" arrow={false} duration={[200, 0]}>
<BsmImage
className="h-8 cursor-pointer"
image={modelSaberIcon}
@@ -332,7 +332,7 @@ export function SettingsPage() {
<span className="font-extrabold">{t("misc.maps")}</span>
</div>
<div className="flex h-full gap-2">
<Tippy content="BeatSaver" placement="top" className="font-bold bg-main-color-3" arrow={false} duration={[200, 0]}>
<Tippy content="BeatSaver" placement="top" className="font-bold bg-main-color-3" theme="default" arrow={false} duration={[200, 0]}>
<BsmImage
className="h-8 cursor-pointer"
image={beatSaverIcon}
@@ -342,7 +342,7 @@ export function SettingsPage() {
}}
/>
</Tippy>
<Tippy content="BeastSaber" placement="top" className="font-bold bg-main-color-3" arrow={false} duration={[200, 0]}>
<Tippy content="BeastSaber" placement="top" className="font-bold bg-main-color-3" theme="default" arrow={false} duration={[200, 0]}>
<BsmImage
className="h-8 rounded-md cursor-pointer"
image={beastSaberIcon}
@@ -352,7 +352,7 @@ export function SettingsPage() {
}}
/>
</Tippy>
<Tippy content="ScoreSaber" placement="top" className="font-bold bg-main-color-3" arrow={false} duration={[200, 0]}>
<Tippy content="ScoreSaber" placement="top" className="font-bold bg-main-color-3" theme="default" arrow={false} duration={[200, 0]}>
<BsmImage
className="h-8 cursor-pointer"
image={scoreSaberIcon}
@@ -362,7 +362,7 @@ export function SettingsPage() {
}}
/>
</Tippy>
<Tippy content="BeatLeader" placement="top" className="font-bold bg-main-color-3" arrow={false} duration={[200, 0]}>
<Tippy content="BeatLeader" placement="top" className="font-bold bg-main-color-3" theme="default" arrow={false} duration={[200, 0]}>
<BsmImage
className="h-8 cursor-pointer"
image={beatleaderIcon}
@@ -379,8 +379,8 @@ export function SettingsPage() {
<BsmCheckbox className="relative z-[1] h-5 w-5" onChange={() => tooglePlaylistsDeepLinks()} checked={playlistsDeepLinkEnabled} />
<span className="font-extrabold">{t("misc.playlists")}</span>
</div>
<div className="flex h-full">
<Tippy content="BeatSaver" placement="top" className="font-bold bg-main-color-3" arrow={false} duration={[200, 0]}>
<div className="flex h-full gap-2">
<Tippy content="BeatSaver" placement="top" className="font-bold bg-main-color-3" theme="default" arrow={false} duration={[200, 0]}>
<BsmImage
className="h-8 cursor-pointer"
image={beatSaverIcon}
@@ -390,6 +390,16 @@ export function SettingsPage() {
}}
/>
</Tippy>
<Tippy content="BeatLeader" placement="top" className="font-bold bg-main-color-3" theme="default" arrow={false} duration={[200, 0]}>
<BsmImage
className="h-8 cursor-pointer"
image={beatleaderIcon}
onClick={e => {
e.stopPropagation();
linkOpener.open("https://www.beatleader.xyz/");
}}
/>
</Tippy>
</div>
</li>
<li className="bg-light-main-color-1 dark:bg-main-color-1 rounded-md group-one flex justify-between items-center basis-0 py-2 px-3 cursor-pointer" onClick={() => toogleModelsDeepLinks()}>
@@ -398,7 +408,7 @@ export function SettingsPage() {
<span className="font-extrabold">{t("misc.models")}</span>
</div>
<div className="flex h-full">
<Tippy content="ModelSaber" placement="top" className="font-bold bg-main-color-3" arrow={false} duration={[200, 0]}>
<Tippy content="ModelSaber" placement="top" className="font-bold bg-main-color-3" theme="default" arrow={false} duration={[200, 0]}>
<BsmImage
className="h-8 cursor-pointer"
image={modelSaberIcon}
@@ -21,6 +21,9 @@ import { CreateLaunchShortcutModal } from "renderer/components/modal/modal-types
import { lastValueFrom } from "rxjs";
import { NotificationService } from "renderer/services/notification.service";
import { BsDownloaderService } from "renderer/services/bs-version-download/bs-downloader.service";
import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
import { safeLt } from "shared/helpers/semver.helpers";
import { ConfigurationService } from "renderer/services/configuration.service";
export function VersionViewer() {
@@ -31,11 +34,41 @@ export function VersionViewer() {
const ipcService = useService(IpcService);
const bsLauncher = useService(BSLauncherService);
const notification = useService(NotificationService);
const config = useService(ConfigurationService);
const { state } = useLocation() as { state: BSVersion };
const navigate = useNavigate();
const [currentTabIndex, setCurrentTabIndex] = useState(0);
useOnUpdate(() => {
checkIsVersionOutaded();
}, [state]);
const checkIsVersionOutaded = async () => {
if(config.get("not-show-bs-version-outdated-notification")){
return;
}
const recommended = bsVersionManagerService.getRecommendedVersion();
const isOutdated = safeLt(state.BSVersion, recommended?.BSVersion);
if(!isOutdated){
return;
}
const res = await notification.notifyWarning({ title: "notifications.bs-version-oudated.title", desc: "notifications.bs-version-oudated.msg", duration: 9000, actions: [
{ id: "0", title: "notifications.bs-version-oudated.actions.do-not-remind" },
{ id: "1", title: "notifications.bs-version-oudated.actions.ok", cancel: true }
] });
if(res === "0"){
config.set("not-show-bs-version-outdated-notification", true);
}
}
const navigateToVersion = (version?: BSVersion) => {
if (!version) {
return navigate("/available-versions");
@@ -195,4 +195,8 @@ export class BSVersionManagerService {
version
]))).values());
}
public getRecommendedVersion(): BSVersion | undefined {
return (this.availableVersions$.value ?? []).find(v => v.recommended);
}
}
+27
View File
@@ -4,6 +4,7 @@ import { Observable } from "rxjs";
import { ConfigurationService } from "./configuration.service";
import { getProperty } from "dot-prop";
import { i18n } from "dateformat";
import { createElement, Fragment } from "react";
export class I18nService {
private static instance: I18nService;
@@ -87,4 +88,30 @@ export class I18nService {
});
return translated;
}
public translateElement(translationKey: string, args?: Record<string, JSX.Element>): JSX.Element {
let translated = this.cache.get(translationKey);
if (!translated) {
translated = getProperty(this.dictionary, translationKey) ?? translationKey;
this.cache.set(translationKey, translated);
}
if (!args) {
return createElement(Fragment, null, translated);
}
// Sort keys by length in descending order to avoid partial matches within keys
const sortedKeys = Object.keys(args).sort((a, b) => b.length - a.length);
const pattern = sortedKeys.map(key => `\\{${key}\\}`).join('|');
const regex = new RegExp(`(${pattern})`, 'g');
const segments = translated.split(regex);
const children = segments.map((segment) => {
const key = segment.slice(1, -1); // Remove surrounding {}
return args[key] || segment;
});
return createElement(Fragment, null, ...children);
}
}
+8
View File
@@ -0,0 +1,8 @@
import { coerce, lt, valid } from "semver";
import { tryit } from "./error.helpers";
export function safeLt(a: string, b: string): boolean {
const { result } = tryit(() => lt(valid(coerce(a)), valid(coerce(b))));
return result ?? false;
}
+18
View File
@@ -0,0 +1,18 @@
const HashAlgorithmsLengths = {
sha1: 40,
} as const;
export function findHashInString(str: string, algorithm: keyof typeof HashAlgorithmsLengths = 'sha1'): string | undefined {
if(!str) { return undefined; }
const hashLength = HashAlgorithmsLengths[algorithm];
const regex = new RegExp(`[a-fA-F0-9]{${hashLength}}`, "g");
const match = regex.exec(str);
return match ? match[0] : undefined;
}
export function escapeRegExp(str: string): string {
// Regex taken from lodash escapeRegExp function
return str.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
}
@@ -15,6 +15,7 @@ export enum BSLaunchError{
BS_EXIT_ERROR = "EXIT",
OCULUS_LIB_NOT_FOUND = "OCULUS_LIB_NOT_FOUND",
UNKNOWN_ERROR = "UNKNOWN_ERROR",
ORIGINAL_OCULUS_NOT_INSTALLED = "ORIGINAL_OCULUS_NOT_INSTALLED"
}
export enum BSLaunchEvent{
+3 -2
View File
@@ -1,3 +1,4 @@
const { nextui } = require("@nextui-org/react");
const colors = require("tailwindcss/colors");
// Suppress deprecation warnings during build
@@ -9,9 +10,9 @@ delete colors.blueGray;
module.exports = {
darkMode: "class",
content: ["./src/renderer/**/*.{js,jsx,ts,tsx,ejs}"],
content: ["./src/renderer/**/*.{js,jsx,ts,tsx,ejs}", "./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}"],
mode: "jit",
plugins: [require("tailwind-scrollbar-hide"), require("tailwind-scrollbar")({ nocompatible: true }), require("tailwindcss-scoped-groups")({ groups: ["one"] }), require("ps-scrollbar-tailwind")],
plugins: [require("tailwindcss-scoped-groups")({ groups: ["one"] }), nextui()],
theme: {
colors: {
...colors,