mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 877879f335 | |||
| 7b658de727 | |||
| 229a149f00 | |||
| be2597fc85 | |||
| c8d888ee1a | |||
| 9033b1ef8f | |||
| 4a9f79c561 | |||
| fd30709af9 | |||
| c73bde2cd9 | |||
| 3027331924 | |||
| 66bab1176b | |||
| 6c6d47333d | |||
| c9286c5419 | |||
| 39739dfb49 | |||
| 5d4f12f72f | |||
| 7b1defcb76 | |||
| f7f5f768fc | |||
| 9340f4635d | |||
| 5aacb34ef4 | |||
| 4ee3f472ec | |||
| 59b7b9aedb | |||
| e1f421831b | |||
| 4f42b3ffcf | |||
| a4192ef411 | |||
| 2744df6803 | |||
| c4ee9ba155 | |||
| 5e981e5a20 | |||
| 578e79edac | |||
| 07d94962c4 | |||
| 57cd955780 | |||
| 1b33b29847 | |||
| f523e34867 | |||
| 5de1b7c3fb | |||
| 2f4bca108b | |||
| bfb6d28a34 | |||
| 8c6f3b0aa8 | |||
| 6bb90c580d | |||
| 4bf2da1065 | |||
| e17b7f70fd | |||
| 99b5dcd527 | |||
| 6c39e5f43d | |||
| 58788b0ebc | |||
| db616c6bd7 | |||
| 0fd4d51898 | |||
| d0831f2e44 | |||
| c54d1491e6 | |||
| dd08f418d5 | |||
| 2b37972dd4 | |||
| 68559add5d | |||
| 7c6c6147a6 | |||
| 0a1b1752f0 | |||
| f594b0900f | |||
| 5cbc480e24 | |||
| 8208874527 | |||
| 2d2fbec858 | |||
| d5f2ba3b21 | |||
| 9e60dc4e0e | |||
| 57e1029b75 | |||
| 0d913641d7 | |||
| b7eb136395 | |||
| a79a6c2c13 | |||
| d01edbe2a8 | |||
| 893b244eb1 | |||
| b45c37d774 | |||
| c18acb5fed | |||
| 7211798c74 | |||
| 589904f813 | |||
| 8181cb3eec | |||
| ec09ee257c | |||
| d378aea1e7 | |||
| b04f199fff | |||
| 83f23c5c16 | |||
| 6666685c43 | |||
| e07aacc9e8 | |||
| 681ddc6b0b | |||
| aa69b3c683 | |||
| 599161b7b2 | |||
| 77987181de | |||
| 3812ea18e4 | |||
| 3797c1b156 | |||
| a1082edcde | |||
| de7a054f89 | |||
| 0398a957ad |
+1
-1
@@ -59,7 +59,7 @@ module.exports = {
|
||||
"react/function-component-definition": "off",
|
||||
"jsx-a11y/control-has-associated-label": "off",
|
||||
"react/button-has-type": "off",
|
||||
"no-labels": ["error", { "allowLoop": true }]
|
||||
"max-classes-per-file": "off",
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020,
|
||||
|
||||
@@ -16,3 +16,4 @@
|
||||
*.woff binary
|
||||
*.woff2 binary
|
||||
assets/scripts/* binary
|
||||
*.zip binary
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -785,7 +785,24 @@
|
||||
"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
|
||||
"recommended": true
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"1.5.0-alpha.1":{
|
||||
"htmlBody" :"<strong>BSManager continues to get better, thanks to your continued support and contributions!</strong><center>🎈🎉🎈</center><p>Note: This is an alpha release, encountering bugs is expected. Please file new issues for any bugs you find.</p><h2>Features</h2><ul><li>Added playlist support</li><li>Added Linux support (Thanks to @Insprill)</li><li>Added reset button to reset values when editing a Beat Saber version (Thanks to @Liborsaf)</li><li>Added a changelog modal</li></ul><h2>Fixes</h2><ul><li>Fixed an issue where switching between Beat Saber versions too quickly displayed mods for the wrong version</li><li>Fixed an issue where the launch window from a shortcut would never close</li><li>Advanced launch arguments are used even when the input field is closed</li><li>Fixed an issue where having many maps caused some maps to not have their information loaded</li></ul><h2>Other changes</h2><ul><li>.NET Framework is no longer required</li><li>Clear English translations for the Steam credentials popup (Thanks to @Aeywoo)</li><li>NSFW models are now blurred when browsing for models</li><li>When no mods are available for a Beat Saber version, the version number is now shown</li><li>Mods load faster</li><li>After a first full maps loading, maps load almost instantly</li><li>Improved performance in lists with lot of contents</li></ul>",
|
||||
"title" : "What's new ?",
|
||||
"timestamp" : 1721048400,
|
||||
"version" : "1.5.0-alpha.1"
|
||||
},
|
||||
"1.5.0-alpha.2":{
|
||||
"htmlBody" :"<strong>BSManager continues to get better, thanks to your continued support and contributions!</strong><center>🎈🎉🎈</center><p>Note: This is an alpha release, encountering bugs is expected. Please file new issues for any bugs you find.</p><h4>Alpha changes</h4><ul><li>Fixed an issue where BSIPA could not be installed if the BS version's path contained spaces #523</li><li>Fixed a crash when changing audio volume #518</li><li>Fixed an issue where playlist maps with a malformed hash were not loaded #517</li><li>Improved reliability of reading maps from a playlist file #517</li><li>Playlists in sub-folders are now loaded #514</li><li>Fixed a crash when loading an invalid playlist file #513</li></ul><h2>Features</h2><ul> <li>Added playlist support</li> <li>Added Linux support (Thanks to @Insprill)</li> <li>Added reset button to reset values when editing a Beat Saber version (Thanks to @Liborsaf)</li> <li>Added a changelog modal</li></ul><h3>Fixes</h3><ul><li>Fixed an issue where switching between Beat Saber versions too quickly displayed mods for the wrong version #472</li><li>Fixed an issue where the launch window from a shortcut would never close #485</li><li>Advanced launch arguments are used even when the input field is closed #496</li><li>Fixed an issue where having many maps caused some maps to not have their information loaded #503</li><li>Fixed an issue where downloading maps sometimes resulted in a timeout error #524</li><li>Fixed an issue where the duration of maps could be wrong in some cases #522</li></ul><h2>Other changes</h2><ul> <li>.NET Framework is no longer required</li> <li>Clear English translations for the Steam credentials popup (Thanks to @Aeywoo)</li> <li>NSFW models are now blurred when browsing for models</li> <li>When no mods are available for a Beat Saber version, the version number is now shown</li> <li>Mods load faster</li> <li>After a first full maps loading, maps load almost instantly</li> <li>Improved performance in lists with lot of contents</li></ul>",
|
||||
"title" : "What's new ?",
|
||||
"timestamp" : 1721665382,
|
||||
"version" : "1.5.0-alpha.2"
|
||||
}
|
||||
}
|
||||
+31
-27
@@ -54,65 +54,69 @@
|
||||
"type": "diamond",
|
||||
"link": "https://www.youtube.com/@lumberjack462"
|
||||
},
|
||||
{
|
||||
{
|
||||
"username": "Xero"
|
||||
},
|
||||
{
|
||||
{
|
||||
"username": "Minescence"
|
||||
},
|
||||
{
|
||||
{
|
||||
"username": "mereknom"
|
||||
},
|
||||
{
|
||||
{
|
||||
"username": "Jascha"
|
||||
},
|
||||
{
|
||||
{
|
||||
"username": "Celldweller",
|
||||
"type": "gold"
|
||||
},
|
||||
{
|
||||
{
|
||||
"username": "rhythmshade"
|
||||
},
|
||||
{
|
||||
{
|
||||
"username": "liborsaf"
|
||||
},
|
||||
{
|
||||
{
|
||||
"username": "aatame3",
|
||||
"type": "gold"
|
||||
},
|
||||
{
|
||||
{
|
||||
"username": "Joshua"
|
||||
},
|
||||
{
|
||||
{
|
||||
"username": "Stuijvi",
|
||||
"type": "gold"
|
||||
},
|
||||
{
|
||||
"username": "_ monaka"
|
||||
},
|
||||
{
|
||||
"username": "Better_Axel"
|
||||
},
|
||||
{
|
||||
{
|
||||
"username": "_ monaka"
|
||||
},
|
||||
{
|
||||
"username": "Better_Axel"
|
||||
},
|
||||
{
|
||||
"username": "Riley",
|
||||
"type": "gold"
|
||||
},
|
||||
{
|
||||
"username": "Austin"
|
||||
},
|
||||
{
|
||||
{
|
||||
"username": "Austin"
|
||||
},
|
||||
{
|
||||
"username": "Fatalution",
|
||||
"type": "diamond",
|
||||
"link": "https://x.com/fatalution"
|
||||
},
|
||||
{
|
||||
{
|
||||
"username": "Taurus Arcade",
|
||||
"type": "gold"
|
||||
},
|
||||
{
|
||||
"username": "Mozz_Zm"
|
||||
},
|
||||
{
|
||||
"username": "clapxz"
|
||||
},
|
||||
{
|
||||
"username": "Mozz_Zm"
|
||||
},
|
||||
{
|
||||
"username": "clapxz"
|
||||
}
|
||||
"username": "Reflected Chop",
|
||||
"type": "gold"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -15,7 +15,11 @@
|
||||
"apply": "Übernehmen",
|
||||
"copy": "Kopieren",
|
||||
"copied": "Kopiert!",
|
||||
"confirm": "Bestätigen"
|
||||
"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",
|
||||
@@ -34,6 +38,7 @@
|
||||
"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, 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"
|
||||
@@ -101,7 +106,8 @@
|
||||
"latest": "Neueste",
|
||||
"description": "Beschreibung",
|
||||
"dropdown": {
|
||||
"uninstall-all": "Alle deinstallieren"
|
||||
"uninstall-all": "Alle deinstallieren",
|
||||
"unselect-all": "Alle abwählen"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -157,8 +163,15 @@
|
||||
},
|
||||
"installation-folder": {
|
||||
"title": "Installations-Ordner",
|
||||
"description": "Ändern Sie den Ordner, der alle von BSManager heruntergeladenen Inhalte enthalten wird.",
|
||||
"choose-folder": "Ordner wählen"
|
||||
"description": "Ändern Sie den Ordner, der alle von BSManager heruntergeladenen Inhalte enthalten wird."
|
||||
},
|
||||
"proton-folder": {
|
||||
"title": "Proton-Ordner",
|
||||
"description": "Ändern Sie den Ordner zum Proton-Pfad. (z. B. Proton - Experimental)",
|
||||
"errors": {
|
||||
"title": "Das Einstellen des Proton-Ordners ist fehlgeschlagen",
|
||||
"invalid-folder": "Ungültiger Proton-Ordnerpfad"
|
||||
}
|
||||
},
|
||||
"additional-content": {
|
||||
"title": "Zusätzlicher Inhalt",
|
||||
@@ -180,6 +193,7 @@
|
||||
"zh-CN": "简体中文",
|
||||
"zh-TW": "正體中文",
|
||||
"ja-JP": "日本語",
|
||||
"ko-KR": "한국어",
|
||||
"translated": {
|
||||
"en-EN": "Englisch, Vereinigtes Königreich",
|
||||
"en-US": "Englisch, USA",
|
||||
@@ -189,7 +203,8 @@
|
||||
"ru-RU": "Russisch",
|
||||
"zh-CN": "Vereinfachtes Chinesisch",
|
||||
"zh-TW": "Traditionelles Chinesisch",
|
||||
"ja-JP": "Japanisch"
|
||||
"ja-JP": "Japanisch",
|
||||
"ko-KR": "Koreanisch"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -217,10 +232,6 @@
|
||||
"open-logs": "Protokolle öffnen"
|
||||
}
|
||||
},
|
||||
"changelogs": {
|
||||
"open" : "🚀 Neues im Changelog entdecken!",
|
||||
"not-founds": "😕 Hoppla, kein Changelog zu sehen!"
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Erweiterte",
|
||||
"description": "Erweiterte Einstellungen für BSManager.",
|
||||
@@ -432,8 +443,11 @@
|
||||
"OCULUS_NOT_RUNNING": "Oculus läuft nicht",
|
||||
"BS_ALREADY_RUNNING": "Beat Saber läuft bereits",
|
||||
"EXE_NOT_FINDED": "Dateien fehlen",
|
||||
"PROTON_NOT_SET": "Proton-Ordner nicht festgelegt",
|
||||
"PROTON_NOT_FOUND": "Proton-Binärdatei nicht gefunden",
|
||||
"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.",
|
||||
@@ -442,7 +456,10 @@
|
||||
"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."
|
||||
"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"
|
||||
@@ -575,6 +592,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": {
|
||||
@@ -782,7 +807,7 @@
|
||||
"original-version-backup-oculus": {
|
||||
"title": "Achtung",
|
||||
"body": {
|
||||
"must-be-installed-once": "Du musst Beat Saber mindestens einmal aus dem Oculus Store installiert haben, sonst könnte sich Beat Saber nach dem Start automatisch schließen.",
|
||||
"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",
|
||||
@@ -819,9 +844,17 @@
|
||||
"ask-install-path": {
|
||||
"title": "Installations Ordner",
|
||||
"choose-folder-description": "Wählen Sie den Ordner aus, der alle von BSManager heruntergeladenen Inhalte enthalten soll. (Versionen, Mods, Karten, Playlists, etc.)",
|
||||
"choose-folder": "Ordner Wählen",
|
||||
"default": "Standard",
|
||||
"default-tooltip": "Standardmäßig in Ihrem persönlichen Ordner"
|
||||
},
|
||||
"choose-proton-folder": {
|
||||
"title": "Proton-Ordner",
|
||||
"proton-folder-description": "Unter Linux benötigt BSManager Proton, um zu funktionieren. Wählen Sie den Installationsordner von Proton, um fortzufahren.",
|
||||
"proton-folder-placeholder": "Proton-Installationsordner",
|
||||
"where-is-proton-installed": "Wo ist Proton installiert?"
|
||||
},
|
||||
"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": {
|
||||
@@ -1064,10 +1097,6 @@
|
||||
"go-to-mods": "Zu den Mods",
|
||||
"not-remind": "Nicht mehr erinnern"
|
||||
},
|
||||
"prevent-for-models-breaks": {
|
||||
"title": "Gebrochene Modelle",
|
||||
"desc": "Seit dem Update der Beat Saber Engine sind alle benutzerdefinierten Modellmods defekt und müssen aktualisiert werden. Sie wurden noch nicht aktualisiert."
|
||||
},
|
||||
"export-success": {
|
||||
"title": "Export abgeschlossen 🎉"
|
||||
}
|
||||
|
||||
@@ -15,7 +15,11 @@
|
||||
"apply": "Apply",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied!",
|
||||
"confirm": "Confirm"
|
||||
"confirm": "Confirm",
|
||||
"choose-folder": "Choose folder",
|
||||
"unknown": "An unknown error occurred ¯\\_(ツ)_/¯",
|
||||
"warning": "Warning",
|
||||
"continue": "Continue"
|
||||
},
|
||||
"nav-bar": {
|
||||
"add-version": "Add a version",
|
||||
@@ -34,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"
|
||||
@@ -101,7 +106,8 @@
|
||||
"latest": "Latest",
|
||||
"description": "Description",
|
||||
"dropdown": {
|
||||
"uninstall-all": "Uninstall all"
|
||||
"uninstall-all": "Uninstall all",
|
||||
"unselect-all": "Unselect all"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -157,13 +163,15 @@
|
||||
},
|
||||
"installation-folder": {
|
||||
"title": "Installation folder",
|
||||
"description": "Change the folder that will contain all the content downloaded by BSManager.",
|
||||
"choose-folder": "Choose folder"
|
||||
"description": "Change the folder that will contain all the content downloaded by BSManager."
|
||||
},
|
||||
"proton-path": {
|
||||
"title": "Proton path",
|
||||
"description": "Change the path to the Proton binary",
|
||||
"choose-file": "Choose file"
|
||||
"proton-folder": {
|
||||
"title": "Proton folder",
|
||||
"description": "Change the folder to the Proton path. (eg. Proton - Experimental)",
|
||||
"errors": {
|
||||
"title": "Setting the Proton folder failed",
|
||||
"invalid-folder": "Invalid Proton folder path"
|
||||
}
|
||||
},
|
||||
"additional-content": {
|
||||
"title": "Additional content",
|
||||
@@ -185,6 +193,7 @@
|
||||
"zh-CN": "简体中文",
|
||||
"zh-TW": "正體中文",
|
||||
"ja-JP": "日本語",
|
||||
"ko-KR": "한국어",
|
||||
"translated": {
|
||||
"en-EN": "English, United Kingdom",
|
||||
"en-US": "English, USA",
|
||||
@@ -194,7 +203,8 @@
|
||||
"ru-RU": "Russian",
|
||||
"zh-CN": "Chinese (Simplified)",
|
||||
"zh-TW": "Chinese (Traditional)",
|
||||
"ja-JP": "Japanese"
|
||||
"ja-JP": "Japanese",
|
||||
"ko-KR": "Korean"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -222,10 +232,6 @@
|
||||
"open-logs": "Open logs"
|
||||
}
|
||||
},
|
||||
"changelogs": {
|
||||
"open" : "🚀 Check Out the Changelog!",
|
||||
"not-founds": "😕 Oops, No Changelog Here!"
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Advanced",
|
||||
"description": "Advanced settings for BSManager.",
|
||||
@@ -437,9 +443,11 @@
|
||||
"OCULUS_NOT_RUNNING": "Oculus is not running",
|
||||
"BS_ALREADY_RUNNING": "Beat Saber already running",
|
||||
"EXE_NOT_FINDED": "Missing files",
|
||||
"PROTON_NOT_SET": "Proton path not set",
|
||||
"PROTON_NOT_SET": "Proton folder not set",
|
||||
"PROTON_NOT_FOUND": "Proton binary not found",
|
||||
"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.",
|
||||
@@ -449,7 +457,9 @@
|
||||
"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.",
|
||||
"PROTON_NOT_SET": "Set the Proton path in settings"
|
||||
"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"
|
||||
@@ -570,7 +580,7 @@
|
||||
"EPERM": "BSManager does not have the necessary permissions to link the folder.",
|
||||
"EACCES": "BSManager does not have the necessary permissions to link the folder.",
|
||||
"ENOSPC": "The disk is full, make some space and try again.",
|
||||
"UNKNOWN_ERROR":"An unknown error has occurred while linking the folder."
|
||||
"UNKNOWN_ERROR": "An unknown error has occurred while linking the folder."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -582,6 +592,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": {
|
||||
@@ -789,7 +807,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",
|
||||
@@ -826,15 +844,23 @@
|
||||
"ask-install-path": {
|
||||
"title": "Installation folder",
|
||||
"choose-folder-description": "Choose the folder that will contain all the content downloaded by BSManager. (versions, mods, maps, playlists, etc.)",
|
||||
"choose-folder": "Choose folder",
|
||||
"default": "Default",
|
||||
"default-tooltip": "Defaults to your home folder"
|
||||
},
|
||||
"choose-proton-folder": {
|
||||
"title": "Proton Folder",
|
||||
"proton-folder-description": "On Linux, BSManager requires Proton to work. Choose the Proton installation folder to continue.",
|
||||
"proton-folder-placeholder": "Proton installation folder",
|
||||
"where-is-proton-installed": "Where is Proton installed?"
|
||||
},
|
||||
"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": {
|
||||
"map-filter-panel": {
|
||||
"duration": "Duration",
|
||||
"nps" : "Notes Per Second",
|
||||
"nps": "Notes Per Second",
|
||||
"njs": "Note Jump Speed",
|
||||
"tags": "tags",
|
||||
"specificities": "general",
|
||||
@@ -909,11 +935,11 @@
|
||||
"by": "By {songAutor}",
|
||||
"mapped-by": "mapped by",
|
||||
"delete": "Delete",
|
||||
"preview" : "Preview map",
|
||||
"bsr-code" : "BSR code",
|
||||
"download" : "Download map",
|
||||
"downloading" :"Downloading map",
|
||||
"cancel-download" : "Cancel download",
|
||||
"preview": "Preview map",
|
||||
"bsr-code": "BSR code",
|
||||
"download": "Download map",
|
||||
"downloading": "Downloading map",
|
||||
"cancel-download": "Cancel download",
|
||||
"hightlight-difficulty": "Highlight difficulty"
|
||||
}
|
||||
},
|
||||
@@ -1071,10 +1097,6 @@
|
||||
"go-to-mods": "Go to mods",
|
||||
"not-remind": "Don't remind me"
|
||||
},
|
||||
"prevent-for-models-breaks": {
|
||||
"title": "Broken models",
|
||||
"desc": "Since the Beat Saber engine update, all custom model mods have broken and need to be updated. They have not been updated yet."
|
||||
},
|
||||
"export-success": {
|
||||
"title": "Export completed 🎉"
|
||||
}
|
||||
@@ -1244,8 +1266,57 @@
|
||||
}
|
||||
},
|
||||
"dateformat": {
|
||||
"dayNames": ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
|
||||
"monthNames": ["Jan", "Feb", "Mar", "Apr", "May", "June", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
|
||||
"timeNames": ["a", "p", "am", "pm", "A", "P", "AM", "PM"]
|
||||
"dayNames": [
|
||||
"Sun",
|
||||
"Mon",
|
||||
"Tue",
|
||||
"Wed",
|
||||
"Thu",
|
||||
"Fri",
|
||||
"Sat",
|
||||
"Sunday",
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday"
|
||||
],
|
||||
"monthNames": [
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mar",
|
||||
"Apr",
|
||||
"May",
|
||||
"June",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sept",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Dec",
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December"
|
||||
],
|
||||
"timeNames": [
|
||||
"a",
|
||||
"p",
|
||||
"am",
|
||||
"pm",
|
||||
"A",
|
||||
"P",
|
||||
"AM",
|
||||
"PM"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,11 @@
|
||||
"apply": "Aplicar",
|
||||
"copy": "Copiar",
|
||||
"copied": "¡Copiado!",
|
||||
"confirm": "Confirmar"
|
||||
"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",
|
||||
@@ -34,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"
|
||||
@@ -101,7 +106,8 @@
|
||||
"latest": "Último",
|
||||
"description": "Descripción",
|
||||
"dropdown": {
|
||||
"uninstall-all": "Desinstalar todos"
|
||||
"uninstall-all": "Desinstalar todos",
|
||||
"unselect-all": "Deseleccionar todo"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -157,8 +163,15 @@
|
||||
},
|
||||
"installation-folder": {
|
||||
"title": "Carpeta de instalación",
|
||||
"description": "Cambiar la carpeta que contendrá todo el contenido descargado por BSManager.",
|
||||
"choose-folder": "Elige la carpeta"
|
||||
"description": "Cambiar la carpeta que contendrá todo el contenido descargado por BSManager."
|
||||
},
|
||||
"proton-folder": {
|
||||
"title": "Carpeta de Proton",
|
||||
"description": "Cambia la carpeta a la ruta de Proton. (por ejemplo, Proton - Experimental)",
|
||||
"errors": {
|
||||
"title": "Falló al configurar la carpeta de Proton",
|
||||
"invalid-folder": "Ruta de carpeta de Proton no válida"
|
||||
}
|
||||
},
|
||||
"additional-content": {
|
||||
"title": "Contenido adicional",
|
||||
@@ -180,6 +193,7 @@
|
||||
"zh-CN": "简体中文",
|
||||
"zh-TW": "正體中文",
|
||||
"ja-JP": "日本語",
|
||||
"ko-KR": "한국어",
|
||||
"translated": {
|
||||
"en-EN": "Inglés, Reino Unido",
|
||||
"en-US": "Inglés, Estados Unidos",
|
||||
@@ -189,7 +203,8 @@
|
||||
"ru-RU": "Ruso",
|
||||
"zh-CN": "Chino simplificado",
|
||||
"zh-TW": "Chino tradicional",
|
||||
"ja-JP": "Japonés"
|
||||
"ja-JP": "Japonés",
|
||||
"ko-KR": "Coreano"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -217,10 +232,6 @@
|
||||
"open-logs": "Abrir los registros"
|
||||
}
|
||||
},
|
||||
"changelogs": {
|
||||
"open" : "🚀 ¡Descubre lo nuevo en el Changelog!",
|
||||
"not-founds": "😕 Uy, ¡Changelog no encontrado!"
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Avanzados",
|
||||
"description": "Configuraciones avanzadas para BSManager.",
|
||||
@@ -432,8 +443,11 @@
|
||||
"OCULUS_NOT_RUNNING": "Oculus no funciona",
|
||||
"BS_ALREADY_RUNNING": "Beat Saber ya está en marcha",
|
||||
"EXE_NOT_FINDED": "Archivos no encontrados",
|
||||
"PROTON_NOT_SET": "Carpeta de Proton no establecida",
|
||||
"PROTON_NOT_FOUND": "Binario de Proton no encontrado",
|
||||
"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.",
|
||||
@@ -442,7 +456,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"
|
||||
@@ -575,6 +592,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": {
|
||||
@@ -782,7 +807,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",
|
||||
@@ -819,15 +844,23 @@
|
||||
"ask-install-path": {
|
||||
"title": "Carpeta de instalación",
|
||||
"choose-folder-description": "Elija la carpeta que contendrá todo el contenido descargado por BSManager. (versiones, mods, mapas, listas de reproducción, etc.)",
|
||||
"choose-folder": "Elige la carpeta",
|
||||
"default": "Predeterminado",
|
||||
"default-tooltip": "Por defecto, en su carpeta personal"
|
||||
},
|
||||
"choose-proton-folder": {
|
||||
"title": "Carpeta de Proton",
|
||||
"proton-folder-description": "En Linux, BSManager necesita Proton para funcionar. Elija la carpeta de instalación de Proton para continuar.",
|
||||
"proton-folder-placeholder": "Carpeta de instalación de Proton",
|
||||
"where-is-proton-installed": "¿Dónde está instalado Proton?"
|
||||
},
|
||||
"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": {
|
||||
"map-filter-panel": {
|
||||
"duration": "Duración",
|
||||
"nps" : "Notas Por Segundo",
|
||||
"nps": "Notas Por Segundo",
|
||||
"njs": "Velocidad de salto de nota",
|
||||
"tags": "tags",
|
||||
"specificities": "general",
|
||||
@@ -902,11 +935,11 @@
|
||||
"by": "Por {songAutor}",
|
||||
"mapped-by": "mapeado por",
|
||||
"delete": "Borrar",
|
||||
"preview" : "Vista previa del mapa",
|
||||
"bsr-code" : "Código BSR",
|
||||
"download" : "Descargar mapa",
|
||||
"downloading" :"Descargando mapa",
|
||||
"cancel-download" : "Cancelar descarga",
|
||||
"preview": "Vista previa del mapa",
|
||||
"bsr-code": "Código BSR",
|
||||
"download": "Descargar mapa",
|
||||
"downloading": "Descargando mapa",
|
||||
"cancel-download": "Cancelar descarga",
|
||||
"hightlight-difficulty": "Resaltar la dificultad"
|
||||
}
|
||||
},
|
||||
@@ -1064,10 +1097,6 @@
|
||||
"go-to-mods": "Ir a mods",
|
||||
"not-remind": "No volver a recordarme"
|
||||
},
|
||||
"prevent-for-models-breaks": {
|
||||
"title": "Modelos rotos",
|
||||
"desc": "Desde la actualización del motor de Beat Saber, todos los mods de modelos personalizados se han roto y necesitan ser actualizados. Todavía no han sido actualizados."
|
||||
},
|
||||
"export-success": {
|
||||
"title": "Exportación completada 🎉"
|
||||
}
|
||||
@@ -1237,8 +1266,57 @@
|
||||
}
|
||||
},
|
||||
"dateformat": {
|
||||
"dayNames": ["Dom", "Lun", "Mar", "Mié", "Jue", "Vie", "Sáb", "Domingo", "Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado"],
|
||||
"monthNames": ["Ene", "Feb", "Mar", "Abr", "May", "Jun", "Jul", "Ago", "Sep", "Oct", "Nov", "Dic", "Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"],
|
||||
"timeNames": ["a", "p", "am", "pm", "A", "P", "AM", "PM"]
|
||||
"dayNames": [
|
||||
"Dom",
|
||||
"Lun",
|
||||
"Mar",
|
||||
"Mié",
|
||||
"Jue",
|
||||
"Vie",
|
||||
"Sáb",
|
||||
"Domingo",
|
||||
"Lunes",
|
||||
"Martes",
|
||||
"Miércoles",
|
||||
"Jueves",
|
||||
"Viernes",
|
||||
"Sábado"
|
||||
],
|
||||
"monthNames": [
|
||||
"Ene",
|
||||
"Feb",
|
||||
"Mar",
|
||||
"Abr",
|
||||
"May",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Ago",
|
||||
"Sep",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Dic",
|
||||
"Enero",
|
||||
"Febrero",
|
||||
"Marzo",
|
||||
"Abril",
|
||||
"Mayo",
|
||||
"Junio",
|
||||
"Julio",
|
||||
"Agosto",
|
||||
"Septiembre",
|
||||
"Octubre",
|
||||
"Noviembre",
|
||||
"Diciembre"
|
||||
],
|
||||
"timeNames": [
|
||||
"a",
|
||||
"p",
|
||||
"am",
|
||||
"pm",
|
||||
"A",
|
||||
"P",
|
||||
"AM",
|
||||
"PM"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,11 @@
|
||||
"apply": "Appliquer",
|
||||
"copy": "Copier",
|
||||
"copied": "Copié !",
|
||||
"confirm": "Confirmer"
|
||||
"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",
|
||||
@@ -34,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"
|
||||
@@ -101,7 +106,8 @@
|
||||
"latest": "Récent",
|
||||
"description": "Description",
|
||||
"dropdown": {
|
||||
"uninstall-all": "Tout désinstaller"
|
||||
"uninstall-all": "Tout désinstaller",
|
||||
"unselect-all": "Tout désélectionner"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -157,8 +163,15 @@
|
||||
},
|
||||
"installation-folder": {
|
||||
"title": "Dossier d'installation",
|
||||
"description": "Changer le dossier qui contiendra tout le contenu téléchargé par BSManager.",
|
||||
"choose-folder": "Choisir un dossier"
|
||||
"description": "Changer le dossier qui contiendra tout le contenu téléchargé par BSManager."
|
||||
},
|
||||
"proton-folder": {
|
||||
"title": "Dossier Proton",
|
||||
"description": "Changez le dossier vers le chemin de Proton. (par exemple, Proton - Experimental)",
|
||||
"errors": {
|
||||
"title": "Le paramétrage du dossier Proton a échoué",
|
||||
"invalid-folder": "Chemin du dossier Proton invalide"
|
||||
}
|
||||
},
|
||||
"additional-content": {
|
||||
"title": "Contenus additionnels",
|
||||
@@ -180,6 +193,7 @@
|
||||
"zh-CN": "简体中文",
|
||||
"zh-TW": "正體中文",
|
||||
"ja-JP": "日本語",
|
||||
"ko-KR": "한국어",
|
||||
"translated": {
|
||||
"en-EN": "Anglais, Royaume-Uni",
|
||||
"en-US": "Anglais, États-Unis",
|
||||
@@ -189,7 +203,8 @@
|
||||
"ru-RU": "Russe",
|
||||
"zh-CN": "Chinois simplifié",
|
||||
"zh-TW": "Chinois traditionnel",
|
||||
"ja-JP": "Japonais"
|
||||
"ja-JP": "Japonais",
|
||||
"ko-KR": "Coréen"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -217,10 +232,6 @@
|
||||
"open-logs": "Ouvrir les logs"
|
||||
}
|
||||
},
|
||||
"changelogs": {
|
||||
"open" : "🚀 Jetez un œil au Changelog !",
|
||||
"not-founds": "😕 Oups, Changelog introuvable !"
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Avancés",
|
||||
"description": "Paramètres avancés pour BSManager.",
|
||||
@@ -432,8 +443,11 @@
|
||||
"OCULUS_NOT_RUNNING": "Oculus n'est pas lancé",
|
||||
"BS_ALREADY_RUNNING": "Beat Saber est déjà lancé",
|
||||
"EXE_NOT_FINDED": "Fichiers manquants",
|
||||
"PROTON_NOT_SET": "Dossier Proton non défini",
|
||||
"PROTON_NOT_FOUND": "Binaire Proton non trouvé",
|
||||
"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.",
|
||||
@@ -442,7 +456,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"
|
||||
@@ -576,6 +593,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": {
|
||||
@@ -764,7 +789,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"
|
||||
@@ -783,7 +808,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",
|
||||
@@ -820,9 +845,17 @@
|
||||
"ask-install-path": {
|
||||
"title": "Dossier d'installation",
|
||||
"choose-folder-description": "Choisissez le dossier qui contiendra tout le contenu téléchargé par BSManager. (versions, mods, cartes, playlists, etc.)",
|
||||
"choose-folder": "Choisir un dossier",
|
||||
"default": "Par défaut",
|
||||
"default-tooltip": "Par défaut, dans votre dossier personnel"
|
||||
},
|
||||
"choose-proton-folder": {
|
||||
"title": "Dossier Proton",
|
||||
"proton-folder-description": "Sous Linux, BSManager a besoin de Proton pour fonctionner. Choisissez le dossier d'installation de Proton pour continuer.",
|
||||
"proton-folder-placeholder": "Dossier d'installation de Proton",
|
||||
"where-is-proton-installed": "Où est installé Proton ?"
|
||||
},
|
||||
"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": {
|
||||
@@ -1065,10 +1098,6 @@
|
||||
"go-to-mods": "Aller aux mods",
|
||||
"not-remind": "Ne plus me rappeler"
|
||||
},
|
||||
"prevent-for-models-breaks": {
|
||||
"title": "Modèles cassés",
|
||||
"desc": "Depuis la mise à jour du moteur Beat Saber, tous les mods de modèles personnalisés ne sont plus fonctionnels et doivent être mis à jour. Ils n'ont pas encore été mis à jour."
|
||||
},
|
||||
"export-success": {
|
||||
"title": "Export terminé 🎉"
|
||||
}
|
||||
|
||||
@@ -15,7 +15,11 @@
|
||||
"apply": "適用",
|
||||
"copy": "コピー",
|
||||
"copied": "コピー済み!",
|
||||
"confirm": "確認"
|
||||
"confirm": "確認",
|
||||
"choose-folder": "フォルダーを選択",
|
||||
"unknown": "不明なエラーが発生しました (´・ω・`)",
|
||||
"warning": "警告",
|
||||
"continue": "続ける"
|
||||
},
|
||||
"nav-bar": {
|
||||
"add-version": "バージョンを追加",
|
||||
@@ -34,6 +38,7 @@
|
||||
"desktop-description": "ファーストパーソン・フライング・コントローラー・モードでは、WASDとマウスを使ってゲーム内のメニューを操作できます。ヘッドセットを装着する必要がないので、テストがより簡単になります!",
|
||||
"debug": "デバックモード",
|
||||
"debug-description": "IPAの出力ログウィンドウを有効にします。これはMODのデバッグコンソールを表示します。",
|
||||
"outdated-tippy": "このバージョンは古いため、一部のMODや機能が期待通りに動作しない可能性があります。最新の機能やバグ修正を楽しむには、推奨バージョン ({recommendedVersion}) のBeat Saberを使用することをお勧めします。",
|
||||
"advanced-launch": {
|
||||
"button": "高度な起動",
|
||||
"placeholder": "追加引数 例:--revert; --nowait"
|
||||
@@ -101,7 +106,8 @@
|
||||
"latest": "最新",
|
||||
"description": "說明",
|
||||
"dropdown": {
|
||||
"uninstall-all": "全てアンインストールする"
|
||||
"uninstall-all": "全てアンインストールする",
|
||||
"unselect-all": "すべて選択解除"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -157,8 +163,15 @@
|
||||
},
|
||||
"installation-folder": {
|
||||
"title": "インストールフォルダー",
|
||||
"description": "BSManager によってダウンロードされたすべてのコンテンツを含むフォルダを変更します。",
|
||||
"choose-folder": "フォルダーを選択"
|
||||
"description": "BSManager によってダウンロードされたすべてのコンテンツを含むフォルダを変更します。"
|
||||
},
|
||||
"proton-folder": {
|
||||
"title": "Protonフォルダー",
|
||||
"description": "フォルダーをProtonのパスに変更してください。(例:Proton - Experimental)",
|
||||
"errors": {
|
||||
"title": "Protonフォルダーの設定に失敗しました",
|
||||
"invalid-folder": "無効なProtonフォルダーのパスです"
|
||||
}
|
||||
},
|
||||
"additional-content": {
|
||||
"title": "追加コンテンツ",
|
||||
@@ -180,6 +193,7 @@
|
||||
"zh-CN": "简体中文",
|
||||
"zh-TW": "正體中文",
|
||||
"ja-JP": "日本語",
|
||||
"ko-KR": "한국어",
|
||||
"translated": {
|
||||
"en-EN": "イギリス英語",
|
||||
"en-US": "アメリカ英語",
|
||||
@@ -189,7 +203,8 @@
|
||||
"ru-RU": "ロシア語",
|
||||
"zh-CN": "簡体字中国",
|
||||
"zh-TW": "繁体字中国",
|
||||
"ja-JP": "日本語"
|
||||
"ja-JP": "日本語",
|
||||
"ko-KR": "韓国語"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -217,10 +232,6 @@
|
||||
"open-logs": "ログを表示"
|
||||
}
|
||||
},
|
||||
"changelogs": {
|
||||
"open" : "🚀 チェンジログを覗いてみて!",
|
||||
"not-founds": "😕 あれ、チェンジログが見当たらない!"
|
||||
},
|
||||
"advanced": {
|
||||
"title": "高度な設定",
|
||||
"description": "BSManagerの高度な設定。",
|
||||
@@ -432,8 +443,11 @@
|
||||
"OCULUS_NOT_RUNNING": "Oculusが起動していません",
|
||||
"BS_ALREADY_RUNNING": "Beat Saberはすでに起動しています",
|
||||
"EXE_NOT_FINDED": "ファイルが見つかりません",
|
||||
"PROTON_NOT_SET": "Protonフォルダーが設定されていません",
|
||||
"PROTON_NOT_FOUND": "Protonのバイナリが見つかりません",
|
||||
"EXIT": "緊急停止",
|
||||
"OCULUS_LIB_NOT_FOUND": "Oculusライブラリが見つかりません"
|
||||
"OCULUS_LIB_NOT_FOUND": "Oculusライブラリが見つかりません",
|
||||
"ORIGINAL_OCULUS_NOT_INSTALLED": "オリジナルのOculusバージョンが見つかりませんでした"
|
||||
},
|
||||
"msg": {
|
||||
"UNKNOWN_ERROR": "不明なエラーが発生しました。",
|
||||
@@ -442,7 +456,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を起動"
|
||||
@@ -575,6 +592,14 @@
|
||||
"error": {
|
||||
"msg": "ショートカットの作成中にエラーが発生しました。"
|
||||
}
|
||||
},
|
||||
"bs-version-oudated": {
|
||||
"title": "バージョンが古い",
|
||||
"msg": "このBeat Saberのバージョンは古いため、推奨バージョンを使用して最新の機能やバグ修正をお楽しみください。",
|
||||
"actions": {
|
||||
"do-not-remind": "再度通知しない",
|
||||
"ok": "OK"
|
||||
}
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
@@ -782,7 +807,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": "二度と表示しないでください",
|
||||
@@ -819,9 +844,17 @@
|
||||
"ask-install-path": {
|
||||
"title": "インストールフォルダー",
|
||||
"choose-folder-description": "BSManager によってダウンロードされたすべてのコンテンツ (バージョン、MOD、マップ、プレイリストなど) を含むフォルダを選択してください。",
|
||||
"choose-folder": "フォルダーを選択",
|
||||
"default": "デフォルト",
|
||||
"default-tooltip": "デフォルトでは、ホームフォルダに設定されます"
|
||||
},
|
||||
"choose-proton-folder": {
|
||||
"title": "Protonフォルダ",
|
||||
"proton-folder-description": "Linuxでは、BSManagerを動作させるためにProtonが必要です。続行するには、Protonのインストールフォルダを選択してください。",
|
||||
"proton-folder-placeholder": "Protonのインストールフォルダ",
|
||||
"where-is-proton-installed": "Protonはどこにインストールされていますか?"
|
||||
},
|
||||
"bs-version-outdated": {
|
||||
"body": "バージョン {outdatedVersion} は古いため、一部のモッドや機能が期待通りに動作しない場合があります。最新の機能やバグ修正を楽しむために、推奨される最新バージョンの Beat Saber ({recommendedVersion}) をダウンロードしてください。"
|
||||
}
|
||||
},
|
||||
"maps": {
|
||||
@@ -1064,10 +1097,6 @@
|
||||
"go-to-mods": "Modインストーラーに移動",
|
||||
"not-remind": "二度と表示しないで"
|
||||
},
|
||||
"prevent-for-models-breaks": {
|
||||
"title": "モデルが壊れています!",
|
||||
"desc": "Beat Saberのエンジンアップデート以来、すべてのカスタムモデルのMODが壊れており、更新が必要です。まだ更新されていません。"
|
||||
},
|
||||
"export-success": {
|
||||
"title": "エクスポート完了🎉"
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,11 @@
|
||||
"apply": "Применить",
|
||||
"copy": "Скопировать",
|
||||
"copied": "Скопировано!",
|
||||
"confirm": "Подтвердить"
|
||||
"confirm": "Подтвердить",
|
||||
"choose-folder": "Изменить папку",
|
||||
"unknown": "Неизвестная ошибка ¯\\_(ツ)_/¯",
|
||||
"warning": "Внимание",
|
||||
"continue": "Продолжить"
|
||||
},
|
||||
"nav-bar": {
|
||||
"add-version": "Добавить версию игры",
|
||||
@@ -34,6 +38,7 @@
|
||||
"desktop-description": "Режим Летающих контроллеров от первого лица(FPFC) позволит вам использовать меню с помощью WASD и мышки. Это облегчает тестирование модов, потому что вам не нужно подключать шлем!",
|
||||
"debug": "Режим отладки",
|
||||
"debug-description": "Включает окно логов для IPA. В окне будут отображаться логи модов.",
|
||||
"outdated-tippy": "Эта версия устарела, и некоторые моды или функции могут работать не так, как ожидалось. Рекомендуется использовать рекомендуемую версию ({recommendedVersion}) Beat Saber, чтобы воспользоваться последними функциями и исправлениями ошибок.",
|
||||
"advanced-launch": {
|
||||
"button": "Расширенный запуск",
|
||||
"placeholder": "Параметры запуска, например: --revert; --nowait"
|
||||
@@ -101,7 +106,8 @@
|
||||
"latest": "Latest",
|
||||
"description": "Описание",
|
||||
"dropdown": {
|
||||
"uninstall-all": "Удалить всё"
|
||||
"uninstall-all": "Удалить всё",
|
||||
"unselect-all": "Снять все выделения"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -157,8 +163,15 @@
|
||||
},
|
||||
"installation-folder": {
|
||||
"title": "Папка установок",
|
||||
"description": "Изменить папку, которая будет содержать весь контент, загруженный BSManager.",
|
||||
"choose-folder": "Изменить папку"
|
||||
"description": "Изменить папку, которая будет содержать весь контент, загруженный BSManager."
|
||||
},
|
||||
"proton-folder": {
|
||||
"title": "Папка Proton",
|
||||
"description": "Измените папку на путь к Proton. (например, Proton - Experimental)",
|
||||
"errors": {
|
||||
"title": "Не удалось установить папку Proton",
|
||||
"invalid-folder": "Недействительный путь к папке Proton"
|
||||
}
|
||||
},
|
||||
"additional-content": {
|
||||
"title": "Дополнительный контент",
|
||||
@@ -180,6 +193,7 @@
|
||||
"zh-CN": "简体中文",
|
||||
"zh-TW": "正體中文",
|
||||
"ja-JP": "日本語",
|
||||
"ko-KR": "한국어",
|
||||
"translated": {
|
||||
"en-EN": "Английский, Великобритания",
|
||||
"en-US": "Английский, США",
|
||||
@@ -189,7 +203,8 @@
|
||||
"ru-RU": "Русский",
|
||||
"zh-CN": "Китайский (упрощенный)",
|
||||
"zh-TW": "Китайский (традиционный)",
|
||||
"ja-JP": "Японский"
|
||||
"ja-JP": "Японский",
|
||||
"ko-KR": "Корейский"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -217,10 +232,6 @@
|
||||
"open-logs": "Открыть логи"
|
||||
}
|
||||
},
|
||||
"changelogs": {
|
||||
"open" : "🚀 Изучаем Changelog!",
|
||||
"not-founds": "😕 Ой, Changelog нигде не видно!"
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Дополнительные",
|
||||
"description": "Дополнительные настройки для BSManager.",
|
||||
@@ -432,8 +443,11 @@
|
||||
"OCULUS_NOT_RUNNING": "Oculus не запущен",
|
||||
"BS_ALREADY_RUNNING": "Beat Saber уде запущен",
|
||||
"EXE_NOT_FINDED": "Отсутствуют файлы",
|
||||
"PROTON_NOT_SET": "Папка Proton не установлена",
|
||||
"PROTON_NOT_FOUND": "Бинарный файл Proton не найде",
|
||||
"EXIT": "Вылет",
|
||||
"OCULUS_LIB_NOT_FOUND": "Библиотека Oculus не найдена"
|
||||
"OCULUS_LIB_NOT_FOUND": "Библиотека Oculus не найдена",
|
||||
"ORIGINAL_OCULUS_NOT_INSTALLED": "Оригинальная версия Oculus не найдена"
|
||||
},
|
||||
"msg": {
|
||||
"UNKNOWN_ERROR": "Возникла неизвестная ошибка.",
|
||||
@@ -442,7 +456,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"
|
||||
@@ -575,6 +592,14 @@
|
||||
"error": {
|
||||
"msg": "Ошибка создания ярлыка."
|
||||
}
|
||||
},
|
||||
"bs-version-oudated": {
|
||||
"title": "Устаревшая версия",
|
||||
"msg": "Эта версия Beat Saber устарела. Используйте рекомендуемую версию, чтобы воспользоваться последними функциями и исправлениями ошибок.",
|
||||
"actions": {
|
||||
"do-not-remind": "Больше не напоминать",
|
||||
"ok": "Ок"
|
||||
}
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
@@ -782,7 +807,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": "Больше не напоминать",
|
||||
@@ -818,9 +843,17 @@
|
||||
"ask-install-path": {
|
||||
"title": "Папка установок",
|
||||
"choose-folder-description": "Выберите папку, которая будет содержать весь контент, загруженный BSManager. (версии, моды, карты, плейлисты и т.д.)",
|
||||
"choose-folder": "Изменить папку",
|
||||
"default": "По умолчанию",
|
||||
"default-tooltip": "По умолчанию в вашей домашней папке"
|
||||
},
|
||||
"choose-proton-folder": {
|
||||
"title": "Папка Proton",
|
||||
"proton-folder-description": "В Linux BSManager требует Proton для работы. Выберите папку установки Proton, чтобы продолжить.",
|
||||
"proton-folder-placeholder": "Папка установки Proton",
|
||||
"where-is-proton-installed": "Где установлен Proton?"
|
||||
},
|
||||
"bs-version-outdated": {
|
||||
"body": "Версия {outdatedVersion} устарела, и некоторые моды или функции могут работать некорректно. Пожалуйста, скачайте последнюю рекомендуемую версию ({recommendedVersion}) Beat Saber, чтобы воспользоваться новейшими функциями и исправлениями ошибок."
|
||||
}
|
||||
},
|
||||
"maps": {
|
||||
@@ -1063,10 +1096,6 @@
|
||||
"go-to-mods": "Перейти к модам",
|
||||
"not-remind": "Больше не напоминать"
|
||||
},
|
||||
"prevent-for-models-breaks": {
|
||||
"title": "Сломанные модели",
|
||||
"desc": "С момента обновления движка Beat Saber, все пользовательские модификации моделей сломались и требуют обновления. Они еще не были обновлены."
|
||||
},
|
||||
"export-success": {
|
||||
"title": "Экспорт завершен 🎉"
|
||||
}
|
||||
|
||||
@@ -15,7 +15,11 @@
|
||||
"apply": "應用",
|
||||
"copy": "複製",
|
||||
"copied": "已複製!",
|
||||
"confirm": "確認"
|
||||
"confirm": "確認",
|
||||
"choose-folder": "選擇文件夾",
|
||||
"unknown": "發生了一個錯誤 ¯\\_(ツ)_/¯",
|
||||
"warning": "警告",
|
||||
"continue": "繼續"
|
||||
},
|
||||
"nav-bar": {
|
||||
"add-version": "新增版本",
|
||||
@@ -34,6 +38,7 @@
|
||||
"desktop-description": "第一人稱飛控模式允許你使用鍵盤 WASD 和滑鼠在遊戲中進行導航。你無需帶上頭顯即可進行測試!",
|
||||
"debug": "Debug 模式",
|
||||
"debug-description": "為 IPA 啟用輸出日誌窗口。 會顯示所用 Mod 的 debug 輸出",
|
||||
"outdated-tippy": "此版本已過時,某些模組或功能可能無法按預期運作。建議使用推薦版本的 Beat Saber ({recommendedVersion}),以享受最新功能和修復。",
|
||||
"advanced-launch": {
|
||||
"button": "高級啟動",
|
||||
"placeholder": "額外啟動參數,例如: --revert; --nowait"
|
||||
@@ -101,7 +106,8 @@
|
||||
"latest": "最新",
|
||||
"description": "描述",
|
||||
"dropdown": {
|
||||
"uninstall-all": "全部移除"
|
||||
"uninstall-all": "全部移除",
|
||||
"unselect-all": "取消全選"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -157,8 +163,15 @@
|
||||
},
|
||||
"installation-folder": {
|
||||
"title": "安裝文件夾",
|
||||
"description": "更改將包含 BSManager 下載的所有內容的文件夾。",
|
||||
"choose-folder": "選擇文件夾"
|
||||
"description": "更改將包含 BSManager 下載的所有內容的文件夾。"
|
||||
},
|
||||
"proton-folder": {
|
||||
"title": "Proton資料夾",
|
||||
"description": "將資料夾更改為Proton路徑。(例如:Proton - Experimental)",
|
||||
"errors": {
|
||||
"title": "設置Proton資料夾失敗",
|
||||
"invalid-folder": "無效的Proton資料夾路徑"
|
||||
}
|
||||
},
|
||||
"additional-content": {
|
||||
"title": "附加內容",
|
||||
@@ -180,6 +193,7 @@
|
||||
"zh-CN": "簡體中文",
|
||||
"zh-TW": "正體中文",
|
||||
"ja-JP": "日本語",
|
||||
"ko-KR": "한국어",
|
||||
"translated": {
|
||||
"en-EN": "英式英語",
|
||||
"en-US": "美式英語",
|
||||
@@ -189,7 +203,8 @@
|
||||
"ru-RU": "俄語",
|
||||
"zh-CN": "簡體中文",
|
||||
"zh-TW": "正體中文",
|
||||
"ja-JP": "日語"
|
||||
"ja-JP": "日語",
|
||||
"ko-KR": "韓語"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -217,10 +232,6 @@
|
||||
"open-logs": "打開日誌"
|
||||
}
|
||||
},
|
||||
"changelogs": {
|
||||
"open" : "🚀 探索更新日誌!",
|
||||
"not-founds": "😕 噢,找不到更新日誌!"
|
||||
},
|
||||
"advanced": {
|
||||
"title": "高级设置",
|
||||
"description": "BSManager的高级设置。",
|
||||
@@ -432,8 +443,11 @@
|
||||
"OCULUS_NOT_RUNNING": "Oculus 未運行",
|
||||
"BS_ALREADY_RUNNING": "Beat Saber 已在運行",
|
||||
"EXE_NOT_FINDED": "缺少文件",
|
||||
"PROTON_NOT_SET": "Proton資料夾未設置",
|
||||
"PROTON_NOT_FOUND": "找不到Proton二進位檔",
|
||||
"EXIT": "突然停止",
|
||||
"OCULUS_LIB_NOT_FOUND": "未找到 Oculus 庫"
|
||||
"OCULUS_LIB_NOT_FOUND": "未找到 Oculus 庫",
|
||||
"ORIGINAL_OCULUS_NOT_INSTALLED": "找不到原始Oculus版本"
|
||||
},
|
||||
"msg": {
|
||||
"UNKNOWN_ERROR": "發生未知錯誤。",
|
||||
@@ -442,7 +456,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"
|
||||
@@ -575,6 +592,14 @@
|
||||
"error": {
|
||||
"msg": "創建捷徑時發生錯誤"
|
||||
}
|
||||
},
|
||||
"bs-version-oudated": {
|
||||
"title": "版本過時",
|
||||
"msg": "此 Beat Saber 版本已過時,請使用推薦版本以享受最新功能和修復。",
|
||||
"actions": {
|
||||
"do-not-remind": "不要再提醒我",
|
||||
"ok": "確定"
|
||||
}
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
@@ -782,7 +807,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": "不再提醒我",
|
||||
@@ -819,9 +844,17 @@
|
||||
"ask-install-path": {
|
||||
"title": "安裝文件夾",
|
||||
"choose-folder-description": "選擇將包含 BSManager 下載的所有內容的文件夾。(版本、mod、地圖、播放列表等)",
|
||||
"choose-folder": "選擇文件夾",
|
||||
"default": "預設",
|
||||
"default-tooltip": "預設為您的主資料夾"
|
||||
},
|
||||
"choose-proton-folder": {
|
||||
"title": "Proton資料夾",
|
||||
"proton-folder-description": "在Linux上,BSManager需要Proton才能運行。請選擇Proton的安裝資料夾以繼續。",
|
||||
"proton-folder-placeholder": "Proton安裝資料夾",
|
||||
"where-is-proton-installed": "Proton安裝在哪裡?"
|
||||
},
|
||||
"bs-version-outdated": {
|
||||
"body": "版本 {outdatedVersion} 已過時,某些模組或功能可能無法正常運作。請下載最新推薦的 Beat Saber 版本 ({recommendedVersion}),以享受最新功能和修正。"
|
||||
}
|
||||
},
|
||||
"maps": {
|
||||
@@ -1064,10 +1097,6 @@
|
||||
"go-to-mods": "前往 Mod 頁",
|
||||
"not-remind": "不再提醒我"
|
||||
},
|
||||
"prevent-for-models-breaks": {
|
||||
"title": "已損壞的模型",
|
||||
"desc": "自從Beat Saber引擎更新以來,所有自定義模型的MOD都已損壞,需要更新。它們還沒有被更新。"
|
||||
},
|
||||
"export-success": {
|
||||
"title": "導出成功 🎉"
|
||||
}
|
||||
|
||||
@@ -15,7 +15,11 @@
|
||||
"apply": "应用",
|
||||
"copy": "复制",
|
||||
"copied": "已复制!",
|
||||
"confirm": "确认"
|
||||
"confirm": "确认",
|
||||
"choose-folder": "选择文件夹",
|
||||
"Unknown": "发生了一个错误 ¯\\_(ツ)_/¯",
|
||||
"warning": "警告",
|
||||
"continue": "继续"
|
||||
},
|
||||
"nav-bar": {
|
||||
"add-version": "添加版本",
|
||||
@@ -34,6 +38,7 @@
|
||||
"desktop-description": "第一人称飞控模式允许你使用键盘 WASD 和鼠标在游戏中进行导航。你无需带上头显即可进行测试!",
|
||||
"debug": "Debug 模式",
|
||||
"debug-description": "为 IPA 启用输出日志窗口。 会显示所用 Mod 的 debug 输出",
|
||||
"outdated-tippy": "此版本已过时,某些模组或功能可能无法按预期运行。建议使用推荐版本的 Beat Saber ({recommendedVersion}),以享受最新功能和修复。",
|
||||
"advanced-launch": {
|
||||
"button": "高级启动",
|
||||
"placeholder": "额外启动参数,例如: --revert; --nowait"
|
||||
@@ -101,7 +106,8 @@
|
||||
"latest": "最新",
|
||||
"description": "描述",
|
||||
"dropdown": {
|
||||
"uninstall-all": "全部卸载"
|
||||
"uninstall-all": "全部卸载",
|
||||
"unselect-all": "取消全选"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -157,8 +163,15 @@
|
||||
},
|
||||
"installation-folder": {
|
||||
"title": "安装文件夹",
|
||||
"description": "更改将包含 BSManager 下载的所有内容的文件夹。",
|
||||
"choose-folder": "选择文件夹"
|
||||
"description": "更改将包含 BSManager 下载的所有内容的文件夹。"
|
||||
},
|
||||
"proton-folder": {
|
||||
"title": "Proton文件夹",
|
||||
"description": "将文件夹更改为Proton路径。(例如:Proton - Experimental)",
|
||||
"errors": {
|
||||
"title": "设置Proton文件夹失败",
|
||||
"invalid-folder": "无效的Proton文件夹路径"
|
||||
}
|
||||
},
|
||||
"additional-content": {
|
||||
"title": "附加内容",
|
||||
@@ -180,6 +193,7 @@
|
||||
"zh-CN": "简体中文",
|
||||
"zh-TW": "正體中文",
|
||||
"ja-JP": "日本語",
|
||||
"ko-KR": "한국어",
|
||||
"translated": {
|
||||
"en-EN": "英式英语",
|
||||
"en-US": "美式英语",
|
||||
@@ -189,7 +203,8 @@
|
||||
"ru-RU": "俄语",
|
||||
"zh-CN": "简体中文",
|
||||
"zh-TW": "正体中文",
|
||||
"ja-JP": "日语"
|
||||
"ja-JP": "日语",
|
||||
"ko-KR": "韩语"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -217,10 +232,6 @@
|
||||
"open-logs": "打开日志"
|
||||
}
|
||||
},
|
||||
"changelogs": {
|
||||
"open" : "🚀 探索更新日志!",
|
||||
"not-founds": "😕 哎呀,找不到更新日志!"
|
||||
},
|
||||
"advanced": {
|
||||
"title": "高级设置",
|
||||
"description": "BSManager的高级设置。",
|
||||
@@ -432,8 +443,11 @@
|
||||
"OCULUS_NOT_RUNNING": "Oculus 未运行",
|
||||
"BS_ALREADY_RUNNING": "Beat Saber 已在运行",
|
||||
"EXE_NOT_FINDED": "缺少文件",
|
||||
"PROTON_NOT_SET": "Proton文件夹未设置",
|
||||
"PROTON_NOT_FOUND": "未找到Proton二进制文件",
|
||||
"EXIT": "突然停止",
|
||||
"OCULUS_LIB_NOT_FOUND": "未找到 Oculus 库"
|
||||
"OCULUS_LIB_NOT_FOUND": "未找到 Oculus 库",
|
||||
"ORIGINAL_OCULUS_NOT_INSTALLED": "未找到原始Oculus版本"
|
||||
},
|
||||
"msg": {
|
||||
"UNKNOWN_ERROR": "发生未知错误。",
|
||||
@@ -442,7 +456,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"
|
||||
@@ -575,6 +592,14 @@
|
||||
"error": {
|
||||
"msg": "创建快捷方式时发生错误"
|
||||
}
|
||||
},
|
||||
"bs-version-oudated": {
|
||||
"title": "版本过时",
|
||||
"msg": "此 Beat Saber 版本已过时,请使用推荐版本以享受最新功能和修复。",
|
||||
"actions": {
|
||||
"do-not-remind": "不再提醒",
|
||||
"ok": "确定"
|
||||
}
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
@@ -782,7 +807,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": "不再提醒我",
|
||||
@@ -819,9 +844,17 @@
|
||||
"ask-install-path": {
|
||||
"title": "安装文件夹",
|
||||
"choose-folder-description": "选择将包含 BSManager 下载的所有内容的文件夹。(版本、mod、地图、播放列表等)",
|
||||
"choose-folder": "选择文件夹",
|
||||
"default": "默认",
|
||||
"default-tooltip": "默认为您的主文件夹"
|
||||
},
|
||||
"choose-proton-folder": {
|
||||
"title": "Proton文件夹",
|
||||
"proton-folder-description": "在Linux上,BSManager需要Proton才能运行。请选择Proton的安装文件夹以继续。",
|
||||
"proton-folder-placeholder": "Proton安装文件夹",
|
||||
"where-is-proton-installed": "Proton安装在哪里?"
|
||||
},
|
||||
"bs-version-outdated": {
|
||||
"body": "版本 {outdatedVersion} 已过时,某些模组或功能可能无法正常工作。请下载最新推荐的 Beat Saber 版本 ({recommendedVersion}),以享受最新功能和修复。"
|
||||
}
|
||||
},
|
||||
"maps": {
|
||||
@@ -1064,10 +1097,6 @@
|
||||
"go-to-mods": "前往 Mod 页",
|
||||
"not-remind": "不再提醒我"
|
||||
},
|
||||
"prevent-for-models-breaks": {
|
||||
"title": "已损坏的模型",
|
||||
"desc": "自从Beat Saber引擎更新以来,所有自定义模型的MOD都已损坏,需要更新。它们还没有被更新。"
|
||||
},
|
||||
"export-success": {
|
||||
"title": "导出成功 🎉"
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
Generated
+94
-74
@@ -20,7 +20,6 @@
|
||||
"color": "^4.2.3",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dateformat": "^5.0.3",
|
||||
"dompurify": "^3.0.9",
|
||||
"dot-prop": "^8.0.2",
|
||||
"electron-debug": "^3.2.0",
|
||||
"electron-log": "^4.4.8",
|
||||
@@ -30,14 +29,12 @@
|
||||
"format-duration": "^3.0.2",
|
||||
"framer-motion": "^11.2.6",
|
||||
"fs-extra": "^11.2.0",
|
||||
"got": "^14.4.2",
|
||||
"got": "^14.4.4",
|
||||
"history": "^5.3.0",
|
||||
"is-elevated": "^4.0.0",
|
||||
"jszip": "^3.10.1",
|
||||
"md5-file": "^5.0.0",
|
||||
"node-abi": "^3.65.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
"node-stream-zip": "^1.15.0",
|
||||
"pako": "^2.1.0",
|
||||
"protobufjs": "^7.4.0",
|
||||
"qrcode.react": "^4.0.1",
|
||||
@@ -62,7 +59,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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron/fuses": "^1.7.0",
|
||||
@@ -76,10 +74,9 @@
|
||||
"@types/color": "^3.0.3",
|
||||
"@types/crypto-js": "^4.2.1",
|
||||
"@types/dateformat": "^5.0.0",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/got": "^9.6.12",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/node": "20.11.15",
|
||||
"@types/node": "22.8.6",
|
||||
"@types/node-fetch": "^2.6.3",
|
||||
"@types/pako": "^2.0.1",
|
||||
"@types/react": "^18.0.33",
|
||||
@@ -5715,7 +5712,8 @@
|
||||
"node_modules/@sec-ant/readable-stream": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
|
||||
"integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="
|
||||
"integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@sinclair/typebox": {
|
||||
"version": "0.27.8",
|
||||
@@ -5723,9 +5721,10 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@sindresorhus/is": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.0.0.tgz",
|
||||
"integrity": "sha512-WDTlVTyvFivSOuyvMeedzg2hdoBLZ3f1uNVuEida2Rl9BrfjrIRjWA/VZIrMRLvSwJYCAlCRA3usDt1THytxWQ==",
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.0.1.tgz",
|
||||
"integrity": "sha512-QWLl2P+rsCJeofkDNIT3WFmb6NrRud1SUYW8dIhXK/46XFV8Q/g7Bsvib0Askb0reRLe+WYPeeE+l5cH7SlkuQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -5761,6 +5760,7 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz",
|
||||
"integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"defer-to-connect": "^2.0.1"
|
||||
},
|
||||
@@ -6105,15 +6105,6 @@
|
||||
"@types/ms": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/dompurify": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
|
||||
"integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/trusted-types": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/eslint": {
|
||||
"version": "8.4.10",
|
||||
"dev": true,
|
||||
@@ -6330,10 +6321,12 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.11.15",
|
||||
"version": "22.8.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.6.tgz",
|
||||
"integrity": "sha512-tosuJYKrIqjQIlVCM4PEGxOmyg3FCPa/fViuJChnGeEIhjA46oy8FMVoF9su1/v8PNs2a8Q0iFNyOx0uOF91nw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
"undici-types": "~6.19.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node-fetch": {
|
||||
@@ -6537,12 +6530,6 @@
|
||||
"integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/use-double-click": {
|
||||
"version": "1.0.4",
|
||||
"dev": true,
|
||||
@@ -8567,7 +8554,6 @@
|
||||
},
|
||||
"node_modules/buffer-crc32": {
|
||||
"version": "0.2.13",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
@@ -8783,6 +8769,7 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz",
|
||||
"integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
}
|
||||
@@ -8791,6 +8778,7 @@
|
||||
"version": "12.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-12.0.1.tgz",
|
||||
"integrity": "sha512-Yo9wGIQUaAfIbk+qY0X4cDQgCosecfBe3V9NSyeY4qPC2SAkbCS4Xj79VP8WOzitpJUZKc/wsRCYF5ariDIwkg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/http-cache-semantics": "^4.0.4",
|
||||
"get-stream": "^9.0.1",
|
||||
@@ -8808,6 +8796,7 @@
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz",
|
||||
"integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sec-ant/readable-stream": "^0.4.1",
|
||||
"is-stream": "^4.0.1"
|
||||
@@ -8823,6 +8812,7 @@
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz",
|
||||
"integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -8830,6 +8820,18 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/cacheable-request/node_modules/mimic-response": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz",
|
||||
"integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind": {
|
||||
"version": "1.0.5",
|
||||
"dev": true,
|
||||
@@ -9148,15 +9150,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/clone-response/node_modules/mimic-response": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
|
||||
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
@@ -10613,11 +10606,6 @@
|
||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.0.9",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.9.tgz",
|
||||
"integrity": "sha512-uyb4NDIvQ3hRn6NiC+SIFaP4mJ/MdXlvtunaqK9Bn6dD3RuB/1S/gasEjDHD8eiaqdSael2vBv+hOs7Y+jhYOQ=="
|
||||
},
|
||||
"node_modules/domutils": {
|
||||
"version": "2.8.0",
|
||||
"dev": true,
|
||||
@@ -11090,6 +11078,16 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/electron/node_modules/@types/node": {
|
||||
"version": "20.17.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.5.tgz",
|
||||
"integrity": "sha512-n8FYY/pRxu496441gIcAQFZPKXbhsd6VZygcq+PTSZ75eMh/Ke0hCAROdUa21qiFqKNsPPYic46yXDO1JGiPBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
},
|
||||
"node_modules/electronmon": {
|
||||
"version": "2.0.2",
|
||||
"dev": true,
|
||||
@@ -12585,6 +12583,17 @@
|
||||
"@types/yauzl": "^2.9.1"
|
||||
}
|
||||
},
|
||||
"node_modules/extract-zip/node_modules/yauzl": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
|
||||
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-crc32": "~0.2.3",
|
||||
"fd-slicer": "~1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"license": "MIT"
|
||||
@@ -12662,6 +12671,8 @@
|
||||
},
|
||||
"node_modules/fd-slicer": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
|
||||
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -12928,6 +12939,7 @@
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.0.2.tgz",
|
||||
"integrity": "sha512-KQVhvhK8ZkWzxKxOr56CPulAhH3dobtuQ4+hNQ+HekH/Wp5gSOafqRAeTphQUJAIk0GBvHZgJ2ZGRWd5kphMuw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
@@ -13350,11 +13362,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/got": {
|
||||
"version": "14.4.2",
|
||||
"resolved": "https://registry.npmjs.org/got/-/got-14.4.2.tgz",
|
||||
"integrity": "sha512-+Te/qEZ6hr7i+f0FNgXx/6WQteSM/QqueGvxeYQQFm0GDfoxLVJ/oiwUKYMTeioColWUTdewZ06hmrBjw6F7tw==",
|
||||
"version": "14.4.4",
|
||||
"resolved": "https://registry.npmjs.org/got/-/got-14.4.4.tgz",
|
||||
"integrity": "sha512-tqiF7eSgTBwQkxb1LxsEpva8TaMYVisbhplrFVmw9GQE3855Z+MH/mnsXLLOkDxR6hZJRFMj5VTAZ8lmTF8ZOA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sindresorhus/is": "^7.0.0",
|
||||
"@sindresorhus/is": "^7.0.1",
|
||||
"@szmarczak/http-timer": "^5.0.1",
|
||||
"cacheable-lookup": "^7.0.0",
|
||||
"cacheable-request": "^12.0.1",
|
||||
@@ -13364,7 +13377,7 @@
|
||||
"lowercase-keys": "^3.0.0",
|
||||
"p-cancelable": "^4.0.1",
|
||||
"responselike": "^3.0.0",
|
||||
"type-fest": "^4.19.0"
|
||||
"type-fest": "^4.26.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
@@ -13374,9 +13387,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/got/node_modules/type-fest": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.21.0.tgz",
|
||||
"integrity": "sha512-ADn2w7hVPcK6w1I0uWnM//y1rLXZhzB9mr0a3OirzclKF1Wp6VzevUmzz/NRAWunOT6E8HrnpGY7xOfc6K57fA==",
|
||||
"version": "4.26.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz",
|
||||
"integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==",
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
@@ -13774,6 +13788,7 @@
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz",
|
||||
"integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"quick-lru": "^5.1.1",
|
||||
"resolve-alpn": "^1.2.0"
|
||||
@@ -13880,6 +13895,7 @@
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
@@ -15796,6 +15812,7 @@
|
||||
},
|
||||
"node_modules/jszip": {
|
||||
"version": "3.10.1",
|
||||
"dev": true,
|
||||
"license": "(MIT OR GPL-3.0-or-later)",
|
||||
"dependencies": {
|
||||
"lie": "~3.3.0",
|
||||
@@ -15806,10 +15823,12 @@
|
||||
},
|
||||
"node_modules/jszip/node_modules/pako": {
|
||||
"version": "1.0.11",
|
||||
"dev": true,
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/jszip/node_modules/readable-stream": {
|
||||
"version": "2.3.7",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
@@ -15927,6 +15946,7 @@
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.3.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"immediate": "~3.0.5"
|
||||
@@ -16592,6 +16612,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz",
|
||||
"integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
@@ -16820,14 +16841,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mimic-response": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz",
|
||||
"integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==",
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
|
||||
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/min-document": {
|
||||
@@ -17213,17 +17233,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-stream-zip": {
|
||||
"version": "1.15.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/antelle"
|
||||
}
|
||||
},
|
||||
"node_modules/nopt": {
|
||||
"version": "6.0.0",
|
||||
"dev": true,
|
||||
@@ -17257,6 +17266,7 @@
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz",
|
||||
"integrity": "sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
@@ -17562,6 +17572,7 @@
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-4.0.1.tgz",
|
||||
"integrity": "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
}
|
||||
@@ -17795,7 +17806,8 @@
|
||||
},
|
||||
"node_modules/pend": {
|
||||
"version": "1.2.0",
|
||||
"dev": true,
|
||||
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/performance-now": {
|
||||
@@ -19494,6 +19506,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz",
|
||||
"integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lowercase-keys": "^3.0.0"
|
||||
},
|
||||
@@ -20026,6 +20039,7 @@
|
||||
},
|
||||
"node_modules/setimmediate": {
|
||||
"version": "1.0.5",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
@@ -21564,7 +21578,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unique-filename": {
|
||||
@@ -22646,12 +22662,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/yauzl": {
|
||||
"version": "2.10.0",
|
||||
"dev": true,
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.2.0.tgz",
|
||||
"integrity": "sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-crc32": "~0.2.3",
|
||||
"fd-slicer": "~1.1.0"
|
||||
"pend": "~1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/yn": {
|
||||
|
||||
+9
-37
@@ -19,6 +19,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=2164d6a7d641ecf6ad57852f665a518ca2bf960f --publish always --win --x64",
|
||||
"publish:linux": "npm run build && electron-builder --publish always --linux --x64"
|
||||
},
|
||||
@@ -53,7 +54,9 @@
|
||||
"afterSign": ".erb/scripts/notarize.js",
|
||||
"afterPack": ".erb/scripts/after-pack.js",
|
||||
"win": {
|
||||
"signingHashAlgorithms": ["sha256"],
|
||||
"signingHashAlgorithms": [
|
||||
"sha256"
|
||||
],
|
||||
"target": [
|
||||
"nsis",
|
||||
"nsis-web"
|
||||
@@ -114,34 +117,6 @@
|
||||
"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/fuses": "^1.7.0",
|
||||
"@electron/notarize": "^2.3.0",
|
||||
@@ -154,10 +129,9 @@
|
||||
"@types/color": "^3.0.3",
|
||||
"@types/crypto-js": "^4.2.1",
|
||||
"@types/dateformat": "^5.0.0",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/got": "^9.6.12",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/node": "20.11.15",
|
||||
"@types/node": "22.8.6",
|
||||
"@types/node-fetch": "^2.6.3",
|
||||
"@types/pako": "^2.0.1",
|
||||
"@types/react": "^18.0.33",
|
||||
@@ -239,7 +213,6 @@
|
||||
"color": "^4.2.3",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dateformat": "^5.0.3",
|
||||
"dompurify": "^3.0.9",
|
||||
"dot-prop": "^8.0.2",
|
||||
"electron-debug": "^3.2.0",
|
||||
"electron-log": "^4.4.8",
|
||||
@@ -249,14 +222,12 @@
|
||||
"format-duration": "^3.0.2",
|
||||
"framer-motion": "^11.2.6",
|
||||
"fs-extra": "^11.2.0",
|
||||
"got": "^14.4.2",
|
||||
"got": "^14.4.4",
|
||||
"history": "^5.3.0",
|
||||
"is-elevated": "^4.0.0",
|
||||
"jszip": "^3.10.1",
|
||||
"md5-file": "^5.0.0",
|
||||
"node-abi": "^3.65.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
"node-stream-zip": "^1.15.0",
|
||||
"pako": "^2.1.0",
|
||||
"protobufjs": "^7.4.0",
|
||||
"qrcode.react": "^4.0.1",
|
||||
@@ -281,7 +252,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"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
@@ -321,6 +293,6 @@
|
||||
"logLevel": "quiet"
|
||||
},
|
||||
"volta": {
|
||||
"node": "20.17.0"
|
||||
"node": "22.11.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
Binary file not shown.
@@ -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].*/)
|
||||
});
|
||||
});
|
||||
@@ -16,3 +16,9 @@ export const CACHE_PATH = path.join(app.getPath("userData"), "CachedData");
|
||||
export const IMAGE_CACHE_PATH = path.join(CACHE_PATH, "imagescache");
|
||||
|
||||
export const HTTP_STATUS_CODES = constants;
|
||||
|
||||
// Linux related stuff
|
||||
|
||||
export const PROTON_BINARY_PREFIX = "proton";
|
||||
export const WINE_BINARY_PREFIX = path.join("files", "bin", "wine64");
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -14,3 +14,4 @@ import "./model-saber.ipcs";
|
||||
import "./bs-model-ipcs";
|
||||
import "./bs-version-download/bs-download-ipcs";
|
||||
import "./static-configuration.ipcs";
|
||||
import "./linux.ipcs.ts";
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { LinuxService } from "main/services/linux.service";
|
||||
import { IpcService } from "../services/ipc.service";
|
||||
import { of } from "rxjs";
|
||||
|
||||
const ipc = IpcService.getInstance();
|
||||
|
||||
ipc.on("linux.verify-proton-folder", (_, reply) => {
|
||||
const linuxService = LinuxService.getInstance();
|
||||
reply(of(linuxService.verifyProtonPath()));
|
||||
});
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { shell, dialog, app, BrowserWindow } from "electron";
|
||||
import { shell, dialog, app, BrowserWindow, OpenDialogOptions } from "electron";
|
||||
import { NotificationService } from "../services/notification.service";
|
||||
import { IpcService } from "../services/ipc.service";
|
||||
import { from, of } from "rxjs";
|
||||
import { readFileSync } from "fs-extra";
|
||||
import log from "electron-log";
|
||||
import path from "path";
|
||||
|
||||
// TODO IMPROVE WINDOW CONTROL BY USING WINDOW SERVICE
|
||||
|
||||
@@ -19,7 +20,20 @@ ipc.on("open-dialog", (args, reply) => {
|
||||
})
|
||||
|
||||
ipc.on("choose-folder", (args, reply) => {
|
||||
reply(from(dialog.showOpenDialog({ properties: ["openDirectory"], defaultPath: args ?? "" })));
|
||||
const options: OpenDialogOptions = {
|
||||
properties: ["openDirectory"],
|
||||
defaultPath: args?.defaultPath ?? ""
|
||||
};
|
||||
|
||||
if (args?.showHidden) {
|
||||
options.properties.push("showHiddenFiles");
|
||||
}
|
||||
|
||||
if (args?.parent) {
|
||||
options.defaultPath = path.join(app.getPath(args.parent), options.defaultPath);
|
||||
}
|
||||
|
||||
reply(from(dialog.showOpenDialog(options)));
|
||||
});
|
||||
|
||||
ipc.on("choose-file", async (args, reply) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { of } from "rxjs";
|
||||
import { from, of } from "rxjs";
|
||||
import { IpcService } from "../services/ipc.service";
|
||||
import { StaticConfigurationService } from "../services/static-configuration.service";
|
||||
|
||||
@@ -10,5 +10,5 @@ ipc.on("static-configuration.get", (args, reply) => {
|
||||
});
|
||||
|
||||
ipc.on("static-configuration.set", (args, reply) => {
|
||||
reply(of(staticConfig.set(args.key, args.value)));
|
||||
reply(from(staticConfig.set(args.key, args.value)));
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,38 +118,41 @@ export class LocalPlaylistsManagerService {
|
||||
version?: BSVersion,
|
||||
dest?: string
|
||||
}): Promise<{path: string, localBPList: LocalBPList}> {
|
||||
const bplist = await this.readPlaylistFromSource(opt.bplistSource);
|
||||
const { bpList, filename } = await this.readPlaylistFromSource(opt.bplistSource);
|
||||
|
||||
const dest = await (async () => {
|
||||
if(opt.dest && path.isAbsolute(opt.dest) && this.acceptPlaylistFiletype(opt.dest)) { return opt.dest; }
|
||||
const playlistFolder = await this.getPlaylistsFolder(opt.version);
|
||||
return path.join(playlistFolder, `${sanitize(bplist.playlistTitle)}.bplist`);
|
||||
return path.join(playlistFolder, sanitize(filename));
|
||||
})();
|
||||
|
||||
writeFileSync(dest, JSON.stringify(bplist, null, 2));
|
||||
writeFileSync(dest, JSON.stringify(bpList, null, 2));
|
||||
|
||||
const localBPList: LocalBPList = { ...bplist, path: dest };
|
||||
const localBPList: LocalBPList = { ...bpList, path: dest };
|
||||
|
||||
return { path: dest, localBPList };
|
||||
}
|
||||
|
||||
|
||||
private async readPlaylistFromSource(source: string): Promise<BPList> {
|
||||
private async readPlaylistFromSource(source: string): Promise<{ bpList: BPList, filename: string }> {
|
||||
const isLocalFile = await pathExists(source).catch(e => { log.error(e); return false; });
|
||||
|
||||
if(!isLocalFile && !isValidUrl(source)) {
|
||||
throw new CustomError(`Invalid source (${source})`, "INVALID_SOURCE");
|
||||
}
|
||||
|
||||
const bpList: BPList = await (async () => {
|
||||
const { bpList, filename }: { bpList: BPList, filename: string } = await (async () => {
|
||||
if(isLocalFile){
|
||||
const res = await tryit(async () => JSON.parse(readFileSync(source).toString()));
|
||||
const res = await tryit(async () => JSON.parse(readFileSync(source).toString()) as BPList);
|
||||
if(res.error) {
|
||||
throw CustomError.fromError(res.error, "CANNOT_PARSE_PLAYLIST");
|
||||
}
|
||||
return res.result;
|
||||
return { bpList: res.result, filename: path.basename(source) };
|
||||
}
|
||||
return this.request.getJSON<BPList>(source);
|
||||
|
||||
const res = await this.request.getJSON<BPList>(source);
|
||||
const filename = this.request.getFilenameFromContentDisposition(res.headers["content-disposition"]) ?? `${res.data.playlistTitle}.bplist`;
|
||||
return { bpList: res.data, filename };
|
||||
})();
|
||||
|
||||
if(!bpList?.playlistTitle) {
|
||||
@@ -160,7 +163,7 @@ export class LocalPlaylistsManagerService {
|
||||
{ ...s, hash: findHashInString(s.hash) ?? s.hash }
|
||||
) : s).filter(Boolean);
|
||||
|
||||
return bpList;
|
||||
return { bpList, filename };
|
||||
}
|
||||
|
||||
private openOneClickDownloadPlaylistWindow(downloadUrl: string): void {
|
||||
@@ -189,14 +192,14 @@ export class LocalPlaylistsManagerService {
|
||||
progress.total = playlistPaths.length;
|
||||
|
||||
for (const playlistPath of playlistPaths) {
|
||||
const {result: bpList, error} = await tryit(() => this.readPlaylistFromSource(playlistPath));
|
||||
const {result, error} = await tryit(() => this.readPlaylistFromSource(playlistPath));
|
||||
|
||||
if(error) {
|
||||
log.error(error);
|
||||
continue;
|
||||
}
|
||||
|
||||
const localBpList: LocalBPList = { ...bpList, path: playlistPath };
|
||||
const localBpList: LocalBPList = { ...result.bpList, path: playlistPath };
|
||||
bpLists.push(localBpList);
|
||||
progress.current += 1;
|
||||
obs.next(progress);
|
||||
|
||||
@@ -8,7 +8,6 @@ import { UtilsService } from "../../utils.service";
|
||||
import crypto, { BinaryLike } from "crypto";
|
||||
import { lstatSync } from "fs";
|
||||
import { copy, createReadStream, ensureDir, pathExists, pathExistsSync, realpath, unlink } from "fs-extra";
|
||||
import StreamZip from "node-stream-zip";
|
||||
import { RequestService } from "../../request.service";
|
||||
import sanitize from "sanitize-filename";
|
||||
import { DeepLinkService } from "../../deep-link.service";
|
||||
@@ -30,6 +29,8 @@ import { MapInfo } from "shared/models/maps/info/map-info.model";
|
||||
import { parseMapInfoDat } from "shared/parsers/maps/map-info.parser";
|
||||
import { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||
import { tryit } from "shared/helpers/error.helpers";
|
||||
import { BsmZipExtractor } from "main/models/bsm-zip-extractor.class";
|
||||
import { escapeRegExp } from "../../../../shared/helpers/string.helpers";
|
||||
|
||||
export class LocalMapsManagerService {
|
||||
private static instance: LocalMapsManagerService;
|
||||
@@ -169,17 +170,13 @@ export class LocalMapsManagerService {
|
||||
return getUrlsAndReturn(mapInfo, hash, 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 dest = path.join(tempPath, fileName);
|
||||
|
||||
|
||||
const zipPath = (await lastValueFrom(this.reqService.downloadFile(zipUrl, dest))).data;
|
||||
const zip = new StreamZip.async({ file: zipPath });
|
||||
|
||||
return { zip, zipPath };
|
||||
return (await lastValueFrom(this.reqService.downloadFile(zipUrl, dest))).data;
|
||||
}
|
||||
|
||||
public getMaps(version?: BSVersion): Observable<BsmLocalMapsProgress> {
|
||||
@@ -328,16 +325,16 @@ export class LocalMapsManagerService {
|
||||
|
||||
public importMaps(zipPaths: string[], version?: BSVersion): Observable<Progression<BsmLocalMap>> {
|
||||
return new Observable<Progression<BsmLocalMap>>(obs => {
|
||||
let unsubscribed = false;
|
||||
let progress: Progression<BsmLocalMap> = { total: 0, current: 0 };
|
||||
let nbImportedMaps = 0;
|
||||
const abortController = new AbortController();
|
||||
let zip: BsmZipExtractor;
|
||||
|
||||
(async () => {
|
||||
const mapsPath = await this.getMapsFolderPath(version);
|
||||
for(const zipPath of zipPaths) {
|
||||
|
||||
if(unsubscribed) {
|
||||
log.info("Maps importation from zip has been cancelled");
|
||||
if(abortController.signal?.aborted) {
|
||||
log.info("Maps import from zip has been cancelled");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -346,75 +343,62 @@ export class LocalMapsManagerService {
|
||||
|
||||
if(!pathExistsSync(zipPath)) { continue; }
|
||||
|
||||
const zip = new StreamZip.async({ file: zipPath });
|
||||
const { result: zipEntries, error } = await tryit(() => zip.entries());
|
||||
zip = await BsmZipExtractor.fromPath(zipPath);
|
||||
const mapsFolders = (await zip.filterEntries(entry => /(^|\/)[Ii]nfo\.dat$/.test(entry.fileName)))
|
||||
.map(entry => path.dirname(entry.fileName));
|
||||
|
||||
if(error) {
|
||||
const res = await tryit(() => zip.close());
|
||||
log.error("Could not read zip entries", zipPath, error, res?.error);
|
||||
continue;
|
||||
}
|
||||
|
||||
const zipEntriesValues = Object.values(zipEntries);
|
||||
|
||||
const mapsFolders = zipEntriesValues.reduce((acc, entry) => {
|
||||
if(!/(^|\/)[Ii]nfo\.dat$/.test(entry.name)){ return acc; }
|
||||
acc.push(path.dirname(entry.name));
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
if(mapsFolders.length === 0) {
|
||||
if (mapsFolders.length === 0) {
|
||||
log.warn("No maps \"info.dat\" found in zip", zipPath);
|
||||
progress.total = 1;
|
||||
progress.current = 1;
|
||||
obs.next(progress);
|
||||
continue;
|
||||
}
|
||||
|
||||
progress.total = mapsFolders.length;
|
||||
obs.next(progress);
|
||||
|
||||
for(const folder of mapsFolders) {
|
||||
const isRoot = mapsFolders.length === 1 && mapsFolders[0] === ".";
|
||||
const destination = isRoot
|
||||
? path.join(mapsPath, path.basename(zipPath, ".zip"))
|
||||
: mapsPath;
|
||||
|
||||
if(unsubscribed) {
|
||||
log.info("Maps importation from zip has been cancelled");
|
||||
await zip.close();
|
||||
return;
|
||||
}
|
||||
log.info("Extracting", `"${zipPath}"`, "into", `"${mapsPath}"`);
|
||||
for (const folder of mapsFolders) {
|
||||
log.info(">", folder);
|
||||
|
||||
const isRoot = folder === ".";
|
||||
const dest = isRoot ? path.join(mapsPath, path.basename(zipPath, ".zip")) : path.join(mapsPath, folder);
|
||||
const regex = new RegExp(`^${escapeRegExp(folder)}\\/`);
|
||||
|
||||
let extract: () => Promise<BsmLocalMap>;
|
||||
const exported = await zip.extract(destination, {
|
||||
entriesNames: [regex],
|
||||
abortToken: abortController
|
||||
});
|
||||
|
||||
if(isRoot){
|
||||
const entries = zipEntriesValues.filter(entry => entry.isFile && path.dirname(entry.name) === ".");
|
||||
extract = async () => {
|
||||
await Promise.all(entries.map(entry => {
|
||||
log.info("Extracting", `"${entry.name}"`, "from", `"${zipPath}"`, "into", `"${path.join(dest, path.basename(entry.name))}"`);
|
||||
return zip.extract(entry.name, path.join(dest, path.basename(entry.name)));
|
||||
}));
|
||||
return this.loadMapInfoFromPath(dest);
|
||||
};
|
||||
} else {
|
||||
extract = async () => {
|
||||
log.info("Extracting", `"${folder}"`, "from", `"${zipPath}"`, "into", `"${mapsPath}"`);
|
||||
await zip.extract(folder, dest);
|
||||
return this.loadMapInfoFromPath(dest);
|
||||
}
|
||||
}
|
||||
|
||||
await ensureDir(dest);
|
||||
const { result: bsmMap, error } = await tryit(extract);
|
||||
|
||||
if(error) {
|
||||
log.error("Could not extract map", zipPath, folder, mapsPath, error);
|
||||
if(exported.length === 0) {
|
||||
log.warn("No files extracted from", folder);
|
||||
continue;
|
||||
}
|
||||
|
||||
nbImportedMaps++;
|
||||
progress.current++;
|
||||
progress.data = bsmMap;
|
||||
if (abortController.signal?.aborted) {
|
||||
break;
|
||||
}
|
||||
|
||||
++nbImportedMaps;
|
||||
++progress.current;
|
||||
progress.data = await this.loadMapInfoFromPath(path.join(destination, folder));
|
||||
obs.next(progress);
|
||||
|
||||
if (abortController.signal?.aborted) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await zip.close();
|
||||
zip.close();
|
||||
|
||||
if (abortController.signal?.aborted) {
|
||||
log.info("Maps import from zip has been cancelled");
|
||||
return;
|
||||
}
|
||||
}
|
||||
})()
|
||||
.then(() => {
|
||||
@@ -424,10 +408,15 @@ export class LocalMapsManagerService {
|
||||
return log.info("Successfully imported", nbImportedMaps, "maps from", zipPaths.length, "zips");
|
||||
})
|
||||
.catch(e => obs.error(e))
|
||||
.finally(() => obs.complete());
|
||||
.finally(() => {
|
||||
if (zip) {
|
||||
zip.close();
|
||||
}
|
||||
obs.complete()
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribed = true;
|
||||
abortController.abort();
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -440,7 +429,7 @@ export class LocalMapsManagerService {
|
||||
log.info("Downloading map", map.name, map.id);
|
||||
|
||||
const zipUrl = map.versions.at(0).downloadURL;
|
||||
const mapFolderName = sanitize(`${map.id}-${map.name}`);
|
||||
const mapFolderName = sanitize(`${map.id} (${map.metadata.songName} - ${map.metadata.levelAuthorName})`);
|
||||
const mapsFolder = await this.getMapsFolderPath(version);
|
||||
|
||||
const mapPath = path.join(mapsFolder, mapFolderName);
|
||||
@@ -454,16 +443,10 @@ export class LocalMapsManagerService {
|
||||
return installedMap;
|
||||
}
|
||||
|
||||
const { zip, zipPath } = await this.downloadMapZip(zipUrl);
|
||||
|
||||
if (!zip) {
|
||||
throw new Error(`Cannot download ${zipUrl}`);
|
||||
}
|
||||
|
||||
await ensureFolderExist(mapPath);
|
||||
|
||||
await zip.extract(null, mapPath);
|
||||
await zip.close();
|
||||
const zipPath = await this.downloadMapZip(zipUrl);
|
||||
const zip = await BsmZipExtractor.fromPath(zipPath);
|
||||
await zip.extract(mapPath);
|
||||
zip.close();
|
||||
await unlink(zipPath);
|
||||
|
||||
const localMap = await this.loadMapInfoFromPath(mapPath);
|
||||
|
||||
@@ -141,6 +141,7 @@ export class SongDetailsCacheService {
|
||||
|
||||
if(error) {
|
||||
lastError = error;
|
||||
log.error("Failed to download SongDetailCache file", etag, error);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -152,7 +153,6 @@ export class SongDetailsCacheService {
|
||||
}
|
||||
}
|
||||
|
||||
log.error("Failed to download SongDetailCache file", etag, lastError);
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,8 +14,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){
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { Observable } from "rxjs";
|
||||
import { BSLaunchError, BSLaunchEvent, BSLaunchEventData, BSLaunchWarning, LaunchOption } from "../../../shared/models/bs-launch";
|
||||
import { StoreLauncherInterface } from "./store-launcher.interface";
|
||||
import { pathExists, rename } from "fs-extra";
|
||||
import { pathExists, pathExistsSync, rename } from "fs-extra";
|
||||
import { SteamService } from "../steam.service";
|
||||
import path from "path";
|
||||
import { BS_APP_ID, BS_EXECUTABLE, STEAMVR_APP_ID } from "../../constants";
|
||||
import { BS_APP_ID, BS_EXECUTABLE, PROTON_BINARY_PREFIX, STEAMVR_APP_ID } from "../../constants";
|
||||
import log from "electron-log";
|
||||
import { AbstractLauncherService } from "./abstract-launcher.service";
|
||||
import { CustomError } from "../../../shared/models/exceptions/custom-error.class";
|
||||
import { UtilsService } from "../utils.service";
|
||||
import { exec } from "child_process";
|
||||
import fs from 'fs';
|
||||
import { StaticConfigurationService } from "../static-configuration.service";
|
||||
|
||||
export class SteamLauncherService extends AbstractLauncherService implements StoreLauncherInterface{
|
||||
|
||||
@@ -23,11 +24,13 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto
|
||||
return SteamLauncherService.instance;
|
||||
}
|
||||
|
||||
private readonly staticConfig: StaticConfigurationService;
|
||||
private readonly steam: SteamService;
|
||||
private readonly util: UtilsService;
|
||||
|
||||
private constructor(){
|
||||
super();
|
||||
this.staticConfig = StaticConfigurationService.getInstance();
|
||||
this.steam = SteamService.getInstance();
|
||||
this.util = UtilsService.getInstance();
|
||||
}
|
||||
@@ -128,9 +131,16 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto
|
||||
`${exePath}`,
|
||||
...launchArgs,
|
||||
];
|
||||
exePath = launchOptions.protonPath;
|
||||
if (!exePath) {
|
||||
throw CustomError.fromError(new Error("Proton path not set"), BSLaunchError.PROTON_NOT_SET);
|
||||
|
||||
if (!this.staticConfig.has("proton-folder")) {
|
||||
throw CustomError.fromError(new Error("Proton folder not set"), BSLaunchError.PROTON_NOT_SET);
|
||||
}
|
||||
exePath = path.join(this.staticConfig.get("proton-folder"), PROTON_BINARY_PREFIX);
|
||||
if (!pathExistsSync(exePath)) {
|
||||
throw CustomError.fromError(
|
||||
new Error("Could not locate proton binary"),
|
||||
BSLaunchError.PROTON_NOT_FOUND
|
||||
);
|
||||
}
|
||||
|
||||
// Setup Proton environment variables
|
||||
|
||||
@@ -33,7 +33,7 @@ export class BSVersionLibService {
|
||||
}
|
||||
|
||||
private getRemoteVersions(): Promise<BSVersion[]> {
|
||||
return this.requestService.getJSON<BSVersion[]>(this.REMOTE_BS_VERSIONS_URL);
|
||||
return this.requestService.getJSON<BSVersion[]>(this.REMOTE_BS_VERSIONS_URL).then(res => res.data);
|
||||
}
|
||||
|
||||
private async getLocalVersions(): Promise<BSVersion[]> {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import path from "path";
|
||||
import { pathExistsSync } from "fs-extra";
|
||||
import { PROTON_BINARY_PREFIX, WINE_BINARY_PREFIX } from "main/constants";
|
||||
import { StaticConfigurationService } from "./static-configuration.service";
|
||||
|
||||
export class LinuxService {
|
||||
private static instance: LinuxService;
|
||||
|
||||
public static getInstance(): LinuxService {
|
||||
if (!LinuxService.instance) {
|
||||
LinuxService.instance = new LinuxService();
|
||||
}
|
||||
return LinuxService.instance;
|
||||
}
|
||||
|
||||
private readonly staticConfig: StaticConfigurationService;
|
||||
|
||||
private constructor() {
|
||||
this.staticConfig = StaticConfigurationService.getInstance();
|
||||
}
|
||||
|
||||
public verifyProtonPath(protonFolder: string = ""): boolean {
|
||||
if (protonFolder === "") {
|
||||
if (!this.staticConfig.has("proton-folder")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
protonFolder = this.staticConfig.get("proton-folder");
|
||||
}
|
||||
|
||||
const protonPath = path.join(protonFolder, PROTON_BINARY_PREFIX);
|
||||
const winePath = path.join(protonFolder, WINE_BINARY_PREFIX);
|
||||
return pathExistsSync(protonPath) && pathExistsSync(winePath);
|
||||
}
|
||||
|
||||
public getWinePath(): string {
|
||||
if (!this.staticConfig.has("proton-folder")) {
|
||||
throw new Error("proton-folder variable not set");
|
||||
}
|
||||
|
||||
const winePath = path.join(
|
||||
this.staticConfig.get("proton-folder"),
|
||||
WINE_BINARY_PREFIX
|
||||
);
|
||||
if (!pathExistsSync(winePath)) {
|
||||
throw new Error(`"${winePath}" binary file not found`);
|
||||
}
|
||||
|
||||
return winePath;
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ export class BeatModsApiService {
|
||||
if (this.aliasesCache.size) {
|
||||
return this.aliasesCache;
|
||||
}
|
||||
return this.requestService.getJSON<Record<string, string[]>>(this.BEAT_MODS_ALIAS).then(rawAliases => {
|
||||
return this.requestService.getJSON<Record<string, string[]>>(this.BEAT_MODS_ALIAS).then(({ data: rawAliases }) => {
|
||||
Object.entries(rawAliases).forEach(([key, value]) => {
|
||||
this.aliasesCache.set(
|
||||
key,
|
||||
@@ -97,7 +97,7 @@ export class BeatModsApiService {
|
||||
|
||||
const alias = await this.getAliasOfVersion(version);
|
||||
|
||||
return this.requestService.getJSON<Mod[]>(this.getVersionModsUrl(alias)).then(mods => {
|
||||
return this.requestService.getJSON<Mod[]>(this.getVersionModsUrl(alias)).then(({ data: mods }) => {
|
||||
mods = mods.map(mod => this.asignDependencies(mod, mods));
|
||||
this.versionModsCache.set(version.BSVersion, mods);
|
||||
|
||||
@@ -111,7 +111,7 @@ export class BeatModsApiService {
|
||||
if (this.allModsCache) {
|
||||
return this.allModsCache;
|
||||
}
|
||||
return this.requestService.getJSON<Mod[]>(this.getAllModsUrl()).then(mods => {
|
||||
return this.requestService.getJSON<Mod[]>(this.getAllModsUrl()).then(({ data: mods }) => {
|
||||
this.allModsCache = mods;
|
||||
return this.allModsCache;
|
||||
});
|
||||
@@ -122,7 +122,7 @@ export class BeatModsApiService {
|
||||
return Promise.resolve(this.modsHashCache.get(hash));
|
||||
}
|
||||
|
||||
return this.requestService.getJSON<Mod[]>(`${this.BEAT_MODS_API_URL}mod?hash=${hash}`).then(mods => {
|
||||
return this.requestService.getJSON<Mod[]>(`${this.BEAT_MODS_API_URL}mod?hash=${hash}`).then(({ data: mods }) => {
|
||||
this.updateModsHashCache(mods);
|
||||
return mods.at(0);
|
||||
});
|
||||
|
||||
@@ -10,20 +10,25 @@ import { BS_EXECUTABLE } from "../../constants";
|
||||
import log from "electron-log";
|
||||
import { deleteFolder, pathExist, Progression, unlinkPath } from "../../helpers/fs.helpers";
|
||||
import { lastValueFrom, Observable } 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 { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||
import { popElement } from "shared/helpers/array.helpers";
|
||||
import { LinuxService } from "../linux.service";
|
||||
import { tryit } from "shared/helpers/error.helpers";
|
||||
import { UtilsService } from "../utils.service";
|
||||
import crypto from "crypto";
|
||||
import { BsmZipExtractor } from "main/models/bsm-zip-extractor.class";
|
||||
|
||||
export class BsModsManagerService {
|
||||
private static instance: BsModsManagerService;
|
||||
|
||||
private readonly beatModsApi: BeatModsApiService;
|
||||
private readonly bsLocalService: BSLocalVersionService;
|
||||
private readonly linuxService: LinuxService;
|
||||
private readonly requestService: RequestService;
|
||||
private readonly utilsService: UtilsService;
|
||||
|
||||
private manifestMatches: Mod[];
|
||||
|
||||
@@ -37,7 +42,9 @@ export class BsModsManagerService {
|
||||
private constructor() {
|
||||
this.beatModsApi = BeatModsApiService.getInstance();
|
||||
this.bsLocalService = BSLocalVersionService.getInstance();
|
||||
this.linuxService = LinuxService.getInstance();
|
||||
this.requestService = RequestService.getInstance();
|
||||
this.utilsService = UtilsService.getInstance();
|
||||
}
|
||||
|
||||
private async getModFromHash(hash: string): Promise<Mod> {
|
||||
@@ -106,7 +113,7 @@ export class BsModsManagerService {
|
||||
return this.beatModsApi.getModByHash(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);
|
||||
@@ -122,10 +129,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> {
|
||||
@@ -140,25 +144,34 @@ export class BsModsManagerService {
|
||||
return false;
|
||||
}
|
||||
|
||||
return new Promise<boolean>(resolve => {
|
||||
const cmd = process.platform === 'linux'
|
||||
? `screen -dmS "BSIPA" dotnet "${ipaPath}" ${args.join(" ")}` // Must run through screen, otherwise BSIPA tries to move console cursor and crashes.
|
||||
: `"${ipaPath}" ${args.join(" ")}`;
|
||||
let cmd = `"${ipaPath}" "${bsExePath}" ${args.join(" ")}`;
|
||||
if (process.platform === "linux") {
|
||||
const { error, result: winePath } = tryit(() => this.linuxService.getWinePath());
|
||||
if (error) {
|
||||
log.error(error);
|
||||
return false;
|
||||
}
|
||||
cmd = `"${winePath}" ${cmd}`;
|
||||
}
|
||||
|
||||
return new Promise<boolean>(resolve => {
|
||||
log.info("START IPA PROCESS", cmd);
|
||||
const processIPA = spawn(cmd, { cwd: versionPath, detached: true, shell: true });
|
||||
|
||||
const timemout = setTimeout(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
log.info("Ipa process timeout");
|
||||
resolve(false)
|
||||
}, sToMs(30));
|
||||
|
||||
processIPA.stdout.on("data", data => {
|
||||
log.info("IPA process stdout", data.toString());
|
||||
});
|
||||
processIPA.stderr.on("data", data => {
|
||||
log.error("IPA process stderr", data.toString());
|
||||
})
|
||||
|
||||
processIPA.once("exit", code => {
|
||||
clearTimeout(timemout);
|
||||
clearTimeout(timeout);
|
||||
if (code === 0) {
|
||||
log.info("Ipa process exist with code 0");
|
||||
return resolve(true);
|
||||
@@ -188,43 +201,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);
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { Agent, RequestOptions } from "https";
|
||||
import { createWriteStream } from "fs";
|
||||
import { Progression } from "main/helpers/fs.helpers";
|
||||
import { Observable, shareReplay, tap } from "rxjs";
|
||||
import log from "electron-log";
|
||||
import fetch, { RequestInfo, RequestInit } from "node-fetch";
|
||||
import got, { Options } from "got";
|
||||
import { IncomingMessage } from "http";
|
||||
import { app } from "electron";
|
||||
import os from "os";
|
||||
import { unlinkSync } from "fs-extra";
|
||||
import { tryit } from "shared/helpers/error.helpers";
|
||||
import { createWriteStream, WriteStream } from 'fs';
|
||||
import { Progression } from 'main/helpers/fs.helpers';
|
||||
import { Observable } from 'rxjs';
|
||||
import { shareReplay, tap } from 'rxjs/operators';
|
||||
import log from 'electron-log';
|
||||
import got from 'got';
|
||||
import { IncomingHttpHeaders, IncomingMessage } from 'http';
|
||||
import { unlinkSync } from 'fs-extra';
|
||||
import { tryit } from 'shared/helpers/error.helpers';
|
||||
import path from 'path';
|
||||
import { pipeline } from 'stream/promises';
|
||||
import sanitize from 'sanitize-filename';
|
||||
import internal from 'stream';
|
||||
|
||||
export class RequestService {
|
||||
private static instance: RequestService;
|
||||
private preferredFamily: number | undefined = undefined;
|
||||
|
||||
public static getInstance(): RequestService {
|
||||
if (!RequestService.instance) {
|
||||
@@ -21,121 +23,194 @@ export class RequestService {
|
||||
return RequestService.instance;
|
||||
}
|
||||
|
||||
private readonly defaultRequestInit: RequestInit;
|
||||
private constructor() {}
|
||||
|
||||
private constructor() {
|
||||
public async getJSON<T = unknown>(url: string): Promise<{ data: T; headers: IncomingHttpHeaders }> {
|
||||
|
||||
this.defaultRequestInit = {
|
||||
headers: {
|
||||
"User-Agent": `BSManager/${app.getVersion()} (${os.type()} ${os.release()})`
|
||||
},
|
||||
agent: new Agent({ family: 4 }),
|
||||
};
|
||||
}
|
||||
const familiesToTry = this.preferredFamily ? [this.preferredFamily, this.preferredFamily === 4 ? 6 : 4] : [4, 6];
|
||||
|
||||
private getInitWithOptions(options?: RequestInit): RequestInit {
|
||||
return { ...this.defaultRequestInit, ...(options || {}) };
|
||||
}
|
||||
for (const family of familiesToTry) {
|
||||
try {
|
||||
|
||||
private requestOptionsFromDefaultInit(): RequestOptions {
|
||||
return {
|
||||
headers: this.defaultRequestInit.headers as Record<string, string>,
|
||||
agent: this.defaultRequestInit.agent as Agent,
|
||||
};
|
||||
}
|
||||
|
||||
public async getJSON<T = unknown>(url: RequestInfo, options?: RequestInit): Promise<T> {
|
||||
|
||||
try {
|
||||
const response = await fetch(url, this.getInitWithOptions(options));
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status} ${url}`);
|
||||
// @ts-ignore (ESM is not well supported in this project, We need to move out electron-react-boilerplate, and use Vite)
|
||||
const res = await got(url, { dnsLookupIpVersion: family, responseType: 'json' });
|
||||
this.preferredFamily = family;
|
||||
return { data: res.body as T, headers: res.headers };
|
||||
} catch (err) {
|
||||
log.warn(`IPv${family} request failed, trying next one... URL: ${url}`, err);
|
||||
}
|
||||
|
||||
return await response.json() as T;
|
||||
} catch (err) {
|
||||
log.error(err);
|
||||
throw err;
|
||||
}
|
||||
|
||||
log.error(`IPv4 and IPv6 requests failed for URL: ${url}`);
|
||||
this.preferredFamily = undefined;
|
||||
throw new Error(`IPv4 and IPv6 requests failed for URL: ${url}`);
|
||||
}
|
||||
|
||||
public downloadFile(url: string, dest: string): Observable<Progression<string>> {
|
||||
return new Observable<Progression<string>>(subscriber => {
|
||||
const progress: Progression<string> = { current: 0, total: 0, data: dest };
|
||||
public downloadFile(
|
||||
url: string,
|
||||
dest: string,
|
||||
opt?: { preferContentDisposition?: boolean }
|
||||
): Observable<Progression<string>> {
|
||||
return new Observable<Progression<string>>((subscriber) => {
|
||||
const progress: Progression<string> = { current: 0, total: 0 };
|
||||
const familiesToTry = this.preferredFamily ? [this.preferredFamily, this.preferredFamily === 4 ? 6 : 4] : [4, 6];
|
||||
|
||||
const stream = got.stream(url)
|
||||
const file = createWriteStream(dest);
|
||||
let attempt = 0;
|
||||
let stream: got.GotEmitter & internal.Duplex;
|
||||
|
||||
stream.on("downloadProgress", ({ transferred, total }) => {
|
||||
progress.current = transferred;
|
||||
progress.total = total;
|
||||
subscriber.next(progress);
|
||||
});
|
||||
const tryNextFamily = () => {
|
||||
if (attempt >= familiesToTry.length) {
|
||||
subscriber.error(new Error(`Download failed over IPv4 and IPv6 for URL: ${url}`));
|
||||
return;
|
||||
}
|
||||
|
||||
stream.on("error", err => {
|
||||
tryit(() => unlinkSync(dest));
|
||||
subscriber.error(err);
|
||||
});
|
||||
const family = familiesToTry[attempt++];
|
||||
let file: WriteStream | undefined;
|
||||
|
||||
stream.on("end", () => {
|
||||
subscriber.next(progress);
|
||||
subscriber.complete();
|
||||
});
|
||||
// @ts-ignore (ESM is not well supported in this project, We need to move out electron-react-boilerplate, and use Vite)
|
||||
stream = got.stream(url, { dnsLookupIpVersion: family });
|
||||
|
||||
stream.pipe(file);
|
||||
stream.on('response', (response) => {
|
||||
this.preferredFamily = family;
|
||||
|
||||
const filename = opt?.preferContentDisposition ? this.getFilenameFromContentDisposition(response.headers['content-disposition']) : null;
|
||||
|
||||
if (filename) {
|
||||
dest = path.join(path.dirname(dest), sanitize(filename));
|
||||
}
|
||||
|
||||
progress.data = dest;
|
||||
file = createWriteStream(dest);
|
||||
|
||||
pipeline(stream, file).catch(err => {
|
||||
file?.destroy();
|
||||
tryit(() => unlinkSync(dest));
|
||||
subscriber.error(err);
|
||||
});
|
||||
});
|
||||
|
||||
stream.on('downloadProgress', ({ transferred, total }) => {
|
||||
progress.current = transferred;
|
||||
progress.total = total;
|
||||
subscriber.next(progress);
|
||||
});
|
||||
|
||||
stream.on('error', err => {
|
||||
log.warn(`Download failed over IPv${family} for URL: ${url}`, err);
|
||||
stream.destroy();
|
||||
file?.destroy();
|
||||
tryNextFamily();
|
||||
});
|
||||
|
||||
stream.on('end', () => {
|
||||
file?.end();
|
||||
subscriber.next(progress);
|
||||
subscriber.complete();
|
||||
});
|
||||
};
|
||||
|
||||
tryNextFamily();
|
||||
|
||||
return () => {
|
||||
stream.destroy();
|
||||
}
|
||||
|
||||
}).pipe(tap({ error: e => log.error(e, url, dest) }), shareReplay(1));
|
||||
stream?.destroy();
|
||||
};
|
||||
}).pipe(
|
||||
tap({ error: (e) => log.error(e, url, dest) }),
|
||||
shareReplay(1)
|
||||
);
|
||||
}
|
||||
|
||||
public downloadBuffer(url: string, options?: Options & { isStream?: true }): Observable<Progression<Buffer, IncomingMessage>> {
|
||||
return new Observable<Progression<Buffer, IncomingMessage>>(subscriber => {
|
||||
public downloadBuffer(
|
||||
url: string,
|
||||
options?: got.GotOptions<null>
|
||||
): Observable<Progression<Buffer, IncomingMessage>> {
|
||||
return new Observable<Progression<Buffer, IncomingMessage>>((subscriber) => {
|
||||
const progress: Progression<Buffer, IncomingMessage> = {
|
||||
current: 0,
|
||||
total: 0,
|
||||
data: null,
|
||||
};
|
||||
|
||||
const req = got.stream(url, options);
|
||||
const familiesToTry = this.preferredFamily ? [this.preferredFamily, this.preferredFamily === 4 ? 6 : 4] : [4, 6];
|
||||
|
||||
let data = Buffer.alloc(0);
|
||||
let response: IncomingMessage;
|
||||
let attempt = 0;
|
||||
let stream: got.GotEmitter & internal.Duplex;
|
||||
|
||||
req.once("response", res => {
|
||||
response = res;
|
||||
});
|
||||
const tryNextFamily = () => {
|
||||
if (attempt >= familiesToTry.length) {
|
||||
subscriber.error(new Error(`Download failed over IPv4 and IPv6 for URL: ${url}`));
|
||||
return;
|
||||
}
|
||||
|
||||
req.on("data", (chunk: Buffer) => {
|
||||
data = Buffer.concat([data, chunk]);
|
||||
})
|
||||
const family = familiesToTry[attempt++];
|
||||
|
||||
req.on("downloadProgress", ({ transferred, total }) => {
|
||||
progress.current = transferred;
|
||||
progress.total = total;
|
||||
subscriber.next(progress);
|
||||
});
|
||||
// @ts-ignore (ESM is not well supported in this project, We need to move out electron-react-boilerplate, and use Vite)
|
||||
stream = got.stream(url, { dnsLookupIpVersion: family, ...(options ?? {}) });
|
||||
|
||||
req.once("error", err => {
|
||||
subscriber.error(err);
|
||||
});
|
||||
let data = Buffer.alloc(0);
|
||||
let response: IncomingMessage;
|
||||
|
||||
req.once("end", () => {
|
||||
progress.data = data;
|
||||
progress.extra = response;
|
||||
subscriber.next(progress);
|
||||
subscriber.complete();
|
||||
});
|
||||
stream.once('response', (res) => {
|
||||
this.preferredFamily = family;
|
||||
response = res;
|
||||
});
|
||||
|
||||
req.resume();
|
||||
stream.on('data', (chunk: Buffer) => {
|
||||
data = Buffer.concat([data, chunk]);
|
||||
});
|
||||
|
||||
stream.on('downloadProgress', ({ transferred, total }) => {
|
||||
progress.current = transferred;
|
||||
progress.total = total;
|
||||
subscriber.next(progress);
|
||||
});
|
||||
|
||||
stream.on('error', err => {
|
||||
log.warn(`Download failed over IPv${family} for URL: ${url}`, err);
|
||||
stream.destroy();
|
||||
tryNextFamily();
|
||||
});
|
||||
|
||||
stream.on('end', () => {
|
||||
progress.data = data;
|
||||
progress.extra = response;
|
||||
subscriber.next(progress);
|
||||
subscriber.complete();
|
||||
});
|
||||
};
|
||||
|
||||
tryNextFamily();
|
||||
|
||||
return () => {
|
||||
req.destroy();
|
||||
}
|
||||
stream?.destroy();
|
||||
};
|
||||
}).pipe(
|
||||
tap({ error: (e) => log.error(e, url) }),
|
||||
shareReplay(1)
|
||||
);
|
||||
}
|
||||
|
||||
}).pipe(tap({ error: log.error }), shareReplay(1))
|
||||
public getFilenameFromContentDisposition(disposition: string): string | undefined {
|
||||
|
||||
if(!disposition) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const utf8FilenameRegex = /filename\*=UTF-8''([\w%\-\.]+)(?:; ?|$)/i;
|
||||
const asciiFilenameRegex = /^filename=(["']?)(.*?[^\\])\1(?:; ?|$)/i;
|
||||
|
||||
const utf8Match = utf8FilenameRegex.exec(disposition);
|
||||
if (utf8Match?.[1]) {
|
||||
return decodeURIComponent(utf8Match[1]);
|
||||
}
|
||||
|
||||
const filenameStart = disposition.toLowerCase().indexOf('filename=');
|
||||
|
||||
if (filenameStart < 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const partialDisposition = disposition.slice(filenameStart);
|
||||
return asciiFilenameRegex.exec(partialDisposition)?.[2];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import ElectronStore from "electron-store";
|
||||
import { pathExistsSync } from "fs-extra";
|
||||
import path from "path";
|
||||
import { PROTON_BINARY_PREFIX, WINE_BINARY_PREFIX } from "main/constants";
|
||||
import { Observable, Subject } from "rxjs";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||
|
||||
export class StaticConfigurationService {
|
||||
private static instance: StaticConfigurationService;
|
||||
@@ -34,7 +38,17 @@ export class StaticConfigurationService {
|
||||
cb(this.get(key));
|
||||
}
|
||||
|
||||
public set<K extends StaticConfigKeys>(key: K, value: StaticConfigKeyValues[K]): void {
|
||||
public async set<K extends StaticConfigKeys>(key: K, value: StaticConfigKeyValues[K]): Promise<void> {
|
||||
// Validate the setters
|
||||
switch (key) {
|
||||
case "proton-folder":
|
||||
this.validateProtonFolder(value as string);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this.store.set(key, value);
|
||||
|
||||
if (this.watchers[key]) {
|
||||
@@ -42,6 +56,16 @@ export class StaticConfigurationService {
|
||||
}
|
||||
}
|
||||
|
||||
// Setters with validation
|
||||
|
||||
private validateProtonFolder(protonFolder: string): void {
|
||||
const protonPath = path.join(protonFolder, PROTON_BINARY_PREFIX);
|
||||
const winePath = path.join(protonFolder, WINE_BINARY_PREFIX);
|
||||
if (!pathExistsSync(protonPath) || !pathExistsSync(winePath)) {
|
||||
throw new CustomError("Invalid proton folder path", "invalid-folder");
|
||||
}
|
||||
}
|
||||
|
||||
public delete<K extends StaticConfigKeys>(key: K): void {
|
||||
this.store.delete(key);
|
||||
}
|
||||
@@ -60,12 +84,15 @@ export class StaticConfigurationService {
|
||||
}
|
||||
|
||||
export interface StaticConfigKeyValues {
|
||||
"versions": BSVersion[];
|
||||
"installation-folder": string;
|
||||
"song-details-cache-etag": string;
|
||||
"disable-hadware-acceleration": boolean;
|
||||
"use-symlinks": boolean;
|
||||
}
|
||||
|
||||
// Linux Specific static configs
|
||||
"versions": BSVersion[];
|
||||
"proton-folder": string;
|
||||
};
|
||||
|
||||
export type StaticConfigKeys = keyof StaticConfigKeyValues;
|
||||
|
||||
@@ -77,5 +104,5 @@ export type StaticConfigGetIpcRequestResponse<K extends StaticConfigKeys> = {
|
||||
export type StaticConfigSetIpcRequest<K extends StaticConfigKeys> = {
|
||||
request: { key: K, value: StaticConfigKeyValues[K] };
|
||||
response: void;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -13,7 +13,9 @@ const { list } = (execOnOs({ win32: () => require("regedit-rs") }, true) ?? {})
|
||||
|
||||
export class SteamService {
|
||||
|
||||
private static readonly PROCESS_NAME = "steam";
|
||||
private static readonly PROCESS_NAME: string = process.platform === "linux"
|
||||
? "steam-runtime-launcher-service"
|
||||
: "steam";
|
||||
|
||||
private static instance: SteamService;
|
||||
|
||||
@@ -116,7 +118,7 @@ export class SteamService {
|
||||
|
||||
public async openSteam(): Promise<void> {
|
||||
|
||||
await shell.openPath("steam://open/games");
|
||||
await shell.openExternal("steam://open/games");
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// Every 3 seconds check if steam is running
|
||||
|
||||
@@ -35,7 +35,7 @@ export class SupportersService {
|
||||
}
|
||||
|
||||
private getRemoteSupporters(): Promise<Supporter[]> {
|
||||
return this.requestService.getJSON(this.PATREONS_URL);
|
||||
return this.requestService.getJSON<Supporter[]>(this.PATREONS_URL).then(res => res.data);
|
||||
}
|
||||
|
||||
private async getLocalSupporters(): Promise<Supporter[]> {
|
||||
|
||||
@@ -15,7 +15,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();
|
||||
@@ -71,7 +71,7 @@ export class BeatSaverApiService {
|
||||
}
|
||||
|
||||
const paramsHashs = hashs.join(",");
|
||||
const data = await this.request.getJSON<Record<Lowercase<T>, BsvMapDetail> | BsvMapDetail>(`${this.bsaverApiUrl}/maps/hash/${paramsHashs}`);
|
||||
const { data } = (await this.request.getJSON<Record<Lowercase<T>, BsvMapDetail> | BsvMapDetail>(`${this.bsaverApiUrl}/maps/hash/${paramsHashs}`));
|
||||
|
||||
if ((data as BsvMapDetail).id) {
|
||||
const key = (data as BsvMapDetail).versions.at(0).hash.toLowerCase();
|
||||
@@ -86,22 +86,22 @@ export class BeatSaverApiService {
|
||||
}
|
||||
|
||||
public async getMapDetailsById(id: string): Promise<BsvMapDetail> {
|
||||
return this.request.getJSON<BsvMapDetail>(`${this.bsaverApiUrl}/maps/id/${id}`);
|
||||
return (await this.request.getJSON<BsvMapDetail>(`${this.bsaverApiUrl}/maps/id/${id}`)).data;
|
||||
}
|
||||
|
||||
public searchMaps(search: SearchParams): Promise<SearchResponse> {
|
||||
const url = new URL(`${this.bsaverApiUrl}/search/text/${search?.page ?? 0}`);
|
||||
url.search = this.searchParamsToUrlParams(search).toString();
|
||||
return this.request.getJSON<SearchResponse>(url.toString());
|
||||
return this.request.getJSON<SearchResponse>(url.toString()).then(res => res.data);
|
||||
}
|
||||
|
||||
public searchPlaylists(search: PlaylistSearchParams): Promise<PlaylistSearchResponse> {
|
||||
const url = new URL(`${this.bsaverApiUrl}/playlists/search/${search?.page ?? 0}`);
|
||||
url.search = new URLSearchParams(this.objectToStringRecord(search)).toString();
|
||||
return this.request.getJSON<PlaylistSearchResponse>(url.toString());
|
||||
return this.request.getJSON<PlaylistSearchResponse>(url.toString()).then(res => res.data);
|
||||
}
|
||||
|
||||
public getPlaylistDetailsById(id: string, page = 0): Promise<BsvPlaylistPage> {
|
||||
return this.request.getJSON<BsvPlaylistPage>(`${this.bsaverApiUrl}/playlists/id/${id}/${page}`);
|
||||
return this.request.getJSON<BsvPlaylistPage>(`${this.bsaverApiUrl}/playlists/id/${id}/${page}`).then(res => res.data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,6 @@ export class ModelSaberApiService {
|
||||
const url = new URL(this.ENDPOINTS.get, this.API_URL);
|
||||
url.search = this.buildUrlQuery(query).toString();
|
||||
|
||||
return this.request.getJSON<MSGetResponse>(url.toString());
|
||||
return (await this.request.getJSON<MSGetResponse>(url.toString())).data;
|
||||
}
|
||||
}
|
||||
|
||||
+11
-12
@@ -1,17 +1,16 @@
|
||||
/* eslint import/prefer-default-export: off, import/no-mutable-exports: off */
|
||||
import { URL } from "url";
|
||||
import { pathToFileURL, URL } from "url";
|
||||
import path from "path";
|
||||
|
||||
export let resolveHtmlPath: (htmlFileName: string) => string;
|
||||
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
const port = process.env.PORT || 1212;
|
||||
resolveHtmlPath = (htmlFileName: string) => {
|
||||
export function resolveHtmlPath (htmlFileName: string) {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
const port = process.env.PORT || 1212;
|
||||
const url = new URL(`http://localhost:${port}/${htmlFileName}`);
|
||||
return url.toString();
|
||||
};
|
||||
} else {
|
||||
resolveHtmlPath = (htmlFileName: string) => {
|
||||
return `file://${path.resolve(__dirname, "../renderer/", htmlFileName)}`;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const filePath = path.resolve(__dirname, "..", "renderer");
|
||||
return pathToFileURL(filePath).toString().concat("/", htmlFileName);
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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, options: {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>
|
||||
);
|
||||
};
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
.content * {
|
||||
all: revert;
|
||||
}
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import { ChangelogVersion } from "renderer/services/auto-updater.service";
|
||||
import { LinkOpenerService } from "renderer/services/link-opener.service";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { ModalComponent } from "renderer/services/modale.service";
|
||||
import DOMPurify from 'dompurify';
|
||||
import './changelog-modal.component.css';
|
||||
import Tippy from "@tippyjs/react";
|
||||
|
||||
export const ChangelogModal: ModalComponent<void, ChangelogVersion> = ({ options: {data: changelog} }) => {
|
||||
|
||||
const linkOpener = useService(LinkOpenerService);
|
||||
const openGithub = () => linkOpener.open("https://github.com/Zagrios/bs-manager");
|
||||
const openTwitter = () => linkOpener.open("https://twitter.com/BSManager_");
|
||||
const openSupportPage = () => linkOpener.open("https://www.patreon.com/bsmanager");
|
||||
const openDiscord = () => linkOpener.open("https://discord.gg/uSqbHVpKdV");
|
||||
const openWebSite = () => linkOpener.open("https://bsmanager.io/");
|
||||
const date = changelog?.timestamp ? new Date(changelog.timestamp * 1000).toLocaleDateString() : '';
|
||||
|
||||
return (
|
||||
<form className="w-[350px] text-gray-800 dark:text-gray-200 h-[70vh] flex flex-col justify-between">
|
||||
<h1 className=" p-4 pt-1 text-3xl uppercase tracking-wide w-full text-center text-gray-800 dark:text-gray-200 font-bold">{changelog?.title}</h1>
|
||||
<div className=" overflow-y-scroll h-full content grow scrollbar-default" dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(changelog?.htmlBody) }}/>
|
||||
<span className="block w-[100%] mx-auto mt-0 mb-4 h-1 rounded-full bg-main-color-1" />
|
||||
<div className="flex flex-row justify-between">
|
||||
<div className="my-auto flex flex-col text-sm">
|
||||
<span>{date}</span>
|
||||
<span>v{changelog?.version}</span>
|
||||
</div>
|
||||
<div className="flex flex-row gap-2 my-auto">
|
||||
<Tippy content="Twitter" placement="top" className="font-bold !bg-neutral-900" duration={[200, 0]} arrow={false}>
|
||||
<div><BsmButton onClick={openTwitter} className="rounded-md p-1 w-7 h-7" icon="twitter" withBar={false} iconColor="#fff" color="#000"/></div>
|
||||
</Tippy>
|
||||
<Tippy content="Github" placement="top" className="font-bold !bg-neutral-900" duration={[200, 0]} arrow={false}>
|
||||
<div><BsmButton onClick={openGithub} className="rounded-md p-1 w-7 h-7" icon="github" withBar={false} iconColor="#fff" color="#000"/></div>
|
||||
</Tippy>
|
||||
<Tippy content="Discord" placement="top" className="font-bold !bg-neutral-900" duration={[200, 0]} arrow={false}>
|
||||
<div><BsmButton onClick={openDiscord} className="rounded-md p-1 w-7 h-7" icon="discord" withBar={false} iconColor="#fff" color="#000"/></div>
|
||||
</Tippy>
|
||||
<Tippy content="Patreon" placement="top" className="font-bold !bg-neutral-900" duration={[200, 0]} arrow={false}>
|
||||
<div><BsmButton onClick={openSupportPage} className="rounded-md p-1 w-7 h-7 " icon="patreon" withBar={false} iconColor="#fff" color="#000"/></div>
|
||||
</Tippy>
|
||||
<Tippy content="Web Site" placement="top" className="font-bold !bg-neutral-900" duration={[200, 0]} arrow={false}>
|
||||
<div><BsmButton onClick={openWebSite} className="rounded-md p-1 w-7 h-7 " icon="web-site" withBar={false} iconColor="#fff" color="#000"/></div>
|
||||
</Tippy>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -206,7 +206,7 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
|
||||
}}
|
||||
>
|
||||
<div className="flex h-9 gap-2 shrink-0">
|
||||
<BsmDropdownButton ref={filterContainerRef} className="shrink-0 h-full relative z-[1] flex justify-start" buttonClassName="flex items-center justify-center h-full rounded-full px-2 py-1 !bg-light-main-color-1 dark:!bg-main-color-1" icon="filter" text="pages.version-viewer.maps.search-bar.filters-btn" withBar={false}>
|
||||
<BsmDropdownButton ref={filterContainerRef} className="shrink-0 h-full relative z-[1] flex justify-start" buttonClassName="flex items-center justify-center h-full rounded-full px-2 py-1 !bg-light-main-color-1 dark:!bg-main-color-1" icon="filter" text="pages.version-viewer.maps.search-bar.filters-btn" textClassName="whitespace-nowrap" withBar={false}>
|
||||
<FilterPanel className="absolute top-[calc(100%+3px)] origin-top w-[450px] h-fit p-2 rounded-md shadow-md shadow-black" localData={false} filter={filter} onChange={setFilter} onApply={handleSearch} onClose={() => filterContainerRef.current.close()} />
|
||||
</BsmDropdownButton>
|
||||
<input className="h-full bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-2 grow pb-0.5" type="text" placeholder={t("pages.version-viewer.maps.search-bar.search-placeholder")} value={query} onChange={e => setQuery(e.target.value)} />
|
||||
@@ -214,6 +214,7 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
|
||||
className="shrink-0 rounded-full py-1 px-3 !bg-light-main-color-1 dark:!bg-main-color-1 flex justify-center items-center capitalize"
|
||||
icon="search"
|
||||
text="modals.download-maps.search-btn"
|
||||
textClassName="whitespace-nowrap"
|
||||
withBar={false}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -229,6 +229,7 @@ export const DownloadModelsModal: ModalComponent<void, { version: BSVersion; typ
|
||||
icon="search"
|
||||
type="submit"
|
||||
text="models.modals.download-models.search-btn"
|
||||
textClassName="whitespace-nowrap"
|
||||
withBar={false}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
|
||||
+2
-2
@@ -51,11 +51,11 @@ export function DownloadPlaylistModalHeader({ className, value, onSubmit }: Prop
|
||||
|
||||
return (
|
||||
<form className={cn('flex flex-row gap-2', className)} onSubmit={e => {e.preventDefault(); submit(searchParams);}}>
|
||||
<BsmDropdownButton ref={dropDownFilterRef} buttonClassName="flex items-center justify-center h-full rounded-full px-2 py-1 !bg-light-main-color-1 dark:!bg-main-color-1" icon="filter" text="pages.version-viewer.maps.search-bar.filters-btn" withBar={false}>
|
||||
<BsmDropdownButton ref={dropDownFilterRef} buttonClassName="flex items-center justify-center h-full rounded-full px-2 py-1 !bg-light-main-color-1 dark:!bg-main-color-1" icon="filter" text="pages.version-viewer.maps.search-bar.filters-btn" textClassName="whitespace-nowrap" withBar={false}>
|
||||
<DownloadPlaylistFilterPanel className="z-10 translate-y-1" params={filter} onSubmit={handleFilterSubmit}/>
|
||||
</BsmDropdownButton>
|
||||
<input className="h-full bg-theme-1 rounded-full px-2 grow pb-0.5" type="text" placeholder={t("playlist.search-playlist")} value={query} onChange={e => setQuery(e.target.value)} />
|
||||
<BsmButton className="shrink-0 rounded-full py-1 px-3 !bg-theme-1 flex justify-center items-center capitalize" icon="search" text="modals.download-maps.search-btn" withBar={false} onClick={() => submit(searchParams)} />
|
||||
<BsmButton className="shrink-0 rounded-full py-1 px-3 !bg-theme-1 flex justify-center items-center capitalize" icon="search" text="modals.download-maps.search-btn" textClassName="whitespace-nowrap" withBar={false} onClick={() => submit(searchParams)} />
|
||||
<BsmSelect className="bg-theme-1 rounded-full px-1 pb-0.5 text-center cursor-pointer" options={sortOptions} selected={order} onChange={handleOrderChange}/>
|
||||
</form>
|
||||
)
|
||||
|
||||
+1
-1
@@ -75,7 +75,7 @@ export const AskInstallPathModal: ModalComponent<{ installPath: string }, {}> =
|
||||
<BsmButton
|
||||
onClick={selectInstallPath}
|
||||
className="shrink-0 whitespace-nowrap mr-2 px-2 font-bold italic text-sm rounded-md"
|
||||
text="modals.ask-install-path.choose-folder"
|
||||
text="misc.choose-folder"
|
||||
withBar={false}
|
||||
/>
|
||||
</div>
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
import { lastValueFrom } from "rxjs";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
|
||||
import { IpcService } from "renderer/services/ipc.service";
|
||||
import { ModalComponent, ModalExitCode } from "renderer/services/modale.service";
|
||||
import { NotificationService } from "renderer/services/notification.service";
|
||||
import { StaticConfigurationService } from "renderer/services/static-configuration.service";
|
||||
|
||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
|
||||
export const ChooseProtonFolderModal: ModalComponent<{}, {}> = ({ resolver }) => {
|
||||
|
||||
const t = useTranslation();
|
||||
const ipcService = useService(IpcService);
|
||||
const notificationService = useService(NotificationService);
|
||||
const staticConfigService = useService(StaticConfigurationService);
|
||||
|
||||
const [protonFolder, setProtonFolder] = useState(null);
|
||||
|
||||
const selectProtonPath = async () => {
|
||||
const response = await lastValueFrom(ipcService.sendV2("choose-folder", {
|
||||
parent: "home",
|
||||
defaultPath: ".local/share/Steam/steamapps/common",
|
||||
showHidden: true,
|
||||
}));
|
||||
|
||||
if (response.canceled || !response.filePaths?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const path = response.filePaths[0];
|
||||
|
||||
await staticConfigService.set("proton-folder", path).then(() => {
|
||||
setProtonFolder(path);
|
||||
}).catch(err => {
|
||||
notificationService.notifyError({
|
||||
title: "pages.settings.proton-folder.errors.title",
|
||||
desc: ["invalid-folder"].includes(err?.code)
|
||||
? `pages.settings.proton-folder.errors.${err.code}`
|
||||
: "misc.unknown"
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const onConfirmButtonPressed = async () => {
|
||||
resolver({
|
||||
exitCode: ModalExitCode.COMPLETED
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
className="max-w-lg w-max flex flex-col gap-3"
|
||||
onSubmit={event => {
|
||||
event.preventDefault();
|
||||
onConfirmButtonPressed();
|
||||
}}>
|
||||
|
||||
<h1 className="tracking-wide w-full uppercase text-3xl text-center mb-4">{t("modals.choose-proton-folder.title")}</h1>
|
||||
|
||||
<p>{t("modals.choose-proton-folder.proton-folder-description")}</p>
|
||||
|
||||
<a className="underline" href="https://github.com/ValveSoftware/Proton/wiki/Proton-FAQ#where-is-proton-installed" target="_blank">{t("modals.choose-proton-folder.where-is-proton-installed")}</a>
|
||||
<div className="relative flex items-center justify-between w-full h-8 bg-light-main-color-1 dark:bg-main-color-1 rounded-md pl-2 py-1">
|
||||
{protonFolder ? (
|
||||
<span
|
||||
className="block text-ellipsis overflow-hidden min-w-0 whitespace-nowrap"
|
||||
title={protonFolder}
|
||||
>
|
||||
{protonFolder}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-gray-500 italic font-bold">{t("modals.choose-proton-folder.proton-folder-placeholder")}</span>
|
||||
)}
|
||||
<BsmButton
|
||||
onClick={selectProtonPath}
|
||||
className="shrink-0 whitespace-nowrap mr-2 px-2 font-bold italic text-sm rounded-md"
|
||||
text="misc.choose-folder"
|
||||
withBar={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="h-8 grid grid-flow-col grid-cols-1">
|
||||
<BsmButton
|
||||
typeColor="primary"
|
||||
className="rounded-md text-center transition-all"
|
||||
type="submit"
|
||||
withBar={false}
|
||||
text="misc.confirm"
|
||||
disabled={!protonFolder}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -47,7 +47,9 @@ export const ShareFoldersModal: ModalComponent<void, BSVersion> = ({ options: {d
|
||||
|
||||
const addFolder = async () => {
|
||||
const versionPath = await lastValueFrom(versionManager.getVersionPath(data));
|
||||
const folder = await lastValueFrom(ipc.sendV2("choose-folder", versionPath));
|
||||
const folder = await lastValueFrom(ipc.sendV2("choose-folder", {
|
||||
defaultPath: versionPath
|
||||
}));
|
||||
|
||||
if (!folder || folder.canceled || !folder.filePaths?.length) {
|
||||
return;
|
||||
|
||||
@@ -12,7 +12,6 @@ import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
|
||||
import { NotificationService } from "renderer/services/notification.service";
|
||||
import { ConfigurationService } from "renderer/services/configuration.service";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { lt } from "semver";
|
||||
import { lastValueFrom, of, take } from "rxjs";
|
||||
import { FolderLinkState } from "renderer/services/version-folder-linker.service";
|
||||
import { BsContentTabItemProps } from "../shared/bs-content-tab-panel/bs-content-tab-item.component";
|
||||
@@ -74,20 +73,6 @@ export function ModelsPanel({ version, isActive, goToMods }: { version?: BSVersi
|
||||
});
|
||||
}, [isActive]);
|
||||
|
||||
useOnUpdate(() => {
|
||||
if (!isActive || !version || lt(version.BSVersion, "1.29.4")) {
|
||||
return;
|
||||
}
|
||||
if (config.get("not-remind-models-breaks")) {
|
||||
return;
|
||||
}
|
||||
notification.notifyInfo({ title: "models.notifications.prevent-for-models-breaks.title", desc: "models.notifications.prevent-for-models-breaks.desc", actions: [{ id: "0", title: "models.notifications.prevent-for-mods.not-remind", cancel: true }], duration: 12_000 }).then(res => {
|
||||
if (res === "0") {
|
||||
config.set("not-remind-models-breaks", true);
|
||||
}
|
||||
});
|
||||
}, [isActive, version]);
|
||||
|
||||
const exportModels = () => {
|
||||
const selectedModels = modelsGridRefs.map(ref => ref.current?.getSelectedModels() as BsmLocalModel[]).flat();
|
||||
modelsManager.exportModels(selectedModels, version);
|
||||
@@ -154,6 +139,7 @@ export function ModelsPanel({ version, isActive, goToMods }: { version?: BSVersi
|
||||
icon="add"
|
||||
text="misc.add"
|
||||
typeColor="primary"
|
||||
textClassName="whitespace-nowrap"
|
||||
withBar={false}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -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} />}
|
||||
|
||||
@@ -90,7 +90,7 @@ export const BsmButton = forwardRef<unknown, Props>(({ className, style, iconSty
|
||||
{icon && <BsmIcon icon={icon} className={iconClassName ?? "size-full text-gray-800 dark:text-white"} style={{ ...(iconStyle ?? {}), color: (iconColor || textColor) }} />}
|
||||
{text &&
|
||||
(type === "submit" ? (
|
||||
<button type="submit" className={textClassName || "size-full"} style={{ ...(!!textColor && { color: textColor }) }}>
|
||||
<button type="submit" className={textClassName || "size-full"} style={{ ...(!!textColor && { color: textColor }) }} disabled={disabled}>
|
||||
{t(text)}
|
||||
</button>
|
||||
) : (
|
||||
|
||||
@@ -56,6 +56,7 @@ import { RussianIcon } from "./flags/russian-icon.component";
|
||||
import { ChineseIcon } from "./flags/chinese-icon.component";
|
||||
import { ChineseTraditionalIcon } from "./flags/chineseTraditional-icon.component";
|
||||
import { JapanIcon } from "./flags/japan-icon.component";
|
||||
import { KoreaIcon } from "./flags/korea-icon.component";
|
||||
import { ChevronTopIcon } from "./icons/chevron-top-icon.component";
|
||||
import { EyeCrossIcon } from "./icons/eye-cross-icon.component";
|
||||
import { ShortcutIcon } from "./icons/shortcut-icon.component";
|
||||
@@ -64,9 +65,11 @@ import { SongDetailDiffCharactertistic } from "shared/models/maps/song-details-c
|
||||
import { CleanIcon } from "./icons/clean-icon.component";
|
||||
import { BrowseIcon } from "./icons/browse-icon.component";
|
||||
import { AddFileIcon } from "./icons/add-file-icon.component";
|
||||
import { CancelIcon } from "./icons/cancel-icon.component";
|
||||
import { WarningIcon } from "./icons/warning-icon.component";
|
||||
|
||||
|
||||
export type BsmIconType = SongDetailDiffCharactertistic | ("settings" | "trash" | "favorite" | "folder" | "bsNote" | "check" | "three-dots" | "twitch" | "eye" | "play" | "checkCircleIcon" | "discord" | "info" | "eye-cross" | "terminal" | "desktop" | "oculus" | "add" | "cross" | "task" | "github" | "close" | "thumbUpFill" | "timerFill" | "pause" | "twitter" | "sync" | "chevron-top" | "copy" | "steam" | "edit" | "export" | "patreon" | "search" | "bsMapDifficulty" | "link" | "unlink" | "download" | "filter" | "mee6" | "volume-up" | "volume-off" | "volume-down" | "shortcut" | "backup-restore" | "web-site" | "clean" | "browse" | "add-file" | "fr-FR-flag" | "es-ES-flag" | "en-US-flag" | "en-EN-flag" | "de-DE-flag" | "ru-RU-flag" | "zh-CN-flag" | "zh-TW-flag" | "ja-JP-flag");
|
||||
export type BsmIconType = SongDetailDiffCharactertistic | ("settings" | "trash" | "favorite" | "folder" | "bsNote" | "check" | "three-dots" | "twitch" | "eye" | "play" | "checkCircleIcon" | "discord" | "info" | "eye-cross" | "terminal" | "desktop" | "oculus" | "add" | "cross" | "task" | "github" | "close" | "thumbUpFill" | "timerFill" | "pause" | "twitter" | "sync" | "chevron-top" | "copy" | "steam" | "edit" | "export" | "patreon" | "search" | "bsMapDifficulty" | "link" | "unlink" | "download" | "filter" | "mee6" | "volume-up" | "volume-off" | "volume-down" | "shortcut" | "backup-restore" | "web-site" | "clean" | "browse" | "add-file" | "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" | "ko-KR-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
|
||||
@@ -130,6 +133,9 @@ export const BsmIcon = memo(({ className, icon, style }: { className?: string; i
|
||||
if (icon === "ja-JP-flag") {
|
||||
return <JapanIcon className={className} style={style} />;
|
||||
}
|
||||
if (icon === "ko-KR-flag"){
|
||||
return <KoreaIcon className={className} style={style} />;
|
||||
}
|
||||
if (icon === "task") {
|
||||
return <TaskIcon className={className} style={style} />;
|
||||
}
|
||||
@@ -272,6 +278,14 @@ export const BsmIcon = memo(({ className, icon, style }: { className?: string; i
|
||||
return <AddFileIcon 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,14 @@
|
||||
import { createSvgIcon } from "../svg-icon.type";
|
||||
|
||||
export const KoreaIcon = createSvgIcon((props, ref) => {
|
||||
return (
|
||||
<svg ref={ref} {...props} width="71" height="48" viewBox="0 0 71 48" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none">
|
||||
<rect fill="#FFF" x=".01" y=".1" width="100%" height="100%" rx="6.64"/>
|
||||
<path d="M10.39 14.36 16.9 4.6l1.63 1.08-6.51 9.76zm40.69-8.68 1.63-1.08 3 4.47-1.63 1.08zM12.83 16a8272 8272 0 0 1 6.51-9.76l1.62 1.08-6.51 9.76zm35.8-8.67c.53-.39 1.09-.73 1.63-1.1q3.25 4.88 6.51 9.76l-1.63 1.08a868 868 0 0 0-6.51-9.74m-26.85.52 1.63 1.08-6.51 9.76-1.63-1.08zm24.41 1.09 1.63-1.08 3 4.48-1.62 1.08c-1.02-1.5-2.01-2.99-3.01-4.48M54.6 11c.54-.36 1.07-.72 1.61-1.07l3 4.45-1.63 1.08zm-4.88 3.22 1.63-1.08 3 4.47-1.63 1.09zM15.27 29.55l1.63-1.08 6.51 9.76-1.63 1.09zm37.43-1.09 1.63 1.08-3 4.47-1.63-1.08zm-39.87 2.71 1.63-1.08 3 4.47-1.63 1.08zm42.31-1.08 1.63 1.08-3 4.47-1.63-1.09zM10.39 32.8l1.63-1.08 6.51 9.76-1.63 1.09zm47.19-1.08c.55.35 1.09.72 1.63 1.08l-3 4.47-1.63-1.09zm-8.4 2.03 1.63 1.08-3 4.47-1.63-1.08zm-32.82 2.72 1.62-1.09c1 1.48 2 3 3 4.47l-1.63 1.09zm32.27 3.38c1-1.49 2-3 3-4.47.54.37 1.09.72 1.62 1.09l-3 4.47zM54.05 37c.55.35 1.09.72 1.63 1.08l-3 4.47-1.63-1.09z" fill="#434A54"/>
|
||||
<path d="M28.86 13.46a11.73 11.73 0 0 1 17.67 9.75 11.65 11.65 0 0 1-1.74 6.53 5.87 5.87 0 0 0-7.74-8.15 5.85 5.85 0 0 0-2.24 2 5.87 5.87 0 0 1-10.76-3.32 6.3 6.3 0 0 1 .61-2.57 11.65 11.65 0 0 1 4.2-4.24" fill="#EC5565"/>
|
||||
<path d="M23.07 23.2a11.5 11.5 0 0 1 1.6-5.53 6.3 6.3 0 0 0-.61 2.57 5.87 5.87 0 0 0 10.76 3.32 5.87 5.87 0 0 1 10.73 3 5.93 5.93 0 0 1-.76 3.22 11.6 11.6 0 0 1-5.27 4.59 11.74 11.74 0 0 1-16.45-11.13z" fill="#4658A9"/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
});
|
||||
@@ -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,7 +1,7 @@
|
||||
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";
|
||||
@@ -15,15 +15,20 @@ import { BsDownloaderService } from "renderer/services/bs-version-download/bs-do
|
||||
import equal from "fast-deep-equal";
|
||||
import { GlowEffect } from "renderer/components/shared/glow-effect.component";
|
||||
import { cn } from "renderer/helpers/css-class.helpers";
|
||||
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";
|
||||
|
||||
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));
|
||||
@@ -66,17 +71,29 @@ export function LaunchSlide({ version }: Props) {
|
||||
desktop: desktopMode,
|
||||
debug: debugMode,
|
||||
additionalArgs: advancedLaunch ? additionalArgs : [],
|
||||
protonPath: bsLauncherService.getProtonPath(),
|
||||
});
|
||||
|
||||
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" />}
|
||||
@@ -112,3 +129,5 @@ export function LaunchSlide({ version }: Props) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -16,9 +16,10 @@ type Props = {
|
||||
disabled?: boolean;
|
||||
uninstallMod?: (mods: Mod) => void;
|
||||
uninstallAllMods?: () => void;
|
||||
unselectAllMods?: () => void;
|
||||
};
|
||||
|
||||
export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreInfoMod, onWantInfos, disabled, uninstallMod, uninstallAllMods }: Props) {
|
||||
export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreInfoMod, onWantInfos, disabled, uninstallMod, uninstallAllMods, unselectAllMods }: Props) {
|
||||
|
||||
const [filter, setFilter] = useState("");
|
||||
const [filterEnabled, setFilterEnabled] = useState(false);
|
||||
@@ -61,12 +62,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 rounded-tl-md">
|
||||
<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: () => uninstallAllMods?.() }]} />
|
||||
<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: () => uninstallAllMods?.() }
|
||||
]} />
|
||||
</span>
|
||||
|
||||
{Array.from(modsMap.keys()).map(
|
||||
|
||||
@@ -133,6 +133,10 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
|
||||
})
|
||||
};
|
||||
|
||||
const unselectAllMods = () => {
|
||||
setModsSelected(() => []);
|
||||
}
|
||||
|
||||
const loadMods = (): Promise<void> => {
|
||||
if (os.isOffline) {
|
||||
return Promise.resolve();
|
||||
@@ -209,7 +213,18 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
|
||||
return (
|
||||
<>
|
||||
<div className="grow overflow-y-scroll w-full min-h-0 scrollbar-default p-0 m-0">
|
||||
<ModsGrid modsMap={modsAvailable} installed={modsInstalled} modsSelected={modsSelected} onModChange={handleModChange} moreInfoMod={moreInfoMod} onWantInfos={handleMoreInfo} disabled={uninstalling || installing} uninstallMod={uninstallMod} uninstallAllMods={uninstallAllMods}/>
|
||||
<ModsGrid
|
||||
modsMap={modsAvailable}
|
||||
installed={modsInstalled}
|
||||
modsSelected={modsSelected}
|
||||
onModChange={handleModChange}
|
||||
moreInfoMod={moreInfoMod}
|
||||
onWantInfos={handleMoreInfo}
|
||||
disabled={uninstalling || installing}
|
||||
uninstallMod={uninstallMod}
|
||||
uninstallAllMods={uninstallAllMods}
|
||||
unselectAllMods={unselectAllMods}
|
||||
/>
|
||||
</div>
|
||||
<div className="shrink-0 flex items-center justify-between px-3 py-2">
|
||||
<BsmButton className="flex items-center justify-center rounded-md px-1 h-8" text="pages.version-viewer.mods.buttons.more-infos" typeColor="cancel" withBar={false} disabled={!moreInfoMod} onClick={handleOpenMoreInfo} style={{ width: downloadWith }}/>
|
||||
|
||||
@@ -23,14 +23,14 @@ export const defaultConfiguration: {
|
||||
"second-color": "#ff4444",
|
||||
theme: "os",
|
||||
language: window.navigator.language.length <= 2 ? `${window.navigator.language}-${window.navigator.language.toLocaleUpperCase()}` : window.navigator.language,
|
||||
supported_languages: ["en-US", "en-EN", "fr-FR", "es-ES", "de-DE", "ru-RU", "zh-CN", "zh-TW", "ja-JP"],
|
||||
supported_languages: ["en-US", "en-EN", "fr-FR", "es-ES", "de-DE", "ru-RU", "zh-CN", "zh-TW", "ja-JP", "ko-KR"],
|
||||
default_mods: ["SongCore", "WhyIsThereNoLeaderboard", "BeatSaverDownloader", "BeatSaverVoting", "PlaylistManager"],
|
||||
"default-shared-folders": [
|
||||
window.electron.path.join("Beat Saber_Data", "CustomLevels"),
|
||||
window.electron.path.join("Beat Saber_Data", "CustomWIPLevels"),
|
||||
],
|
||||
"playlist-sort-order": BsvSearchOrder.Relevance,
|
||||
"map-sort-order": BsvSearchOrder.Latest,
|
||||
"map-sort-order": BsvSearchOrder.Relevance,
|
||||
};
|
||||
|
||||
export type ThemeConfig = "dark" | "light" | "os";
|
||||
|
||||
@@ -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);
|
||||
@@ -15,3 +20,35 @@ export function useTranslation(): (translationKey: string, args?: Record<string,
|
||||
return i18nService.translate(key, args);
|
||||
};
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@ 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);
|
||||
|
||||
@@ -19,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]);
|
||||
@@ -26,9 +32,22 @@ 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)
|
||||
const isVersionOutdated = safeLt(version.BSVersion, recommendedVersion?.BSVersion);
|
||||
|
||||
if (showOutdated && isVersionOutdated) {
|
||||
|
||||
const res = await modals.openModal(BSVersionOutdatedModal, {data: { outdated: version, recommended: recommendedVersion }});
|
||||
|
||||
if (res.exitCode !== ModalExitCode.COMPLETED) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return bsDownloader.downloadVersion(version)
|
||||
.catch(logRenderError)
|
||||
.finally(() => setSelectedVersion(null));
|
||||
};
|
||||
@@ -56,7 +75,7 @@ export function AvailableVersionsList() {
|
||||
|
||||
<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>
|
||||
)}
|
||||
|
||||
@@ -39,16 +39,13 @@ import { BsStore } from "shared/models/bs-store.enum";
|
||||
import { SteamIcon } from "renderer/components/svgs/icons/steam-icon.component";
|
||||
import { OculusIcon } from "renderer/components/svgs/icons/oculus-icon.component";
|
||||
import { BsDownloaderService } from "renderer/services/bs-version-download/bs-downloader.service";
|
||||
import { AutoUpdaterService } from "renderer/services/auto-updater.service";
|
||||
import BeatWaitingImg from "../../../assets/images/apngs/beat-waiting.png";
|
||||
import BeatConflict from "../../../assets/images/apngs/beat-conflict.png";
|
||||
import { logRenderError } from "renderer";
|
||||
import { BSLauncherService } from "renderer/services/bs-launcher.service";
|
||||
import { SettingToogleSwitchGrid } from "renderer/components/settings/setting-toogle-switch-grid.component";
|
||||
import { BasicModal } from "renderer/components/modal/basic-modal.component";
|
||||
import { StaticConfigurationService } from "renderer/services/static-configuration.service";
|
||||
import { tryit } from "shared/helpers/error.helpers";
|
||||
import { InstallationLocationService } from "renderer/services/installation-location.service";
|
||||
import { AutoUpdaterService } from "renderer/services/auto-updater.service";
|
||||
|
||||
export function SettingsPage() {
|
||||
|
||||
@@ -57,7 +54,6 @@ export function SettingsPage() {
|
||||
const ipcService = useService(IpcService);
|
||||
const modalService = useService(ModalService);
|
||||
const bsDownloader = useService(BsDownloaderService);
|
||||
const bsLauncher = useService(BSLauncherService);
|
||||
const steamDownloader = useService(SteamDownloaderService);
|
||||
const progressBarService = useService(ProgressBarService);
|
||||
const notificationService = useService(NotificationService);
|
||||
@@ -67,9 +63,9 @@ export function SettingsPage() {
|
||||
const playlistsManager = useService(PlaylistsManagerService);
|
||||
const modelsManager = useService(ModelsManagerService);
|
||||
const versionLinker = useService(VersionFolderLinkerService);
|
||||
const autoUpdater = useService(AutoUpdaterService);
|
||||
const staticConfig = useService(StaticConfigurationService);
|
||||
const installationLocationService = useService(InstallationLocationService);
|
||||
const autoUpdater = useService(AutoUpdaterService);
|
||||
|
||||
const { firstColor, secondColor } = useThemeColor();
|
||||
|
||||
@@ -94,7 +90,7 @@ export function SettingsPage() {
|
||||
const downloadStore = useObservable(() => bsDownloader.defaultStore$);
|
||||
|
||||
const [installationFolder, setInstallationFolder] = useState(null);
|
||||
const [protonPath, setProtonPath] = useState(bsLauncher.getProtonPath());
|
||||
const [protonFolder, setProtonFolder] = useState("");
|
||||
const [showSupporters, setShowSupporters] = useState(false);
|
||||
const [mapDeepLinksEnabled, setMapDeepLinksEnabled] = useState(false);
|
||||
const [playlistsDeepLinkEnabled, setPlaylistsDeepLinkEnabled] = useState(false);
|
||||
@@ -102,10 +98,7 @@ export function SettingsPage() {
|
||||
const [hasDownloaderSession, setHasDownloaderSession] = useState(false);
|
||||
const [hardwareAccelerationEnabled, setHardwareAccelerationEnabled] = useState(true);
|
||||
const [useSymlink, setUseSymlink] = useState(false);
|
||||
const appVersion = useObservable(() => ipcService.sendV2("current-version"));
|
||||
|
||||
const [isChangelogAvailable, setIsChangelogAvailable] = useState(true);
|
||||
const [changlogsLoading, setChanglogsLoading] = useState(false);
|
||||
const appVersion = useObservable(() => autoUpdater.getAppVersion());
|
||||
|
||||
useEffect(() => {
|
||||
loadInstallationFolder();
|
||||
@@ -116,6 +109,7 @@ export function SettingsPage() {
|
||||
|
||||
staticConfig.get("disable-hadware-acceleration").then(disabled =>setHardwareAccelerationEnabled(() => disabled !== true));
|
||||
staticConfig.get("use-symlinks").then(useSymlinks => setUseSymlink(() => useSymlinks));
|
||||
staticConfig.get("proton-folder").then(setProtonFolder);
|
||||
}, []);
|
||||
|
||||
const allDeepLinkEnabled = mapDeepLinksEnabled && playlistsDeepLinkEnabled && modelsDeepLinkEnabled;
|
||||
@@ -162,36 +156,36 @@ export function SettingsPage() {
|
||||
i18nService.setLanguage(item.value);
|
||||
};
|
||||
|
||||
const handleVersionClick = async () => {
|
||||
let isChangelogResolved = false;
|
||||
const timeoutId = setTimeout(() => setChanglogsLoading(() => !isChangelogResolved), 100);
|
||||
|
||||
await autoUpdater.showChangelog(await lastValueFrom(autoUpdater.getAppVersion()))
|
||||
.then(() => {
|
||||
setIsChangelogAvailable(() => true);
|
||||
})
|
||||
.catch(err => {
|
||||
logRenderError(err);
|
||||
setIsChangelogAvailable(() => false);
|
||||
})
|
||||
.finally(() => { isChangelogResolved = true; });
|
||||
|
||||
setChanglogsLoading(() => false);
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
|
||||
const setDefaultProtonPath = () => {
|
||||
const setDefaultProtonFolder = async () => {
|
||||
if (!progressBarService.require()) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastValueFrom(ipcService.sendV2("choose-file")).then(res => {
|
||||
if (!res.canceled && res.filePaths?.length) {
|
||||
const protonPath = res.filePaths[0];
|
||||
setProtonPath(protonPath);
|
||||
bsLauncher.setProtonPath(protonPath);
|
||||
try {
|
||||
const pathResponse = await lastValueFrom(ipcService.sendV2("choose-folder", {
|
||||
parent: "home",
|
||||
defaultPath: ".local/share/Steam/steamapps/common",
|
||||
showHidden: true,
|
||||
}));
|
||||
if (
|
||||
pathResponse.canceled
|
||||
|| !pathResponse.filePaths
|
||||
|| pathResponse.filePaths.length === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
const folder = pathResponse.filePaths[0];
|
||||
await staticConfig.set("proton-folder", folder);
|
||||
setProtonFolder(folder);
|
||||
} catch (error: any) {
|
||||
notificationService.notifyError({
|
||||
title: "pages.settings.proton-folder.errors.title",
|
||||
desc: ["invalid-folder"].includes(error?.code)
|
||||
? `pages.settings.proton-folder.errors.${error.code}`
|
||||
: "misc.unknown",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const setDefaultInstallationFolder = () => {
|
||||
@@ -390,16 +384,16 @@ export function SettingsPage() {
|
||||
<span className="block text-ellipsis overflow-hidden min-w-0" title={installationFolder}>
|
||||
{installationFolder}
|
||||
</span>
|
||||
<BsmButton onClick={setDefaultInstallationFolder} className="shrink-1 whitespace-nowrap mr-2 px-2 font-bold italic text-sm rounded-md" text="pages.settings.installation-folder.choose-folder" withBar={false} />
|
||||
<BsmButton onClick={setDefaultInstallationFolder} className="shrink-1 whitespace-nowrap mr-2 px-2 font-bold italic text-sm rounded-md" text="misc.choose-folder" withBar={false} />
|
||||
</div>
|
||||
</SettingContainer>
|
||||
|
||||
<SettingContainer os="linux" title="pages.settings.proton-path.title" description="pages.settings.proton-path.description">
|
||||
<SettingContainer os="linux" title="pages.settings.proton-folder.title" description="pages.settings.proton-folder.description">
|
||||
<div className="relative flex items-center justify-between w-full h-8 bg-light-main-color-1 dark:bg-main-color-1 rounded-md pl-2 py-1">
|
||||
<span className="block text-ellipsis overflow-hidden min-w-0 whitespace-nowrap" title={protonPath}>
|
||||
{protonPath}
|
||||
<span className="block text-ellipsis overflow-hidden min-w-0 whitespace-nowrap" title={protonFolder}>
|
||||
{protonFolder}
|
||||
</span>
|
||||
<BsmButton onClick={setDefaultProtonPath} className="shrink-0 whitespace-nowrap mr-2 px-2 font-bold italic text-sm rounded-md" text="pages.settings.proton-path.choose-file" withBar={false} />
|
||||
<BsmButton onClick={setDefaultProtonFolder} className="shrink-0 whitespace-nowrap mr-2 px-2 font-bold italic text-sm rounded-md" text="misc.choose-folder" withBar={false} />
|
||||
</div>
|
||||
</SettingContainer>
|
||||
|
||||
@@ -411,7 +405,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}
|
||||
@@ -421,7 +415,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}
|
||||
@@ -431,7 +425,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}
|
||||
@@ -441,7 +435,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}
|
||||
@@ -451,7 +445,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}
|
||||
@@ -470,7 +464,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}
|
||||
@@ -480,7 +474,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}
|
||||
@@ -490,7 +484,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}
|
||||
@@ -500,7 +494,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}
|
||||
@@ -517,8 +511,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}
|
||||
@@ -528,6 +522,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()}>
|
||||
@@ -536,7 +540,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}
|
||||
@@ -588,12 +592,7 @@ export function SettingsPage() {
|
||||
]}/>
|
||||
</SettingContainer>
|
||||
|
||||
<Tippy content={isChangelogAvailable ? t("pages.settings.changelogs.open") : t("pages.settings.changelogs.not-founds")} placement="left" className="font-bold bg-main-color-3">
|
||||
<div className="!bg-light-main-color-1 dark:!bg-main-color-1 rounded-md py-1 px-2 font-bold float-right mb-5 hover:brightness-125 h-auto w-auto">
|
||||
<BsmButton onClick={handleVersionClick} text={`v${appVersion}`} withBar={false} typeColor="none"/>
|
||||
</div>
|
||||
</Tippy>
|
||||
<BsmImage className={`h-7 my-auto spin-loading float-right ${changlogsLoading ? "" : "hidden"}`} image={BeatWaitingImg} loading="eager" />
|
||||
<span className="bg-light-main-color-1 dark:bg-main-color-1 rounded-md py-1 px-2 font-bold float-right mb-5">v{appVersion}</span>
|
||||
</div>
|
||||
<SupportersView isVisible={showSupporters} setVisible={setShowSupporters} />
|
||||
</div>
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { IpcService } from "./ipc.service";
|
||||
import { ProgressBarService } from "./progress-bar.service";
|
||||
import { ModalService } from "renderer/services/modale.service";
|
||||
import { ChangelogModal } from "renderer/components/modal/modal-types/chabgelog-modal/changelog-modal.component";
|
||||
import { ConfigurationService } from "./configuration.service";
|
||||
import { Observable, lastValueFrom } from "rxjs";
|
||||
import { Progression } from "main/helpers/fs.helpers";
|
||||
|
||||
@@ -22,12 +19,6 @@ export class AutoUpdaterService {
|
||||
private progressService: ProgressBarService;
|
||||
private ipcService: IpcService;
|
||||
|
||||
private modal: ModalService;
|
||||
|
||||
private configurationService: ConfigurationService;
|
||||
|
||||
private cacheChangelog: Changelog;
|
||||
|
||||
public static getInstance(): AutoUpdaterService {
|
||||
if (!AutoUpdaterService.instance) {
|
||||
AutoUpdaterService.instance = new AutoUpdaterService();
|
||||
@@ -38,8 +29,6 @@ export class AutoUpdaterService {
|
||||
private constructor() {
|
||||
this.progressService = ProgressBarService.getInstance();
|
||||
this.ipcService = IpcService.getInstance();
|
||||
this.modal = ModalService.getInstance();
|
||||
this.configurationService = ConfigurationService.getInstance();
|
||||
}
|
||||
|
||||
public isUpdateAvailable(): Promise<boolean> {
|
||||
@@ -64,56 +53,8 @@ export class AutoUpdaterService {
|
||||
return lastValueFrom(this.ipcService.sendV2("install-update"));
|
||||
}
|
||||
|
||||
public getLastAppVersion(): string {
|
||||
return this.configurationService.get("last-app-version");
|
||||
}
|
||||
|
||||
public setLastAppVersion(value : string){
|
||||
this.configurationService.set("last-app-version", value);
|
||||
}
|
||||
|
||||
private async getChangelog(): Promise<Changelog> {
|
||||
if (this.cacheChangelog) {
|
||||
return this.cacheChangelog;
|
||||
}
|
||||
|
||||
const path = `https://raw.githubusercontent.com/Zagrios/bs-manager/master/assets/jsons/changelogs.json`
|
||||
const response = await fetch(path);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch changelogs (${response.status})`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!data) {
|
||||
throw new Error(`Failed to parse changelogs`);
|
||||
}
|
||||
|
||||
this.cacheChangelog = data;
|
||||
return data;
|
||||
}
|
||||
|
||||
private async getChangelogVersion(version:string): Promise<ChangelogVersion> {
|
||||
const changelogs = await this.getChangelog();
|
||||
|
||||
const changelogVersion = changelogs[version];
|
||||
if (!changelogVersion) {
|
||||
throw new Error(`No changelog found for this version (${version})`);
|
||||
}
|
||||
|
||||
return changelogVersion;
|
||||
}
|
||||
|
||||
public getAppVersion() : Observable<string> {
|
||||
return this.ipcService.sendV2("current-version");
|
||||
}
|
||||
|
||||
public async showChangelog(version:string): Promise<void>{
|
||||
if (version.toLowerCase().includes("alpha")) {
|
||||
throw new Error("Alpha version, skipping changelog");
|
||||
}
|
||||
|
||||
const changelog = await this.getChangelogVersion(version);
|
||||
|
||||
this.modal.openModal(ChangelogModal, {data: changelog});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,6 @@ export class BSLauncherService {
|
||||
|
||||
public readonly versionRunning$: BehaviorSubject<BSVersion> = new BehaviorSubject(null);
|
||||
|
||||
private readonly PROTON_PATH_KEY = "protonPath";
|
||||
|
||||
public static getInstance(){
|
||||
if(!BSLauncherService.instance){ BSLauncherService.instance = new BSLauncherService(); }
|
||||
return BSLauncherService.instance;
|
||||
@@ -38,14 +36,6 @@ export class BSLauncherService {
|
||||
this.modals = ModalService.getInstance();
|
||||
}
|
||||
|
||||
public setProtonPath(protonPath: string|undefined): void {
|
||||
this.config.set(this.PROTON_PATH_KEY, protonPath);
|
||||
}
|
||||
|
||||
public getProtonPath(): string|undefined {
|
||||
return this.config.get<string>(this.PROTON_PATH_KEY);
|
||||
}
|
||||
|
||||
private notRewindBackupOculus(): boolean{
|
||||
return this.config.get<boolean>("not-rewind-backup-oculus");
|
||||
}
|
||||
@@ -93,7 +83,6 @@ export class BSLauncherService {
|
||||
}
|
||||
|
||||
public doLaunch(launchOptions: LaunchOption): Observable<BSLaunchEventData>{
|
||||
launchOptions.protonPath = this.getProtonPath();
|
||||
return this.ipcService.sendV2("bs-launch.launch", launchOptions);
|
||||
}
|
||||
|
||||
|
||||
@@ -211,4 +211,8 @@ export class BSVersionManagerService {
|
||||
version
|
||||
]))).values());
|
||||
}
|
||||
|
||||
public getRecommendedVersion(): BSVersion | undefined {
|
||||
return (this.availableVersions$.value ?? []).find(v => v.recommended);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -90,4 +91,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ import { InstallationLocationService } from "./installation-location.service";
|
||||
import { IpcService } from "./ipc.service";
|
||||
import { ModalService } from "./modale.service";
|
||||
|
||||
import { AskInstallPathModal } from "renderer/components/modal/modal-types/ask-install-path.component";
|
||||
import { AskInstallPathModal } from "renderer/components/modal/modal-types/setup/ask-install-path.component";
|
||||
import { ChooseProtonFolderModal } from "renderer/components/modal/modal-types/setup/choose-proton-folder-modal.component";
|
||||
|
||||
// Handle setup modals/prompts, ordering of the modals/prompts may be done here
|
||||
export class SetupService {
|
||||
@@ -35,6 +36,10 @@ export class SetupService {
|
||||
try {
|
||||
// NOTE: for modal sequencing
|
||||
await this.checkInstallationPath();
|
||||
|
||||
if (window.electron.platform === "linux") {
|
||||
await this.checkProtonFolder();
|
||||
}
|
||||
} catch (error) {
|
||||
logRenderError(error);
|
||||
}
|
||||
@@ -61,4 +66,20 @@ export class SetupService {
|
||||
}
|
||||
}
|
||||
|
||||
private async checkProtonFolder(): Promise<void> {
|
||||
try {
|
||||
const valid = await lastValueFrom(this.ipcService.sendV2("linux.verify-proton-folder"));
|
||||
if (valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.modalService.openModal(
|
||||
ChooseProtonFolderModal,
|
||||
{ closable: false }
|
||||
);
|
||||
} catch (error) {
|
||||
logRenderError(error);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -18,16 +18,14 @@ import { MapsManagerService } from "renderer/services/maps-manager.service";
|
||||
import { PlaylistsManagerService } from "renderer/services/playlists-manager.service";
|
||||
import { ModelsManagerService } from "renderer/services/models-management/models-manager.service";
|
||||
import { NotificationService } from "renderer/services/notification.service";
|
||||
import { lastValueFrom, timer } from "rxjs";
|
||||
import { timer } from "rxjs";
|
||||
import { ConfigurationService } from "renderer/services/configuration.service";
|
||||
import { OsDiagnosticService } from "renderer/services/os-diagnostic.service";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { AutoUpdaterService } from "renderer/services/auto-updater.service";
|
||||
import { SetupService } from "renderer/services/setup.service";
|
||||
import { gt, parse } from "semver"
|
||||
import { logRenderError } from "renderer";
|
||||
|
||||
export default function App() {
|
||||
|
||||
useService(OsDiagnosticService);
|
||||
const pageState = useService(PageStateService);
|
||||
const maps = useService(MapsManagerService);
|
||||
@@ -35,43 +33,18 @@ export default function App() {
|
||||
const models = useService(ModelsManagerService);
|
||||
const notification = useService(NotificationService);
|
||||
const config = useService(ConfigurationService);
|
||||
const autoUpdater = useService(AutoUpdaterService);
|
||||
const setup = useService(SetupService);
|
||||
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
checkIsUpdated();
|
||||
setup.check()
|
||||
.then(() => {
|
||||
checkOneClicks();
|
||||
})
|
||||
}, []);
|
||||
|
||||
const checkIsUpdated = async () => {
|
||||
const appVersion = await lastValueFrom(autoUpdater.getAppVersion());
|
||||
const lastAppVersion = autoUpdater.getLastAppVersion();
|
||||
|
||||
if(!lastAppVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastAppVersion.toLowerCase().includes("alpha")) {
|
||||
autoUpdater.setLastAppVersion("0"); // Reset last app version if it's an alpha version for users with alpha versions in the var stored.
|
||||
}
|
||||
|
||||
if (appVersion.toLowerCase().includes("alpha")) {
|
||||
return;
|
||||
}
|
||||
|
||||
autoUpdater.setLastAppVersion(appVersion);
|
||||
|
||||
if (parse(lastAppVersion) && gt(appVersion, lastAppVersion)) {
|
||||
await autoUpdater.showChangelog(appVersion).catch(logRenderError);
|
||||
}
|
||||
};
|
||||
|
||||
const checkOneClicks = async () => {
|
||||
|
||||
if (config.get("not-remind-oneclick") === true) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -11,3 +11,8 @@ export function findHashInString(str: string, algorithm: keyof typeof HashAlgori
|
||||
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,7 +15,9 @@ export enum BSLaunchError{
|
||||
BS_EXIT_ERROR = "EXIT",
|
||||
OCULUS_LIB_NOT_FOUND = "OCULUS_LIB_NOT_FOUND",
|
||||
PROTON_NOT_SET = "PROTON_NOT_SET",
|
||||
PROTON_NOT_FOUND = "PROTON_NOT_FOUND",
|
||||
UNKNOWN_ERROR = "UNKNOWN_ERROR",
|
||||
ORIGINAL_OCULUS_NOT_INSTALLED = "ORIGINAL_OCULUS_NOT_INSTALLED"
|
||||
}
|
||||
|
||||
export enum BSLaunchEvent{
|
||||
|
||||
@@ -7,5 +7,4 @@ export interface LaunchOption {
|
||||
debug?: boolean,
|
||||
additionalArgs?: string[],
|
||||
admin?: boolean,
|
||||
protonPath?: string,
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ export interface IpcChannelMapping {
|
||||
/* ** os-controls-ipcs ** */
|
||||
"new-window": { request: string, response: void };
|
||||
"open-dialog": { request: OpenDialogOptions, response: OpenDialogReturnValue };
|
||||
"choose-folder": { request: string, response: OpenDialogReturnValue };
|
||||
"choose-folder": { request: { defaultPath?: string, parent?: "home", showHidden?: boolean }, response: OpenDialogReturnValue };
|
||||
"choose-file": { request: string, response: OpenDialogReturnValue }
|
||||
"choose-image": { request: { multiple?: boolean, base64?: boolean }, response: string[] }
|
||||
"window.progression": { request: number, response: void };
|
||||
@@ -150,6 +150,9 @@ export interface IpcChannelMapping {
|
||||
"static-configuration.get": StaticConfigGetIpcRequestResponse<StaticConfigKeys>;
|
||||
"static-configuration.set": StaticConfigSetIpcRequest<StaticConfigKeys>;
|
||||
|
||||
/* ** linux.ipcs ** */
|
||||
"linux.verify-proton-folder": { request: void, response: boolean };
|
||||
|
||||
/* ** OTHERS (if your IPC channel is not in a "-ipcs" file, put it here) ** */
|
||||
"shortcut-launch-options": { request: void, response: LaunchOption };
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { SongDetailDiffCharactertistic, SongDiffName } from "../song-details-cache/song-details-cache.model";
|
||||
import { RawMapInfoDataV2 } from "./raw-map-info-v2.model";
|
||||
import { RawMapInfoDataV3 } from "./raw-map-info-v3.model";
|
||||
import { RawMapInfoDataV4 } from "./raw-map-info-v4.model";
|
||||
|
||||
export type AnyRawMapInfo = RawMapInfoDataV2 | RawMapInfoDataV4;
|
||||
export type AnyRawMapInfo = RawMapInfoDataV2 | RawMapInfoDataV3 | RawMapInfoDataV4;
|
||||
|
||||
// interfaces/mapInfo.ts
|
||||
export interface MapInfo {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { SongDetailDiffCharactertistic, SongDiffName } from "../song-details-cache/song-details-cache.model";
|
||||
|
||||
export interface RawMapInfoDataV2 {
|
||||
_version: string;
|
||||
_version: `2.${number}.${number}`;
|
||||
_songName: string;
|
||||
_songSubName: string;
|
||||
_songAuthorName: string;
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { SongDetailDiffCharactertistic, SongDiffName } from "../song-details-cache/song-details-cache.model";
|
||||
|
||||
export interface RawMapInfoDataV3 {
|
||||
_version: `3.${number}.${number}`;
|
||||
_songName: string;
|
||||
_songSubName: string;
|
||||
_songAuthorName: string;
|
||||
_levelAuthorName: string;
|
||||
_beatsPerMinute: number;
|
||||
_songTimeOffset: number;
|
||||
_shuffle: number;
|
||||
_shufflePeriod: number;
|
||||
_previewStartTime: number;
|
||||
_previewDuration: number;
|
||||
_songFilename: string;
|
||||
_coverImageFilename: string;
|
||||
_environmentName: string;
|
||||
_environmentNames?: string[];
|
||||
_allDirectionsEnvironmentName: string;
|
||||
_difficultyBeatmapSets: RawDifficultySetV3[];
|
||||
}
|
||||
|
||||
interface RawDifficultySetV3 {
|
||||
_beatmapCharacteristicName: SongDetailDiffCharactertistic;
|
||||
_difficultyBeatmaps: RawMapDifficultyV3[];
|
||||
}
|
||||
|
||||
interface RawMapDifficultyV3 {
|
||||
_difficulty: SongDiffName;
|
||||
_difficultyRank: number;
|
||||
_beatmapFilename: string;
|
||||
_noteJumpMovementSpeed: number;
|
||||
_noteJumpStartBeatOffset: number;
|
||||
_beatmapColorSchemeIdx?: number;
|
||||
_environmentNameIdx?: number;
|
||||
_customData?: {
|
||||
_difficultyLabel?: string;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { SongDetailDiffCharactertistic, SongDiffName } from "../song-details-cac
|
||||
|
||||
// interfaces/version4.ts
|
||||
export interface RawMapInfoDataV4 {
|
||||
version: string;
|
||||
version: `4.${number}.${number}`;
|
||||
song: {
|
||||
title: string;
|
||||
subTitle: string;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { MapDifficulty, MapInfo, AnyRawMapInfo } from "shared/models/maps/info/map-info.model";
|
||||
import { RawMapInfoDataV2 } from "shared/models/maps/info/raw-map-info-v2.model";
|
||||
import { RawMapInfoDataV3 } from "shared/models/maps/info/raw-map-info-v3.model";
|
||||
import { RawMapInfoDataV4 } from "shared/models/maps/info/raw-map-info-v4.model";
|
||||
|
||||
function parseVersion2(data: RawMapInfoDataV2): MapInfo {
|
||||
@@ -34,6 +35,11 @@ function parseVersion2(data: RawMapInfoDataV2): MapInfo {
|
||||
};
|
||||
}
|
||||
|
||||
function parseVersion3(data: RawMapInfoDataV3): MapInfo {
|
||||
// It's the same treatment as the version 2
|
||||
return parseVersion2(data as unknown as RawMapInfoDataV2)
|
||||
}
|
||||
|
||||
function parseVersion4(data: RawMapInfoDataV4): MapInfo {
|
||||
return {
|
||||
version: data.version,
|
||||
@@ -64,7 +70,7 @@ function parseVersion4(data: RawMapInfoDataV4): MapInfo {
|
||||
}
|
||||
|
||||
export function parseMapInfoDat(info: AnyRawMapInfo): MapInfo | never {
|
||||
const version = (info as RawMapInfoDataV2)?._version || (info as RawMapInfoDataV4)?.version;
|
||||
const version = (info as RawMapInfoDataV2)?._version || (info as RawMapInfoDataV3)?._version || (info as RawMapInfoDataV4)?.version;
|
||||
|
||||
if(!version) {
|
||||
throw new Error('Cannot determine info.dat version');
|
||||
@@ -74,6 +80,10 @@ export function parseMapInfoDat(info: AnyRawMapInfo): MapInfo | never {
|
||||
return parseVersion2(info as RawMapInfoDataV2);
|
||||
}
|
||||
|
||||
if (version.startsWith('3.')) {
|
||||
return parseVersion3(info as RawMapInfoDataV3);
|
||||
}
|
||||
|
||||
if (version.startsWith('4.')) {
|
||||
return parseVersion4(info as RawMapInfoDataV4);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user