mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b841d067c7 | |||
| 27e5d79bf7 | |||
| 28a5576ef9 | |||
| 3b6e908352 | |||
| 004d335eda | |||
| ef8662edca | |||
| f2153f3d0b | |||
| b7ac9901c4 | |||
| 962f1d8aae | |||
| 66b834bd31 | |||
| 0214a518ee | |||
| 40137b15fa | |||
| d30e203d29 | |||
| 7810b31d13 | |||
| 9720335adf | |||
| 1af3ef7565 | |||
| 7da09c117f | |||
| 9ff47b6895 | |||
| ed4137949d | |||
| f833f28930 | |||
| 97fa44cd5d | |||
| 6562f6c764 | |||
| 1177091cf0 | |||
| a785828a6b | |||
| 39b4a48fef | |||
| a62038450f | |||
| 070eb5b796 | |||
| bc1e809545 | |||
| dee49c964a | |||
| 4aa1521e88 | |||
| 4a53917f27 | |||
| 75c849d203 | |||
| 3e3cffc6bc | |||
| 2d211e3c05 | |||
| e5142fe131 | |||
| 89312f0bf9 | |||
| f2e6fa486a | |||
| 4f0f80bbc2 | |||
| 31e5c0ed10 | |||
| 9a2afe6820 | |||
| c93f9a7041 | |||
| 5401248286 | |||
| 0511540b98 | |||
| dcb233c957 | |||
| 804cb22f56 | |||
| 00e4059213 | |||
| 83fcbf6d04 | |||
| 420ecc9ae6 | |||
| 835e9a0678 | |||
| e1a6721f7d | |||
| 2e588ef4cb | |||
| d3e0099d14 | |||
| b9acdc9578 | |||
| fefd552f60 | |||
| 6eb2866eee | |||
| 01401c4691 | |||
| 37c2bfcd04 | |||
| e190217390 | |||
| 5fa7013106 | |||
| abf53489f5 | |||
| 0db13a8474 | |||
| e1229a45a0 | |||
| eafe26507a |
+1
-1
@@ -1 +1 @@
|
||||
|
||||
patreon: bsmanager
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<a href="https://github.com/Zagrios/bs-manager/issues"><img alt="GitHub issues" src="https://img.shields.io/github/issues/Zagrios/bs-manager?style=for-the-badge"></a>
|
||||
<br>
|
||||
<a href="https://discord.gg/uSqbHVpKdV"><img src ="https://img.shields.io/badge/-DISCORD-5865f2?style=for-the-badge&logo=discord&logoColor=ffffff" alt="discord"/></a>
|
||||
<!--<a href="https://mee6.xyz/fr/m/bsmanager"><img src ="https://img.shields.io/badge/-🥰%20Support%20BSM-EC4546?style=for-the-badge" alt="Donation"/></a>-->
|
||||
<a href="https://www.patreon.com/bsmanager"><img src ="https://img.shields.io/badge/-🥰%20Support%20BSM-EC4546?style=for-the-badge" alt="Donation"/></a>
|
||||
<a href="https://twitter.com/BSManager_"><img src ="https://img.shields.io/badge/-Twitter-F5F8FA?style=for-the-badge&logo=Twitter" alt="Twitter"/></a>
|
||||
<br>
|
||||
[<a href="#readme-bot">Go to bottom</a>]
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
@@ -517,5 +517,21 @@
|
||||
"ReleaseImg": "https://cdn.akamai.steamstatic.com/steamcommunity/public/images/clans//32055887/8f978bbf292810004bca12523e76be8a7eec8c4e.png",
|
||||
"ReleaseDate": "1677593822",
|
||||
"year": "2023"
|
||||
},
|
||||
{
|
||||
"BSVersion": "1.29.0",
|
||||
"BSManifest": "3341527958186345367",
|
||||
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/6169409105082976092",
|
||||
"ReleaseImg": "https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/clans//32055887/255fd49cd96042b97089f754609be291293e783f.png",
|
||||
"ReleaseDate": "1680188760",
|
||||
"year": "2023"
|
||||
},
|
||||
{
|
||||
"BSVersion": "1.29.1",
|
||||
"BSManifest": "886973241045584398",
|
||||
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/6169409105101272202",
|
||||
"ReleaseImg": "https://cdn.akamai.steamstatic.com/steamcommunity/public/images/clans/32055887/c328e407367e9914abaf92f609501877ee5abb63.png",
|
||||
"ReleaseDate": "1680623885",
|
||||
"year": "2023"
|
||||
}
|
||||
]
|
||||
@@ -1,3 +1,7 @@
|
||||
[
|
||||
{"username": "Shidorien"}
|
||||
{"username": "Shidorien"},
|
||||
{"username": "Doloro", "type": "gold"},
|
||||
{"username": "iPixelGalaxy", "type": "diamond", "link": "https://twitch.tv/ipixelgalaxy"},
|
||||
{"username": "Emyte", "type": "gold"},
|
||||
{"username": "GoodOldNervy"}
|
||||
]
|
||||
@@ -0,0 +1,741 @@
|
||||
{
|
||||
"misc": {
|
||||
"download": "Laden",
|
||||
"verify": "Prüfen",
|
||||
"launch": "Starten",
|
||||
"mods": "Mods",
|
||||
"maps": "Maps",
|
||||
"playlists": "Playlisten",
|
||||
"models": "Modelle",
|
||||
"cancel": "Abbruch",
|
||||
"delete": "Löschen",
|
||||
"accept": "Akzeptieren",
|
||||
"refuse": "Ablehnen",
|
||||
"apply": "Übernehmen"
|
||||
},
|
||||
"nav-bar":{
|
||||
"add-version": "Version Hinzufügen",
|
||||
"settings": "Einstellungen"
|
||||
},
|
||||
"pages": {
|
||||
"shared-maps":{
|
||||
"title": "Geteilte Maps"
|
||||
},
|
||||
"version-viewer": {
|
||||
"launch-mods": {
|
||||
"oculus": "Oculus Modus",
|
||||
"oculus-description": "Wenn du Beat Saber über Steam benutzt, es ermöglicht dir Oculus's VR-Composer zu nutzen ohne SteamVR, um bessere Leistung zu erhalten. Dies wird nicht benötigt für Oculus Headsets",
|
||||
"desktop": "Desktop Modus",
|
||||
"desktop-description": "Auf diese Weise kannst du WASD und die Maus verwenden, um im Spiel durch das Menü zu navigieren. Dies erleichtert das Testen erheblich, da Du dein Headset nicht aufsetzen musst!",
|
||||
"debug": "Debug Modus",
|
||||
"debug-description": "Aktiviert das Ausgabeprotokollfenster für IPA. Dies zeigt die Debug-Konsole, die Mods verwenden.",
|
||||
"advanced-launch": {
|
||||
"button": "Erweiterter Start",
|
||||
"placeholder": "Weitere Argumente Bspw: --revert; --nowait"
|
||||
}
|
||||
},
|
||||
"maps": {
|
||||
"title": "Geteilte Maps",
|
||||
"search-bar": {
|
||||
"search-placeholder": "Suche eine Map",
|
||||
"filters-btn": "Filter",
|
||||
"dropdown": {
|
||||
"export-maps": "Maps Exportieren",
|
||||
"delete-maps": "Maps Löschen"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"maps": {
|
||||
"actions": {
|
||||
"add-maps": {
|
||||
"text": "Hinzufügen"
|
||||
},
|
||||
"link-maps": {
|
||||
"tooltips": {
|
||||
"link": "Maps Verknüpfen",
|
||||
"unlink": "Maps Verknüpfung aufheben"
|
||||
}
|
||||
}
|
||||
},
|
||||
"empty-maps":{
|
||||
"text":"Keine Maps",
|
||||
"button":"Maps Herunterladen"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"mods": {
|
||||
"loading-mods": "Lade Mods...",
|
||||
"no-internet": "Kein Internet",
|
||||
"mods-not-available": "Für diese Version von Beat Saber sind noch keine Mods verfügbar",
|
||||
"buttons": {
|
||||
"more-infos": "Mehr Infos",
|
||||
"install-or-update": "Installieren oder aktualisieren"
|
||||
},
|
||||
"mods-grid": {
|
||||
"header-bar": {
|
||||
"name": "Name",
|
||||
"installed": "Installiert",
|
||||
"latest": "Neueste",
|
||||
"description": "Beschreibung",
|
||||
"dropdown": {
|
||||
"uninstall-all": "Alle deinstallieren"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dropdown": {
|
||||
"open-folder": "Ordner Öffnen",
|
||||
"verify-files": "Daten Prüfen",
|
||||
"clone": "Klonen",
|
||||
"edit": "Bearbeiten",
|
||||
"uninstall": "Entfernen",
|
||||
"shared-folders": "Geteilte Ordner"
|
||||
}
|
||||
},
|
||||
"available-versions": {
|
||||
"title": "Wähle eine Version",
|
||||
"steam-release": "Versions Seite",
|
||||
"dropdown": {
|
||||
"refresh": "Liste aktualisieren",
|
||||
"import-version" :"Importiere eine Version"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"steam": {
|
||||
"title": "Steam",
|
||||
"description": "Wenn Du dich von Steam abmeldest, musst Du dich erneut anmelden, um eine neue Version von Beat Saber herunterzuladen.",
|
||||
"logout": "Abmelden"
|
||||
},
|
||||
"appearance": {
|
||||
"title": "Aussehen",
|
||||
"description": "Wähle Zwei von den Hauptfarben von BSManager",
|
||||
"reset": "Zurücksetzen",
|
||||
"sub-title": "Design",
|
||||
"themes": {
|
||||
"dark": "Dunkel",
|
||||
"light": "Hell",
|
||||
"os": "Mit Computer Synchronisieren"
|
||||
}
|
||||
},
|
||||
"installation-folder": {
|
||||
"title": "Installations Ordner",
|
||||
"description": "Ändern Sie den Standardordner für Beat Saber-Versionen und andere kommende Funktionen.",
|
||||
"choose-folder": "Ordner Wählen"
|
||||
},
|
||||
"additional-content": {
|
||||
"title": "Zusätzlicher Inhalt",
|
||||
"description": "Mit zusätzlichen Inhalten kannst du BeatSaber anpassen!",
|
||||
"deep-links": {
|
||||
"sub-title": "OneClick-Installationen"
|
||||
}
|
||||
},
|
||||
"language": {
|
||||
"title": "Sprache",
|
||||
"description": "Wähle eine Sprache.",
|
||||
"languages": {
|
||||
"en-EN": "English, UK",
|
||||
"en-US": "English, US",
|
||||
"fr-FR": "Français",
|
||||
"es-ES": "Español",
|
||||
"de-DE": "Deutsch",
|
||||
"translated": {
|
||||
"en-EN": "Englisch, Vereinigtes Königreich",
|
||||
"en-US": "Englisch, USA",
|
||||
"fr-FR": "Französisch",
|
||||
"es-ES": "Spanisch",
|
||||
"de-DE": "Deutsch"
|
||||
}
|
||||
}
|
||||
},
|
||||
"patreon": {
|
||||
"title": "BSManager Unterstützen 💖",
|
||||
"description": "Unterstütze das Projekt und helfe uns, BSManager kontinuierlich zu verbessern.",
|
||||
"buttons": {
|
||||
"support": "BSManager Unterstützen 🥰",
|
||||
"supporters": "Unterstützer 👀"
|
||||
},
|
||||
"view": {
|
||||
"no-supporters": "Noch keine Unterstützer",
|
||||
"sponsors": "Sponsoren",
|
||||
"supporters": "Unterstützer"
|
||||
}
|
||||
},
|
||||
"discord": {
|
||||
"description": "Trete der BSManager-Community bei, indem Du uns in unseren sozialen Medien folgst!"
|
||||
},
|
||||
"contribution": {
|
||||
"description": "Schlage neue Funktionen vor, oder melde einen Fehler und BSManager zu verbessern!",
|
||||
"buttons": {
|
||||
"request-features": "Funktion Vorschlagen",
|
||||
"report-bug": "Fehler melden",
|
||||
"open-logs": "Protokolle öffnen"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"types": {
|
||||
"error": "🚨 Fehler",
|
||||
"warning": "⚠️ Warnung",
|
||||
"success": "🎉 Erfolg"
|
||||
},
|
||||
"common": {
|
||||
"msg": {
|
||||
"error-occurred": "Ein Fehler ist aufgetreten"
|
||||
}
|
||||
},
|
||||
"shared": {
|
||||
"errors": {
|
||||
"titles": {
|
||||
"operation-running": "Vorgang läuft",
|
||||
"no-internet": "Kein Internet"
|
||||
},
|
||||
"msg": {
|
||||
"operation-running": "Warte auf das Ende des Aktuellen Vorgangs und versuche es erneut",
|
||||
"no-internet": "Überprüfe deine Verbindung und versuche es erneut."
|
||||
}
|
||||
}
|
||||
},
|
||||
"bs-download": {
|
||||
"success": {
|
||||
"titles": {
|
||||
"download-success": "Erfolgreich Heruntergeladen",
|
||||
"verification-finished": "Verifikation Erfolgreich"
|
||||
}
|
||||
},
|
||||
"warnings": {
|
||||
"msg": {
|
||||
"ManifestChecksum": "Die zuvor heruntergeladene Manifest stimmt nicht mit dem neuen überein 🤔",
|
||||
"ConnectionTimeout": "Deine Internetverbindung scheint instabil zu sein 🥶",
|
||||
"ConnectionLost": "Die Verbindung wurde unterbrochen, versuche es erneut...",
|
||||
"ConnectionError": "Kann keine Verbindung zu Steam herstellen, versuche es erneut...",
|
||||
"Unknown": "Etwas Seltsames ist passiert 🤔 Deine Verbindung ist wahrscheinlich instabil."
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"titles":{
|
||||
"dotnet-required": ".NET 6 Benötigt"
|
||||
},
|
||||
"msg": {
|
||||
"401": "Steam scheint uns BeatSaber nicht herunterladen zu lassen 😢",
|
||||
"404": "Steam-Server können nicht kontaktiert werden.",
|
||||
"Password": "Passwort ist ungültig.",
|
||||
"InvalidCredentials": "Deine E-Mail oder Passwort ist ungültig 😴",
|
||||
"InvalidPassword": "Dein Passwort ist ungültig 😕",
|
||||
"NoManifest": "Es wurde keine Manifest gefunden",
|
||||
"DirectoryCreate": "Die erforderlichen Ordner können nicht installiert werden.",
|
||||
"NotAvailableApp": "Versuchst du, Beat Saber herunterzuladen, wenn Du es nicht hast? 🤣",
|
||||
"DepotNotFound": "BeatSaber kann nicht heruntergeladen werden 😥 versuche es später noch einmal 😕",
|
||||
"NotCompleted": "Der Download konnte nicht beendet werden ¯\\_(ツ)_/¯",
|
||||
"InvalidManifest": "BeatSaber kann nicht heruntergeladen werden 😥 versuche es später noch einmal 😕",
|
||||
"NoValidKey": "BeatSaber kann nicht heruntergeladen werden 😥 versuche es später noch einmal 😕",
|
||||
"NoManifestCode": "BeatSaber kann nicht heruntergeladen werden 😥 versuche es später noch einmal 😕",
|
||||
"Unknown": "Ein Unbekannter Fehler ist aufgetreten ¯\\_(ツ)_/¯",
|
||||
"NoServer": "Steam-Server können nicht kontaktiert werden.",
|
||||
"NotAllowed": "Anscheinend darfst Du BeatSaber nicht herunterladen 🥱",
|
||||
"ConnectionTimeout": "Kann keine Verbindung zu Steam herstellen 😕",
|
||||
"SteamLib": "Wenn dieser Fehler auftritt, melde den Fehler auf Github mit Protokollen, bitte.",
|
||||
"ConnectionError": "Kann nach 10 Versuchen keine Verbindung zu Steam herstellen 🤯",
|
||||
"LicenceError": "Die Liste der Lizenzen kann nicht abgerufen werden.",
|
||||
"verification-failed": "Verifikation fehlgeschlagen, versuche es später erneut.",
|
||||
"RateLimitExceeded": "Du hast es zu oft versucht, warte eine Weile und versuche es später erneut.",
|
||||
"AlreadyDownloading": "Ein Download ist bereits im Gange",
|
||||
"no-internet": "Keine Internet Verbindung",
|
||||
"dotnet-required": ".NET 6 Runtime muss installiert sein, um eine Version von BeatSaber herunterzuladen. Lade es herunter, indem Du auf die Schaltfläche unten klickst."
|
||||
},
|
||||
"actions":{
|
||||
"download-dotnet": ".NET 6 Herunterladen"
|
||||
}
|
||||
}
|
||||
},
|
||||
"bs-import-version":{
|
||||
"success":{
|
||||
"start-import": {
|
||||
"title": "Import im Gange 👌",
|
||||
"desc": "Der Import kann je nach Konfiguration einige Minuten dauern."
|
||||
},
|
||||
"imported": {
|
||||
"title": "Versionen Importiert 🎉"
|
||||
}
|
||||
},
|
||||
"errors":{
|
||||
"import-error":{
|
||||
"desc": "Überprüfe, ob der ausgewählte Ordner eine BeatSaber-Installation ist."
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"move-folder": {
|
||||
"success": {
|
||||
"titles": {
|
||||
"transfer-started": "Übertragung läuft",
|
||||
"transfer-finished": "Transfer abgeschlossen"
|
||||
},
|
||||
"descs": {
|
||||
"transfer-started": "Die Übertragung hat begonnen und kann je nach Konfiguration mehrere Minuten dauern."
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"titles": {
|
||||
"transfer-failed": "Transfer fehlgeschlagen 😕"
|
||||
},
|
||||
"descs": {
|
||||
"COPY_TO_SUBPATH": "Der Zielordner darf kein Unterordner des Quellordners sein.",
|
||||
"restore-linked-folders": "Beim Wiederherstellen von freigegebenen Ordnern ist ein Fehler aufgetreten. Du kannst sie dennoch manuell über das Menü 'Freigegebene Ordner' auf der Versionsseite wiederherstellen."
|
||||
}
|
||||
}
|
||||
},
|
||||
"steam": {
|
||||
"success": {
|
||||
"titles": {
|
||||
"logout": "Von Steam getrennt."
|
||||
}
|
||||
}
|
||||
},
|
||||
"additional-content":{
|
||||
"deep-link": {
|
||||
"select-all": "Alle Auswählen",
|
||||
"activation":{
|
||||
"success":{
|
||||
"title":"OneClick aktiviert!",
|
||||
"description":"OneClick-Installationen wurden aktiviert."
|
||||
},
|
||||
"error":{
|
||||
"description":"OneClick-Installationen konnten nicht aktiviert werden."
|
||||
}
|
||||
},
|
||||
"deactivation":{
|
||||
"success":{
|
||||
"title":"OneClick deaktiviert!",
|
||||
"description":"OneClick-Installationen wurden deaktiviert."
|
||||
},
|
||||
"error":{
|
||||
"description":"Ein Unbekannter Fehler ist aufgetreten."
|
||||
}
|
||||
},
|
||||
"check-all-enabled":{
|
||||
"title": "OneClick deaktiviert",
|
||||
"description": "Eine oder mehrere OneClick-Installationen sind deaktiviert. Gehe in die Einstellungen, um sie zu aktivieren.",
|
||||
"actions":{
|
||||
"settings": "Einstellungen",
|
||||
"not-remind": "Erinnere mich nicht"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"bs-launch": {
|
||||
"success": {
|
||||
"titles": {
|
||||
"launching": "Starte...🚀"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"titles": {
|
||||
"UNABLE_TO_LAUNCH": "Start nicht möglich",
|
||||
"STEAM_NOT_RUNNING": "Steam läuft nicht",
|
||||
"OCULUS_NOT_RUNNING": "Oculus läuft nicht",
|
||||
"BS_ALREADY_RUNNING": "BeatSaber läuft bereits",
|
||||
"EXE_NOT_FINDED": "Dateien Fehlen",
|
||||
"EXIT": "Abrupter Stop"
|
||||
},
|
||||
"msg": {
|
||||
"STEAM_NOT_RUNNING": "Steam muss laufen, um BeatSaber zu starten.",
|
||||
"OCULUS_NOT_RUNNING": "Oculus muss ausgeführt werden, um BeatSaber zu starten.",
|
||||
"BS_ALREADY_RUNNING": "Schließe BeatSaber, bevor Du es erneut startest.",
|
||||
"EXE_NOT_FINDED": "Einige Dateien scheinen zu fehlen, versuche die Dateien zu überprüfen.",
|
||||
"EXIT": "BeatSaber stoppte abrupt, versuche die Dateien zu überprüfen."
|
||||
},
|
||||
"actions":{
|
||||
"STEAM_NOT_RUNNING": "Starte Steam"
|
||||
}
|
||||
}
|
||||
},
|
||||
"steam":{
|
||||
"steam-launching":{
|
||||
"title": "Steam startet!",
|
||||
"description": "Abhängig von deiner Konfiguration kann es einige Zeit dauern, bis Steam gestartet wird."
|
||||
}
|
||||
},
|
||||
"custom-version": {
|
||||
"errors": {
|
||||
"titles": {
|
||||
"CantEditSteam": "Kann nicht bearbeitet werden",
|
||||
"CantRename": "Umbenennen unmöglich",
|
||||
"VersionAlreadExist": "Diese Version existiert bereits",
|
||||
"CantClone": "Klonen unmöglich"
|
||||
},
|
||||
"msg": {
|
||||
"CantEditSteam": "Die Steam-Version kann nicht bearbeitet werden. Du kannst es aber klonen."
|
||||
}
|
||||
},
|
||||
"success": {
|
||||
"titles": {
|
||||
"CloningFinished": "Klonen abgeschlossen 🎉"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mods": {
|
||||
"install-mods": {
|
||||
"titles": {
|
||||
"success": "Mods Installiert 🎉",
|
||||
"warning": "Mods Installiert 🤔"
|
||||
},
|
||||
"msg": {
|
||||
"success": "Alle Mods wurden installiert.",
|
||||
"warning": "Eine oder mehr Mods konnten nicht installiert werden.",
|
||||
"errors": {
|
||||
"no-mods": "Keine Mods zum installieren.",
|
||||
"cannot-install-bsipa": "BSIPA installation fehlgeschlagen 😨"
|
||||
}
|
||||
}
|
||||
},
|
||||
"uninstall-mod": {
|
||||
"titles": {
|
||||
"success": "Mod entfernt 🎉"
|
||||
},
|
||||
"msg": {
|
||||
"errors": {
|
||||
"no-mods": "Diese Mod kann nicht installiert werden 😑"
|
||||
}
|
||||
}
|
||||
},
|
||||
"uninstall-all-mods": {
|
||||
"titles": {
|
||||
"success": "Mods entfernt 🎉"
|
||||
},
|
||||
"msg": {
|
||||
"success": "Alle Mods wurden entfernt.",
|
||||
"errors": {
|
||||
"no-mods": "Keine Mods sind in dieser Version installiert 😑"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"maps":{
|
||||
"one-click-install":{
|
||||
"success":"Map installation abegschlossen",
|
||||
"error":"Ein Fehler ist aufgetreten während der installation der Map"
|
||||
}
|
||||
},
|
||||
"playlists":{
|
||||
"one-click-install":{
|
||||
"success":"Playlist installation abgeschlossen",
|
||||
"error":"Ein Fehler ist aufgetreten während der installation der Playlist"
|
||||
}
|
||||
},
|
||||
"models":{
|
||||
"one-click-install":{
|
||||
"success":"Modell installation abgeschlossen",
|
||||
"error":"Ein Fehler ist aufgetreten während der installation des Modells"
|
||||
}
|
||||
},
|
||||
"shared-folder": {
|
||||
"info": {
|
||||
"userdata-backup-created":{
|
||||
"title": "Sicherung erstellt",
|
||||
"msg": "Das Freigeben des Ordners „UserData“ kann zu Fehlern führen. Bei Problemen trenne den Ordner, um die Sicherung wiederherzustellen"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
"misc":{
|
||||
"remember-my-choice": "Merke meine Wahl"
|
||||
},
|
||||
"guard": {
|
||||
"title": "Steam Guard",
|
||||
"inputs": {
|
||||
"guard-code": {
|
||||
"label": "Guard-Code",
|
||||
"placeholder": "Gebe deinen Guard-Code ein"
|
||||
}
|
||||
},
|
||||
"buttons": {
|
||||
"submit": "Anmelden"
|
||||
}
|
||||
},
|
||||
"steam-login": {
|
||||
"title": "Steam Login",
|
||||
"inputs": {
|
||||
"username": {
|
||||
"label": "Nutzername",
|
||||
"placeholder": "Gebe deinen Nutzernamen ein"
|
||||
},
|
||||
"password": {
|
||||
"label": "Passwort",
|
||||
"placeholder": "Gebe dein Passwort ein"
|
||||
},
|
||||
"stay": {
|
||||
"label": "Verbunden bleiben"
|
||||
}
|
||||
},
|
||||
"why-credentials": "Warum braucht ihr meine Anmeldeinformationen?",
|
||||
"buttons": {
|
||||
"submit": "Anmelden"
|
||||
}
|
||||
},
|
||||
"steam-credentials":{
|
||||
"title": "Steam Anmeldeinformationen",
|
||||
"p-1": "Die Anmeldeinformationen werden nur zum Herunterladen des Spiels verwendet, da Steam überprüfen muss, ob Du das Spiel bezahlt hast, um es herunterladen zu dürfen. Sie werden nicht gespeichert und direkt an DepotDownloader weitergegeben. Wenn Du deine Anmeldeinformationen nicht eingeben möchtest, kannst Du diesem Tutorial folgen:",
|
||||
"p-2": "und klicke dann auf das Zahnradsymbol in der oberen rechten Ecke und wähle \"Version importieren\", wähle den Ordner aus, in den Beat Saber heruntergeladen wurde (Wenn Du dem obigen Tutorial folgst, solltest Du den richtigen Speicherort haben)"
|
||||
},
|
||||
"bs-import-version":{
|
||||
"title": "Importiere eine Version",
|
||||
"description": "Importiert eine Version von BeatSaber zur Verwendung mit BSManager. Der Import kopiert den ausgewählten BeatSaber-Installationsordner in den BSManager-Versionsordner.",
|
||||
"buttons":{
|
||||
"submit": "Importiere eine Version"
|
||||
}
|
||||
},
|
||||
"bs-uninstall": {
|
||||
"title": "Deinstallieren",
|
||||
"description": "Möchtest du BeatSaber {version} wirklich deinstallieren? Du musst es erneut herunterladen, um es zu spielen.",
|
||||
"buttons": {
|
||||
"submit": "Deinstallieren"
|
||||
}
|
||||
},
|
||||
"install-folder": {
|
||||
"title": "Installation Ordner",
|
||||
"description": "Das Ändern des Standardinstallationsordners führt dazu, dass alle installierten Daten in den neuen Ordner verschoben werden.",
|
||||
"buttons": {
|
||||
"submit": "Ordner Wählen"
|
||||
}
|
||||
},
|
||||
"edit-version": {
|
||||
"title": "Version ändern",
|
||||
"buttons": {
|
||||
"submit": "Ändern"
|
||||
}
|
||||
},
|
||||
"clone-version": {
|
||||
"title": "Version Klonen",
|
||||
"description": "Durch das Klonen der Version kannst Du zusätzlichen BeatSaber-Inhalt zwischen zwei Versionen trennen.",
|
||||
"inputs": {
|
||||
"name": {
|
||||
"label": "Name",
|
||||
"placeholder": "Versions Name"
|
||||
},
|
||||
"color": {
|
||||
"label": "Farbe"
|
||||
}
|
||||
},
|
||||
"buttons": {
|
||||
"submit": "Klonen"
|
||||
}
|
||||
},
|
||||
"uninstall-mod": {
|
||||
"title": "Entfernen",
|
||||
"description": "Bist du sicher das du die Mod {mod} entfernen möchtest? Es könnte dafür sorgen, dass andere installationen nicht mehr richtig funktionieren.",
|
||||
"description-bsipa": "Bist du sicher, dass du BSIPA entfernen möchtest? Dannach werden deine Mods nicht mehr funktionieren."
|
||||
},
|
||||
"uninstall-all-mods": {
|
||||
"title": "Entferne Alle Mods",
|
||||
"description": "Bist du sicher das du alle Mods von der Version {version} entfernen möchtest? Diese Aktion kann nicht rückgängig gemacht werden."
|
||||
},
|
||||
"maps-actions": {
|
||||
"delete-maps": {
|
||||
"title": {
|
||||
"single": "Map Entfernen?",
|
||||
"multiple": "Maps Entfernen?"
|
||||
},
|
||||
"desc": {
|
||||
"single": "Bist du sicher das du die Map {name} entfernen willst?",
|
||||
"multiple": "Bist du sicher das du alle {nb} maps entfernen willst?"
|
||||
},
|
||||
"info": {
|
||||
"desc": {
|
||||
"single": "Diese Map ist teil der geteilten Maps",
|
||||
"multiple": "Diese Maps sind teil der geteilten Maps"
|
||||
},
|
||||
"title": {
|
||||
"single": "Diese Map wird auch aus Versionen entfernt, die geteilte Maps verwenden",
|
||||
"multiple": "Diese Maps werden auch aus Versionen entfernt, die geteilte Maps verwenden"
|
||||
}
|
||||
}
|
||||
},
|
||||
"link-maps": {
|
||||
"title": "Maps Verknüpfen",
|
||||
"desc": "Das Verknüpfen von Maps ermöglicht es, Maps zwischen allen Versionen zu teilen. Nach der Verknüpfung profitiert diese Version von den geteilten Maps",
|
||||
"info": "Das Hinzufügen und Löschen von Maps wird ebenfalls geteilt",
|
||||
"keep-maps": {
|
||||
"label": "Maps behalten",
|
||||
"title": "Maps behalten verschiebt die Maps der aktuellen Version in den geteilten Maps. Andernfalls gehen sie verloren."
|
||||
},
|
||||
"valid-btn": "Maps Verknüpfen"
|
||||
},
|
||||
"unlink-maps": {
|
||||
"title": "Maps Verknüpfung aufheben",
|
||||
"desc": "Bitte beachte, dass das Aufheben der Verknüpfung von Maps die Verwendung von geteilten Maps für diese Version nicht zulässt.",
|
||||
"keep-maps": {
|
||||
"label": "Maps behalten",
|
||||
"title": "Maps behalten verschiebt die Maps der aktuellen Version in den geteilten Maps. Andernfalls gehen sie verloren."
|
||||
},
|
||||
"valid-btn": "Verknüpfung aufheben"
|
||||
}
|
||||
},
|
||||
"download-maps": {
|
||||
"search-btn": "Suchen",
|
||||
"loading-maps": "Lade Maps...",
|
||||
"no-maps-found": "Keine Maps gefunden",
|
||||
"no-internet": "Kein Internet"
|
||||
},
|
||||
"mods-disclaimer":{
|
||||
"title": "Haftungsausschluss",
|
||||
"p-1": "Indem Du dich für die Verwendung von Mods entscheiden, verstehst Du Folgendes:",
|
||||
"li-1": "Möglicherweise treten Probleme auf, die im Vanilla-Spiel nicht vorhanden sind. 99.9% der Fehler, Abstürze und Verzögerungen sind auf Mods zurückzuführen.",
|
||||
"li-2": "Mods können durch Updates beschädigt werden und das ist normal - sei geduldig und respektvoll, wenn dies passiert, da Modder Freiwillige mit echtem Leben sind.",
|
||||
"li-3": "Beat Games versuchen nicht absichtlich, Mods kaputt zu machen. Sie möchten an der Codebasis arbeiten und manchmal macht dies Mods kaputt, aber sie sind nicht darauf aus, Mods zu verbieten.",
|
||||
"p-2": "Greife die Spieleentwickler nicht wegen Problemen im Zusammenhang mit Mods an und umgekehrt - Modder und Spieleentwickler sind zwei getrennte Gruppen. Sei einfach nicht gemein, okay?"
|
||||
},
|
||||
"shared-folders":{
|
||||
"title": "Geteilte Ordner",
|
||||
"description": "Verknüpfe BeatSaber-Ordner, um ihre Inhalte mit freigegebenen Ordnern in anderen Versionen zu synchronisieren. Beachte, dass auch das Löschen von Inhalten geteilt wird.",
|
||||
"buttons": {
|
||||
"add-folder": "Ordner Hinzufügen",
|
||||
"link-folder": "Ordner Verknüpfen",
|
||||
"unlink-folder": "Ordner Verknüpfung Aufheben",
|
||||
"link-all": "Alle verlinken"
|
||||
}
|
||||
}
|
||||
},
|
||||
"maps": {
|
||||
"map-filter-panel": {
|
||||
"duration": "Länge",
|
||||
"tags": "Stichworte",
|
||||
"specificities": "Allgemein",
|
||||
"requirements": "Anforderungen"
|
||||
},
|
||||
"map-types": {
|
||||
"accuracy": "Präzision",
|
||||
"balanced": "Ausgeglichen",
|
||||
"challenge": "Herausforderung",
|
||||
"dancestyle": "Tanz",
|
||||
"fitness": "Fitness",
|
||||
"speed": "Geschwindigkeit",
|
||||
"tech": "Tech"
|
||||
},
|
||||
"map-styles": {
|
||||
"dance": "Tanz",
|
||||
"swing": "Swing",
|
||||
"nightcore": "Nightcore",
|
||||
"folk": "Folk",
|
||||
"family": "Familie",
|
||||
"ambient": "umgebung",
|
||||
"funk": "Funk",
|
||||
"jazz": "Jazz",
|
||||
"soul": "Soul",
|
||||
"speedcore": "Speedcore",
|
||||
"punk": "Punk",
|
||||
"rb": "R&B",
|
||||
"holiday": "Urlaub",
|
||||
"vocaloid": "Vokaloid",
|
||||
"jrock": "J-Rock",
|
||||
"trance": "Trance",
|
||||
"drumbass": "Basstrommel",
|
||||
"comedy": "Komödie",
|
||||
"instrumental": "Instrumental",
|
||||
"hardcore": "Hardcore",
|
||||
"kpop": "K-Pop",
|
||||
"indie": "Indie",
|
||||
"techno": "Techno",
|
||||
"house": "House",
|
||||
"game": "Videospiele",
|
||||
"film": "Filme",
|
||||
"alt": "Alternativ",
|
||||
"dubstep": "Dubstep",
|
||||
"metal": "Metal",
|
||||
"anime": "Anime",
|
||||
"hiphop": "HipHop",
|
||||
"jpop": "J-Pop",
|
||||
"rock": "Rock",
|
||||
"pop": "Pop",
|
||||
"electronic": "Elektronisch",
|
||||
"classical-orchestral": "Klassisch & Orchestra"
|
||||
},
|
||||
"map-specificities": {
|
||||
"automapper": "KI",
|
||||
"ranked": "ranked",
|
||||
"curated": "curated",
|
||||
"verified": "verifiziert",
|
||||
"fullSpread": "full spread"
|
||||
},
|
||||
"difficulties": {
|
||||
"Easy": "einfach",
|
||||
"Normal": "normal",
|
||||
"Hard": "hard",
|
||||
"Expert": "expert",
|
||||
"ExpertPlus": "expert+"
|
||||
},
|
||||
"map-item": {
|
||||
"by": "Von {songAutor}",
|
||||
"mapped-by": "Gemapped von"
|
||||
}
|
||||
},
|
||||
"beat-saver": {
|
||||
"maps-sorts": {
|
||||
"Latest": "Neueste",
|
||||
"Relevance": "Relevanz",
|
||||
"Rating": "Bewertung",
|
||||
"Curated": "Curated"
|
||||
}
|
||||
},
|
||||
"auto-update": {
|
||||
"checking": "Prüfe auf Updates",
|
||||
"downloading": "Lade Updates Herunter"
|
||||
},
|
||||
"dateformat": {
|
||||
"dayNames": [
|
||||
"So",
|
||||
"Mo",
|
||||
"Di",
|
||||
"Mi",
|
||||
"Do",
|
||||
"Fr",
|
||||
"Sa",
|
||||
"Sonntag",
|
||||
"Montag",
|
||||
"Dienstag",
|
||||
"Mittwoch",
|
||||
"Donnerstag",
|
||||
"Freitag",
|
||||
"Samstag"
|
||||
],
|
||||
"monthNames": [
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mär",
|
||||
"Apr",
|
||||
"Mai",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sep",
|
||||
"Okt",
|
||||
"Nov",
|
||||
"Dez",
|
||||
"Januar",
|
||||
"Februar",
|
||||
"März",
|
||||
"April",
|
||||
"Mai",
|
||||
"Juni",
|
||||
"Juli",
|
||||
"August",
|
||||
"September",
|
||||
"Oktober",
|
||||
"November",
|
||||
"Dezember"
|
||||
],
|
||||
"timeNames": [
|
||||
"a",
|
||||
"p",
|
||||
"am",
|
||||
"pm",
|
||||
"A",
|
||||
"P",
|
||||
"AM",
|
||||
"PM"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -67,6 +67,7 @@
|
||||
"mods": {
|
||||
"loading-mods": "Loading mods...",
|
||||
"no-internet": "No internet",
|
||||
"mods-not-available": "No mods are available yet for this version of Beat Saber",
|
||||
"buttons": {
|
||||
"more-infos": "More info",
|
||||
"install-or-update": "Install or update"
|
||||
@@ -137,11 +138,13 @@
|
||||
"en-US": "English, US",
|
||||
"fr-FR": "Français",
|
||||
"es-ES": "Español",
|
||||
"de-DE": "Deutsch",
|
||||
"translated": {
|
||||
"en-EN": "English, United Kingdom",
|
||||
"en-US": "English, USA",
|
||||
"fr-FR": "French",
|
||||
"es-ES": "Spanish"
|
||||
"es-ES": "Spanish",
|
||||
"de-DE": "German"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -266,12 +269,20 @@
|
||||
"move-folder": {
|
||||
"success": {
|
||||
"titles": {
|
||||
"transfer-started": "Transfer in progress",
|
||||
"transfer-finished": "Transfer complete"
|
||||
},
|
||||
"descs": {
|
||||
"transfer-started": "The transfer has started and may take several minutes depending on your configuration."
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"titles": {
|
||||
"transfer-failed": "Transfer failed 😕"
|
||||
},
|
||||
"descs": {
|
||||
"COPY_TO_SUBPATH": "The destination folder cannot be a subfolder of the source folder.",
|
||||
"restore-linked-folders": "An error occurred while restoring shared folders. You can still manually restore them via the 'Shared Folders' menu on the versions page."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -583,7 +594,8 @@
|
||||
"buttons": {
|
||||
"add-folder": "Add Folder",
|
||||
"link-folder": "Link Folder",
|
||||
"unlink-folder": "Unlink Folder"
|
||||
"unlink-folder": "Unlink Folder",
|
||||
"link-all": "Link all"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
"mods": {
|
||||
"loading-mods": "Cargando mods...",
|
||||
"no-internet": "Sin internet",
|
||||
"mods-not-available": "Aún no hay mods disponibles para esta versión de Beat Saber",
|
||||
"buttons": {
|
||||
"more-infos": "Más información",
|
||||
"install-or-update": "Instalar o actualizar"
|
||||
@@ -136,11 +137,13 @@
|
||||
"en-US": "English, US",
|
||||
"fr-FR": "Français",
|
||||
"es-ES": "Español",
|
||||
"de-DE": "Deutsch",
|
||||
"translated": {
|
||||
"en-EN": "Inglés, Reino Unido",
|
||||
"en-US": "Inglés, Estados Unidos",
|
||||
"fr-FR": "Francés",
|
||||
"es-ES": "Español"
|
||||
"es-ES": "Español",
|
||||
"de-DE": "Alemán"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -265,12 +268,20 @@
|
||||
"move-folder": {
|
||||
"success": {
|
||||
"titles": {
|
||||
"transfer-started": "Transferencia en curso",
|
||||
"transfer-finished": "Transferencia completa"
|
||||
},
|
||||
"descs": {
|
||||
"transfer-started": "La transferencia ha comenzado y puede tomar varios minutos dependiendo de su configuración."
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"titles": {
|
||||
"transfer-failed": "Transferencia fallida 😕"
|
||||
},
|
||||
"descs": {
|
||||
"COPY_TO_SUBPATH": "La carpeta de destino no puede ser una subcarpeta de la carpeta de origen.",
|
||||
"restore-linked-folders": "Se produjo un error al restaurar las carpetas compartidas. Aún puede restaurarlas manualmente a través del menú 'Carpetas compartidas' en la página de versiones."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -582,7 +593,8 @@
|
||||
"buttons": {
|
||||
"add-folder": "Añadir Carpeta",
|
||||
"link-folder": "Enlazar Carpeta",
|
||||
"unlink-folder": "Desenlazar Carpeta"
|
||||
"unlink-folder": "Desenlazar Carpeta",
|
||||
"link-all": "Enlazar todo"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
"mods": {
|
||||
"loading-mods": "Chargement des mods...",
|
||||
"no-internet": "Pas d'internet",
|
||||
"mods-not-available": "Aucun mod n'est encore disponible pour cette version de Beat Saber",
|
||||
"buttons": {
|
||||
"more-infos": "Plus d'infos",
|
||||
"install-or-update": "Installer ou mettre à jour"
|
||||
@@ -136,11 +137,13 @@
|
||||
"en-US": "English, US",
|
||||
"fr-FR": "Français",
|
||||
"es-ES": "Español",
|
||||
"de-DE": "Deutsch",
|
||||
"translated": {
|
||||
"en-EN": "Anglais, Royaume-Uni",
|
||||
"en-US": "Anglais, États-Unis",
|
||||
"fr-FR": "Français",
|
||||
"es-ES": "Espagnol"
|
||||
"es-ES": "Espagnol",
|
||||
"de-DE": "Allemand"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -265,13 +268,21 @@
|
||||
"move-folder": {
|
||||
"success": {
|
||||
"titles": {
|
||||
"transfer-started": "Transfert en cours",
|
||||
"transfer-finished": "Transfert terminé 👌"
|
||||
},
|
||||
"descs":{
|
||||
"transfer-started": "Le transfert a commencé, il peut durer plusieurs minutes selon votre configuration."
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"titles": {
|
||||
"transfer-failed": "Le transfert a échoué 😕"
|
||||
}
|
||||
},
|
||||
"descs": {
|
||||
"COPY_TO_SUBPATH": "Le dossier de destination ne peut pas être un sous-dossier du dossier source.",
|
||||
"restore-linked-folders": "Une erreur s'est produite lors de la restauration des dossiers partagés. Vous pouvez toujours les restaurer manuellement via le menu 'Dossiers partagés' sur la page des versions."
|
||||
}
|
||||
}
|
||||
},
|
||||
"steam": {
|
||||
@@ -582,7 +593,8 @@
|
||||
"buttons": {
|
||||
"add-folder": "Ajouter un dossier",
|
||||
"link-folder": "Lier le dossier",
|
||||
"unlink-folder": "Délier le dossier"
|
||||
"unlink-folder": "Délier le dossier",
|
||||
"link-all": "Tout lier"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Generated
+1015
-1329
File diff suppressed because it is too large
Load Diff
+13
-14
@@ -154,10 +154,13 @@
|
||||
"@types/dateformat": "^5.0.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "17.0.23",
|
||||
"@types/react": "^17.0.48",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"@types/react": "^18.0.33",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@types/react-outside-click-handler": "^1.3.1",
|
||||
"@types/react-test-renderer": "^17.0.2",
|
||||
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||
"@types/react-window": "^1.8.5",
|
||||
"@types/recursive-readdir": "^2.2.1",
|
||||
"@types/terser-webpack-plugin": "^5.0.4",
|
||||
"@types/use-double-click": "^1.0.1",
|
||||
"@types/webpack-bundle-analyzer": "^4.4.2",
|
||||
@@ -173,7 +176,7 @@
|
||||
"css-loader": "^6.7.1",
|
||||
"css-minimizer-webpack-plugin": "^4.1.0",
|
||||
"detect-port": "^1.3.0",
|
||||
"electron": "^20.0.3",
|
||||
"electron": "^24.0.0",
|
||||
"electron-builder": "^23.6.0",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-notarize": "^1.2.1",
|
||||
@@ -210,7 +213,7 @@
|
||||
"sass-loader": "^12.6.0",
|
||||
"style-loader": "^3.3.1",
|
||||
"tailwind-scrollbar": "^2.0.1",
|
||||
"tailwindcss": "^3.2.4",
|
||||
"tailwindcss": "^3.3.1",
|
||||
"terser-webpack-plugin": "^5.3.5",
|
||||
"ts-jest": "^27.1.5",
|
||||
"ts-loader": "^9.3.0",
|
||||
@@ -226,19 +229,16 @@
|
||||
"dependencies": {
|
||||
"@node-steam/vdf": "^2.2.0",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||
"@types/react-window": "^1.8.5",
|
||||
"@types/recursive-readdir": "^2.2.1",
|
||||
"archiver": "^5.3.1",
|
||||
"dateformat": "^5.0.3",
|
||||
"dot-prop": "^7.2.0",
|
||||
"electron-debug": "^3.2.0",
|
||||
"electron-log": "^4.4.8",
|
||||
"electron-store": "^8.1.0",
|
||||
"electron-updater": "^5.2.4",
|
||||
"electron-updater": "^6.0.3",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"framer-motion": "^8.0.2",
|
||||
"fs-extra": "^10.1.0",
|
||||
"framer-motion": "^10.11.2",
|
||||
"fs-extra": "^11.1.1",
|
||||
"history": "^5.3.0",
|
||||
"is-online": "^9.0.1",
|
||||
"md5-file": "^5.0.0",
|
||||
@@ -249,17 +249,16 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"react-range": "^1.8.14",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-virtualized-auto-sizer": "^1.0.7",
|
||||
"react-virtualized-auto-sizer": "^1.0.12",
|
||||
"react-window": "^1.8.8",
|
||||
"recursive-readdir": "^2.2.3",
|
||||
"regedit": "^5.1.1",
|
||||
"regedit": "^5.1.2",
|
||||
"rfdc": "^1.3.0",
|
||||
"rxjs": "^7.5.6",
|
||||
"rxjs": "^7.8.0",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"semver": "^7.3.8",
|
||||
"tailwind-scrollbar-hide": "^1.1.7",
|
||||
"tailwindcss-scoped-groups": "^2.0.0",
|
||||
"use-delayed-state": "^1.2.0",
|
||||
"use-double-click": "^1.0.5",
|
||||
"use-fit-text": "^2.4.0"
|
||||
},
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "bs-manager",
|
||||
"version": "1.0.1",
|
||||
"version": "1.1.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "bs-manager",
|
||||
"version": "1.0.1",
|
||||
"version": "1.1.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bs-manager",
|
||||
"version": "1.1.0-alpha.2",
|
||||
"version": "1.1.1",
|
||||
"description": "A foundation for scalable desktop apps",
|
||||
"main": "./dist/main/main.js",
|
||||
"author": {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { move } from "fs-extra";
|
||||
import { CopyOptions, copy, ensureDir, move, symlink } from "fs-extra";
|
||||
import { access, mkdir, rm, readdir, unlink, lstat, readlink } from "fs/promises";
|
||||
import path from "path";
|
||||
import { Observable } from "rxjs";
|
||||
import log from "electron-log"
|
||||
import { BsmException } from "shared/models/bsm-exception.model";
|
||||
|
||||
export async function pathExist(path: string): Promise<boolean> {
|
||||
try{
|
||||
@@ -29,7 +30,7 @@ export async function unlinkPath(path: string): Promise<void>{
|
||||
return unlink(path);
|
||||
}
|
||||
|
||||
export async function getFoldersInFolder(folderPath: string): Promise<string[]> {
|
||||
export async function getFoldersInFolder(folderPath: string, opts?: {ignoreSymlinkTargetError?: boolean}): Promise<string[]> {
|
||||
if(!(await pathExist(folderPath))){ return []; }
|
||||
|
||||
const files = await readdir(folderPath, {withFileTypes: true});
|
||||
@@ -40,7 +41,10 @@ export async function getFoldersInFolder(folderPath: string): Promise<string[]>
|
||||
try{
|
||||
const targetPath = await readlink(path.join(folderPath, file.name));
|
||||
return (await lstat(targetPath)).isDirectory() ? path.join(folderPath, file.name) : undefined;
|
||||
}catch(e){
|
||||
}catch(e: any){
|
||||
if(e.code === "ENOENT" && opts?.ignoreSymlinkTargetError === true && !path.extname(file.name)){
|
||||
return path.join(folderPath, file.name);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
@@ -81,6 +85,48 @@ export function moveFolderContent(src: string, dest: string): Observable<Progres
|
||||
});
|
||||
}
|
||||
|
||||
export function isSubdirectory(parent: string, child: string): boolean {
|
||||
const parentNormalized = path.resolve(parent);
|
||||
const childNormalized = path.resolve(child);
|
||||
|
||||
if (parentNormalized === childNormalized) { return false; }
|
||||
|
||||
const relativePath = path.relative(parentNormalized, childNormalized);
|
||||
|
||||
if (path.parse(parentNormalized).root !== path.parse(childNormalized).root) { return false; }
|
||||
|
||||
return relativePath && !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
|
||||
}
|
||||
|
||||
export async function copyDirectoryWithJunctions(src: string, dest: string, options?: CopyOptions): Promise<void> {
|
||||
|
||||
if(isSubdirectory(src, dest)){
|
||||
throw {message: `Cannot copy directory '${src}' into itself '${dest}'.`, code: "COPY_TO_SUBPATH"} as BsmException;
|
||||
}
|
||||
|
||||
await ensureDir(dest);
|
||||
const items = await readdir(src, { withFileTypes: true });
|
||||
|
||||
for (const item of items) {
|
||||
const sourcePath = path.join(src, item.name);
|
||||
const destinationPath = path.join(dest, item.name);
|
||||
|
||||
if (item.isDirectory()) {
|
||||
await copyDirectoryWithJunctions(sourcePath, destinationPath, options);
|
||||
} else if (item.isFile()) {
|
||||
await copy(sourcePath, destinationPath, options);
|
||||
} else if (item.isSymbolicLink()) {
|
||||
if(options?.overwrite){
|
||||
await unlinkPath(destinationPath);
|
||||
}
|
||||
const symlinkTarget = await readlink(sourcePath);
|
||||
const relativePath = path.relative(src, symlinkTarget);
|
||||
const newTarget = path.join(dest, relativePath);
|
||||
await symlink(newTarget, destinationPath, 'junction');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface Progression<T = unknown>{
|
||||
total: number;
|
||||
current: number;
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
export type AllSettledHelperOptions = {
|
||||
keepStructure?: boolean;
|
||||
removeFalsy?: boolean;
|
||||
}
|
||||
|
||||
export async function allSettled<T>(promises: Promise<T>[], options?: AllSettledHelperOptions): Promise<T[]> {
|
||||
const settledPromises = await Promise.allSettled(promises);
|
||||
if(options?.keepStructure){
|
||||
return settledPromises.map(p => p.status === 'fulfilled' ? p.value : null);
|
||||
}
|
||||
|
||||
return settledPromises.reduce((acc, p) => {
|
||||
if(p.status === 'fulfilled'){
|
||||
if(options?.removeFalsy && !p.value){ return acc; }
|
||||
acc.push(p.value);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
@@ -14,6 +14,8 @@ import { FolderLinkerService, LinkOptions } from '../services/folder-linker.serv
|
||||
import { LocalMapsManagerService } from '../services/additional-content/local-maps-manager.service';
|
||||
import { readJSON, writeJSON } from 'fs-extra';
|
||||
import log from "electron-log"
|
||||
import { VersionLinkerAction } from 'renderer/services/version-folder-linker.service';
|
||||
import { VersionFolderLinkerService } from '../services/version-folder-linker.service';
|
||||
|
||||
const ipc = IpcService.getInstance();
|
||||
|
||||
@@ -57,9 +59,7 @@ ipcMain.on("bs-version.clone", async (event, req: IpcRequest<{version: BSVersion
|
||||
|
||||
ipc.on("get-version-full-path", async (req: IpcRequest<BSVersion>, reply) => {
|
||||
const localVersions = BSLocalVersionService.getInstance();
|
||||
reply(from(
|
||||
localVersions.getVersionPath(req.args)
|
||||
));
|
||||
reply(from( localVersions.getVersionPath(req.args) ));
|
||||
});
|
||||
|
||||
ipc.on("relative-version-path-to-full", async (req: IpcRequest<{version: BSVersion, relative: string}>, reply) => {
|
||||
@@ -80,9 +80,9 @@ ipc.on("full-version-path-to-relative", async (req: IpcRequest<{version: BSVersi
|
||||
reply(from(promise));
|
||||
});
|
||||
|
||||
ipc.on("get-linked-folders", async (req: IpcRequest<BSVersion>, reply) => {
|
||||
const localVersions = BSLocalVersionService.getInstance();
|
||||
reply(from(localVersions.getLinkedFolders(req.args)));
|
||||
ipc.on("get-linked-folders", async (req: IpcRequest<{version: BSVersion, options?: { relative?: boolean }}>, reply) => {
|
||||
const versionLinker = VersionFolderLinkerService.getInstance();
|
||||
reply(from(versionLinker.getLinkedFolders(req.args.version, req.args.options)));
|
||||
});
|
||||
|
||||
ipc.on("link-folder", async (req: IpcRequest<{ folder: string, options?: LinkOptions}>, reply) => {
|
||||
@@ -121,6 +121,16 @@ ipc.on("link-folder", async (req: IpcRequest<{ folder: string, options?: LinkOpt
|
||||
|
||||
});
|
||||
|
||||
ipc.on("link-version-folder-action", async (req: IpcRequest<VersionLinkerAction>, reply) => {
|
||||
const versionLinker = VersionFolderLinkerService.getInstance();
|
||||
reply(from(versionLinker.doAction(req.args)));
|
||||
});
|
||||
|
||||
ipc.on("is-version-folder-linked", async (req: IpcRequest<{ version: BSVersion, relativeFolder: string }>, reply) => {
|
||||
const versionLinker = VersionFolderLinkerService.getInstance();
|
||||
reply(from(versionLinker.isFolderLinked(req.args.version, req.args.relativeFolder)));
|
||||
});
|
||||
|
||||
ipc.on("unlink-folder", async (req: IpcRequest<{ folder: string, options?: LinkOptions}>, reply) => {
|
||||
|
||||
req.args.options ??= {};
|
||||
@@ -141,4 +151,9 @@ ipc.on("unlink-folder", async (req: IpcRequest<{ folder: string, options?: LinkO
|
||||
|
||||
});
|
||||
|
||||
ipc.on("relink-all-versions-folders", async (req: IpcRequest<void>, reply) => {
|
||||
const versionLinker = VersionFolderLinkerService.getInstance();
|
||||
reply(from(versionLinker.relinkAllVersionsFolders()));
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -77,15 +77,4 @@ ipcMain.on("open-steam", async (event, request: IpcRequest<void>) => {
|
||||
}).catch((e) => {
|
||||
utils.ipcSend(request.responceChannel, {success: false, error: e});
|
||||
});
|
||||
});
|
||||
|
||||
ipc.on("is-folder-symlink", async (req: IpcRequest<string>, reply) => {
|
||||
const promise = new Promise<boolean>(async resolve => {
|
||||
if(!(await pathExist(req.args))){ return resolve(false); }
|
||||
lstat(req.args, (err, stats) => {
|
||||
if(err){ return resolve(false); }
|
||||
resolve(stats.isSymbolicLink());
|
||||
});
|
||||
});
|
||||
reply(from(promise));
|
||||
});
|
||||
+7
-1
@@ -9,7 +9,7 @@
|
||||
* `./src/main.js` using webpack. This gives us some performance wins.
|
||||
*/
|
||||
import path from 'path';
|
||||
import { app, protocol } from 'electron';
|
||||
import { app, ipcMain, protocol } from 'electron';
|
||||
import log from 'electron-log';
|
||||
import './ipcs';
|
||||
import { UtilsService } from './services/utils.service';
|
||||
@@ -21,6 +21,7 @@ import { LocalPlaylistsManagerService } from './services/additional-content/loca
|
||||
import { LocalModelsManagerService } from './services/additional-content/local-models-manager.service';
|
||||
import { APP_NAME } from './constants';
|
||||
import { BSLauncherService } from './services/bs-launcher.service';
|
||||
import { IpcRequest } from 'shared/models/ipc';
|
||||
|
||||
const isDebug = process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true';
|
||||
|
||||
@@ -100,5 +101,10 @@ else{
|
||||
|
||||
BSLauncherService.getInstance().restoreSteamVR();
|
||||
|
||||
// Log renderer errors
|
||||
ipcMain.on('log-error', (event, args: IpcRequest<any>) => {
|
||||
log.error(args?.args);
|
||||
});
|
||||
|
||||
}).catch(log.error);
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import { Archive } from "../../models/archive.class";
|
||||
import { deleteFolder, ensureFolderExist, getFoldersInFolder, pathExist } from "../../helpers/fs.helpers";
|
||||
import { readFile } from "fs/promises";
|
||||
import { FolderLinkerService } from "../folder-linker.service";
|
||||
import { allSettled } from "../../helpers/promise.helpers";
|
||||
|
||||
export class LocalMapsManagerService {
|
||||
|
||||
@@ -155,12 +156,7 @@ export class LocalMapsManagerService {
|
||||
return mapInfo;
|
||||
});
|
||||
|
||||
const mapsInfo = (await Promise.allSettled(promises)).reduce((acc, mapInfo) => {
|
||||
if(mapInfo.status === "fulfilled" && mapInfo.value){
|
||||
acc.push(mapInfo.value);
|
||||
}
|
||||
return acc;
|
||||
} ,[] as BsmLocalMap[]);
|
||||
const mapsInfo = await allSettled(promises, {removeFalsy: true});
|
||||
|
||||
progression.maps = mapsInfo;
|
||||
|
||||
|
||||
@@ -45,7 +45,6 @@ export class BSLauncherService{
|
||||
}
|
||||
|
||||
public async restoreSteamVR(): Promise<void>{
|
||||
console.log("restoreSteamVR");
|
||||
const steamVrFolder = await this.getSteamVRPath();
|
||||
const steamVrBackup = steamVrFolder + ".bak";
|
||||
if(!await pathExist(steamVrBackup)){ return; }
|
||||
@@ -53,13 +52,13 @@ export class BSLauncherService{
|
||||
}
|
||||
|
||||
public isBsRunning(): boolean{
|
||||
return this.bsProcess?.connected || this.utilsService.taskRunning(BS_EXECUTABLE);
|
||||
return this.bsProcess?.connected || this.utilsService.taskRunning(BS_EXECUTABLE) === true;
|
||||
}
|
||||
|
||||
public async launch(launchOptions: LauchOption): Promise<LaunchResult>{
|
||||
if(launchOptions.version.oculus && !this.oculusService.oculusRunning()){ return "OCULUS_NOT_RUNNING" }
|
||||
if(!launchOptions.version.oculus && !this.steamService.steamRunning()){ return "STEAM_NOT_RUNNING" }
|
||||
if(this.isBsRunning()){ return "BS_ALREADY_RUNNING" }
|
||||
if(launchOptions.version.oculus && this.oculusService.oculusRunning() === false){ return "OCULUS_NOT_RUNNING" }
|
||||
if(!launchOptions.version.oculus && this.steamService.steamRunning() === false){ return "STEAM_NOT_RUNNING" }
|
||||
if(this.isBsRunning() === true){ return "BS_ALREADY_RUNNING" }
|
||||
|
||||
const cwd = await this.localVersionService.getVersionPath(launchOptions.version);
|
||||
const exePath = path.join(cwd, BS_EXECUTABLE);
|
||||
|
||||
@@ -5,7 +5,6 @@ import { SteamService } from "./steam.service";
|
||||
import { BS_APP_ID, OCULUS_BS_DIR } from "../constants";
|
||||
import path from "path";
|
||||
import { createReadStream } from "fs";
|
||||
import fs from "fs-extra";
|
||||
import { ConfigurationService } from "./configuration.service";
|
||||
import { rename } from "fs/promises";
|
||||
import { BsmException } from "shared/models/bsm-exception.model";
|
||||
@@ -13,7 +12,7 @@ import log from "electron-log";
|
||||
import { OculusService } from "./oculus.service";
|
||||
import { DownloadLinkType } from "shared/models/mods";
|
||||
import sanitize from "sanitize-filename";
|
||||
import { deleteFolder, getFoldersInFolder, pathExist } from "../helpers/fs.helpers";
|
||||
import { copyDirectoryWithJunctions, deleteFolder, getFoldersInFolder, pathExist } from "../helpers/fs.helpers";
|
||||
import { FolderLinkerService } from "./folder-linker.service";
|
||||
|
||||
export class BSLocalVersionService{
|
||||
@@ -184,7 +183,7 @@ export class BSLocalVersionService{
|
||||
}
|
||||
|
||||
public async editVersion(version: BSVersion, name: string, color: string): Promise<BSVersion>{
|
||||
if(version.steam || version.oculus){ throw {title: "CantEditSteam", msg: "CantEditSteam"} as BsmException; }
|
||||
if(version.steam || version.oculus){ throw {title: "CantEditSteam", message: "CantEditSteam"} as BsmException; }
|
||||
const oldPath = await this.getVersionPath(version);
|
||||
const editedVersion: BSVersion = version.BSVersion === name
|
||||
? {...version, name: undefined, color}
|
||||
@@ -205,7 +204,7 @@ export class BSLocalVersionService{
|
||||
return editedVersion;
|
||||
}).catch((err: Error) => {
|
||||
log.error("edit version error", err, version, name, color);
|
||||
throw {title: "CantRename", error: err} as BsmException;
|
||||
throw {title: "CantRename", ...err} as BsmException;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -223,12 +222,12 @@ export class BSLocalVersionService{
|
||||
|
||||
if(await pathExist(newPath)){ throw {title: "VersionAlreadExist"} as BsmException; }
|
||||
|
||||
return fs.copy(originPath, newPath, {dereference: true}).then(() => {
|
||||
return copyDirectoryWithJunctions(originPath, newPath).then(() => {
|
||||
this.addCustomVersion(cloneVersion);
|
||||
return cloneVersion;
|
||||
}).catch((err: Error) => {
|
||||
log.error("clone version error", err, version, name, color);
|
||||
throw {title: "CantClone", error: err} as BsmException
|
||||
throw {title: "CantClone", ...err} as BsmException
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ import path from 'path';
|
||||
import { writeFileSync } from 'fs';
|
||||
import { BSVersion } from 'shared/bs-version.interface';
|
||||
import { RequestService } from "./request.service"
|
||||
import isOnline from 'is-online';
|
||||
import { readJSON } from 'fs-extra';
|
||||
import { allSettled } from '../helpers/promise.helpers';
|
||||
|
||||
export class BSVersionLibService{
|
||||
|
||||
@@ -43,16 +43,19 @@ export class BSVersionLibService{
|
||||
writeFileSync(localVersionsPath, JSON.stringify(versions, null, "\t"), {encoding: 'utf-8', flag: 'w'});
|
||||
};
|
||||
|
||||
private async loadBsVersions(): Promise<BSVersion[]>{
|
||||
if(this.bsVersions){ return this.bsVersions; }
|
||||
const [localVersions, remoteVersions] = await Promise.all([
|
||||
this.getLocalVersions(), (await isOnline({timeout: 1500}) && this.getRemoteVersions())
|
||||
]);
|
||||
let resVersions = localVersions;
|
||||
if(remoteVersions && remoteVersions.length){ resVersions = remoteVersions; this.updateLocalVersions(resVersions); }
|
||||
this.bsVersions = resVersions;
|
||||
return this.bsVersions;
|
||||
}
|
||||
private async loadBsVersions(): Promise<BSVersion[]>{
|
||||
|
||||
if(this.bsVersions){ return this.bsVersions; }
|
||||
|
||||
const [localVersions, remoteVersions] = await allSettled([
|
||||
this.getLocalVersions(), this.getRemoteVersions()
|
||||
], {keepStructure: true});
|
||||
|
||||
let resVersions = localVersions;
|
||||
if(remoteVersions && remoteVersions.length){ resVersions = remoteVersions; this.updateLocalVersions(resVersions); }
|
||||
this.bsVersions = resVersions;
|
||||
return this.bsVersions;
|
||||
}
|
||||
|
||||
public async getAvailableVersions(): Promise<BSVersion[]>{
|
||||
const bsVersions = await this.loadBsVersions();
|
||||
|
||||
@@ -3,7 +3,7 @@ import log from "electron-log";
|
||||
import { deleteFolder, ensureFolderExist, moveFolderContent, pathExist, unlinkPath } from "../helpers/fs.helpers";
|
||||
import { lstat, symlink } from "fs/promises";
|
||||
import path from "path";
|
||||
import { copy } from "fs-extra";
|
||||
import { copy, readlink } from "fs-extra";
|
||||
|
||||
export class FolderLinkerService {
|
||||
|
||||
@@ -18,12 +18,12 @@ export class FolderLinkerService {
|
||||
|
||||
private readonly installLocationService = InstallationLocationService.getInstance();
|
||||
|
||||
private readonly sharedFolder: string;
|
||||
|
||||
private constructor(){
|
||||
this.installLocationService = InstallationLocationService.getInstance();
|
||||
}
|
||||
|
||||
this.sharedFolder = this.installLocationService.sharedContentPath;
|
||||
private get sharedFolder(): string{
|
||||
return this.installLocationService.sharedContentPath;
|
||||
}
|
||||
|
||||
private getSharedFolder(folderPath: string, intermediateFolder?: string): string {
|
||||
@@ -48,17 +48,23 @@ export class FolderLinkerService {
|
||||
|
||||
public async linkFolder(folderPath: string, options?: LinkOptions): Promise<void> {
|
||||
|
||||
if(await this.isFolderSymlink(folderPath)){ return; }
|
||||
|
||||
const sharedPath = this.getSharedFolder(folderPath, options?.intermediateFolder);
|
||||
|
||||
await ensureFolderExist(folderPath);
|
||||
if(await this.isFolderSymlink(folderPath)){
|
||||
const isTargetedToSharedPath = await readlink(folderPath).then(target => target === sharedPath).catch(() => false);
|
||||
if(isTargetedToSharedPath){ return; }
|
||||
await unlinkPath(folderPath);
|
||||
return symlink(sharedPath, folderPath, "junction");
|
||||
}
|
||||
|
||||
await ensureFolderExist(sharedPath);
|
||||
|
||||
if(options?.backup === true){
|
||||
await this.backupFolder(folderPath);
|
||||
}
|
||||
|
||||
await ensureFolderExist(folderPath);
|
||||
|
||||
if(options?.keepContents !== false){
|
||||
await moveFolderContent(folderPath, sharedPath).toPromise();
|
||||
}
|
||||
@@ -68,7 +74,7 @@ export class FolderLinkerService {
|
||||
return symlink(sharedPath, folderPath, "junction");
|
||||
}
|
||||
|
||||
public async unlinkFolder(folderPath: string, options?: LinkOptions): Promise<void> {
|
||||
public async unlinkFolder(folderPath: string, options?: UnlinkOptions): Promise<void> {
|
||||
|
||||
if(!(await this.isFolderSymlink(folderPath))){ return; }
|
||||
await unlinkPath(folderPath);
|
||||
@@ -81,10 +87,14 @@ export class FolderLinkerService {
|
||||
return this.restoreFolder(folderPath);
|
||||
}
|
||||
|
||||
if(options.moveContents === true){
|
||||
return moveFolderContent(sharedPath, folderPath).toPromise().then(() => {});
|
||||
}
|
||||
|
||||
if(options?.keepContents === false){ return; }
|
||||
|
||||
await ensureFolderExist(sharedPath);
|
||||
|
||||
|
||||
return copy(sharedPath, folderPath, { errorOnExist: false, recursive: true });
|
||||
}
|
||||
|
||||
@@ -104,5 +114,9 @@ export class FolderLinkerService {
|
||||
export interface LinkOptions {
|
||||
keepContents?: boolean,
|
||||
intermediateFolder?: string,
|
||||
backup?: boolean
|
||||
}
|
||||
backup?: boolean,
|
||||
}
|
||||
|
||||
export interface UnlinkOptions extends LinkOptions {
|
||||
moveContents?: boolean,
|
||||
};
|
||||
@@ -1,10 +1,8 @@
|
||||
import path from "path";
|
||||
import fs from 'fs-extra';
|
||||
import log from "electron-log";
|
||||
import { app } from "electron";
|
||||
import { BsmException } from "shared/models/bsm-exception.model";
|
||||
import ElectronStore from "electron-store";
|
||||
import { ensureFolderExist, pathExist } from "../helpers/fs.helpers";
|
||||
import { copyDirectoryWithJunctions, deleteFolder, ensureFolderExist } from "../helpers/fs.helpers";
|
||||
|
||||
export class InstallationLocationService {
|
||||
|
||||
@@ -48,15 +46,24 @@ export class InstallationLocationService {
|
||||
const oldDir = this.installationDirectory;
|
||||
const newDest = path.join(newDir, this.INSTALLATION_FOLDER);
|
||||
return new Promise<string>(async (resolve, reject) => {
|
||||
if(!(await pathExist(oldDir))){ ensureFolderExist(oldDir); }
|
||||
fs.move(oldDir, newDest, { overwrite: true }).then(() => {
|
||||
|
||||
await ensureFolderExist(oldDir);
|
||||
|
||||
try{
|
||||
await copyDirectoryWithJunctions(oldDir, newDest, {overwrite: true});
|
||||
|
||||
this._installationDirectory = newDir;
|
||||
this.installPathConfig.set(this.STORE_INSTALLATION_PATH_KEY, newDir);
|
||||
resolve(this.installationDirectory);
|
||||
}).catch((err: Error) => {
|
||||
reject({title: "CantMoveFolder", error: err} as BsmException);
|
||||
|
||||
deleteFolder(oldDir);
|
||||
|
||||
return resolve(this.installationDirectory);
|
||||
}
|
||||
catch(err){
|
||||
log.error(err);
|
||||
})
|
||||
reject(err);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ export class OculusService {
|
||||
this.utils = UtilsService.getInstance();
|
||||
}
|
||||
|
||||
public oculusRunning(): boolean{
|
||||
public oculusRunning(): boolean | null{
|
||||
return this.utils.taskRunning("OculusClient.exe");
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ export class RequestService {
|
||||
resolve(JSON.parse(body));
|
||||
});
|
||||
res.on('error', (err) => reject(err))
|
||||
});
|
||||
}).on("error", err => reject(err));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ export class SteamService{
|
||||
return SteamService.instance;
|
||||
}
|
||||
|
||||
public steamRunning(): boolean{
|
||||
public steamRunning(): boolean | null{
|
||||
return this.utils.taskRunning('steam.exe');
|
||||
}
|
||||
|
||||
|
||||
@@ -33,10 +33,16 @@ export class UtilsService{
|
||||
public setMainWindows(windows: Map<AppWindow, BrowserWindow>){ this.windows = windows; }
|
||||
public getMainWindows(win: AppWindow){ return this.windows.get(win); }
|
||||
|
||||
public taskRunning(task: string): boolean{
|
||||
const tasks = spawnSync('tasklist').stdout.toString();
|
||||
return tasks.includes(task);
|
||||
}
|
||||
public taskRunning(task: string): boolean | null{
|
||||
try{
|
||||
const tasks = spawnSync('tasklist').stdout;
|
||||
return tasks.toString().includes(task);
|
||||
}
|
||||
catch(error){
|
||||
log.error(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public ipcSend<T = any>(channel: string, response: IpcResponse<T>): void{
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { getFoldersInFolder, pathExist } from "../helpers/fs.helpers";
|
||||
import path from "path";
|
||||
import { VersionLinkerAction, VersionLinkFolderAction, VersionUnlinkFolderAction } from "renderer/services/version-folder-linker.service";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { LocalMapsManagerService } from "./additional-content/local-maps-manager.service";
|
||||
import { BSLocalVersionService } from "./bs-local-version.service";
|
||||
import { FolderLinkerService, LinkOptions } from "./folder-linker.service";
|
||||
import { allSettled } from "../helpers/promise.helpers";
|
||||
|
||||
export class VersionFolderLinkerService {
|
||||
|
||||
private static instance: VersionFolderLinkerService;
|
||||
|
||||
public static getInstance(): VersionFolderLinkerService{
|
||||
if(!VersionFolderLinkerService.instance){ VersionFolderLinkerService.instance = new VersionFolderLinkerService() }
|
||||
return VersionFolderLinkerService.instance;
|
||||
}
|
||||
|
||||
private readonly localVersion: BSLocalVersionService;
|
||||
private readonly folderLinker: FolderLinkerService;
|
||||
|
||||
private constructor(){
|
||||
this.localVersion = BSLocalVersionService.getInstance();
|
||||
this.folderLinker = FolderLinkerService.getInstance();
|
||||
}
|
||||
|
||||
private specialFolderOption(relativeFolder: string, options: LinkOptions): LinkOptions{
|
||||
if(relativeFolder.includes(LocalMapsManagerService.CUSTOM_LEVELS_FOLDER)){
|
||||
return {...options, intermediateFolder: LocalMapsManagerService.SHARED_MAPS_FOLDER};
|
||||
}
|
||||
|
||||
if(relativeFolder.includes("UserData")){
|
||||
return { ...options, backup: true };
|
||||
}
|
||||
|
||||
return options ?? {};
|
||||
}
|
||||
|
||||
private async isOtherVersionHaveFolderLinked(relativeFolder: string, ignorePath: string): Promise<boolean>{
|
||||
const versions = await this.localVersion.getInstalledVersions();
|
||||
const versionPaths = await allSettled(versions.map(version => this.localVersion.getVersionPath(version)));
|
||||
|
||||
for(const versionPath of versionPaths){
|
||||
const folderPath = this.relativeToFullPath(versionPath, relativeFolder);
|
||||
if(folderPath === ignorePath){ continue; }
|
||||
if(await this.folderLinker.isFolderSymlink(folderPath)){ return true; }
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private relativeToFullPath(parentPath: string, relativePath: string): string{
|
||||
return path.join(parentPath, relativePath);
|
||||
}
|
||||
|
||||
|
||||
public async linkVersionFolder(action: VersionLinkerAction): Promise<boolean>{
|
||||
action.options = this.specialFolderOption(action.relativeFolder, action.options);
|
||||
const versionPath = await this.localVersion.getVersionPath(action.version);
|
||||
const folderPath = this.relativeToFullPath(versionPath, action.relativeFolder);
|
||||
return this.folderLinker.linkFolder(folderPath, action.options).catch(() => false).then(() => true)
|
||||
}
|
||||
|
||||
public async unlinkVersionFolder(action: VersionUnlinkFolderAction): Promise<boolean>{
|
||||
action.options = this.specialFolderOption(action.relativeFolder, action.options);
|
||||
|
||||
const versionPath = await this.localVersion.getVersionPath(action.version);
|
||||
const folderPath = this.relativeToFullPath(versionPath, action.relativeFolder);
|
||||
|
||||
action.options.moveContents = !(await this.isOtherVersionHaveFolderLinked(action.relativeFolder, folderPath));
|
||||
|
||||
return this.folderLinker.unlinkFolder(folderPath, action.options).catch(() => false).then(() => true);
|
||||
}
|
||||
|
||||
public async doAction(action: VersionLinkerAction): Promise<boolean>{
|
||||
if(action.type === "link"){
|
||||
return this.linkVersionFolder(action);
|
||||
}
|
||||
return this.unlinkVersionFolder(action as VersionUnlinkFolderAction);
|
||||
}
|
||||
|
||||
public async isFolderLinked(version: BSVersion, relativeFolder: string): Promise<boolean>{
|
||||
const versionPath = await this.localVersion.getVersionPath(version);
|
||||
const folderPath = this.relativeToFullPath(versionPath, relativeFolder);
|
||||
return this.folderLinker.isFolderSymlink(folderPath);
|
||||
}
|
||||
|
||||
public async getLinkedFolders(version: BSVersion, options?: { relative?: boolean, ignoreSymlinkTargetError?: boolean }): Promise<string[]>{
|
||||
const versionPath = await this.localVersion.getVersionPath(version);
|
||||
const [rootFolders, beatSaberDataFolders] = await Promise.all([
|
||||
getFoldersInFolder(versionPath, {ignoreSymlinkTargetError: options?.ignoreSymlinkTargetError}),
|
||||
getFoldersInFolder(path.join(versionPath, "Beat Saber_Data"), {ignoreSymlinkTargetError: options?.ignoreSymlinkTargetError})
|
||||
]);
|
||||
|
||||
const linkedFolders = await Promise.all([...rootFolders, ...beatSaberDataFolders].map(async folder => {
|
||||
if(!(await this.folderLinker.isFolderSymlink(folder))){ return null; }
|
||||
return folder;
|
||||
}));
|
||||
|
||||
if(options?.relative){
|
||||
return linkedFolders.filter(folder => folder).map(folder => path.relative(versionPath, folder));
|
||||
}
|
||||
|
||||
return linkedFolders.filter(folder => folder);
|
||||
}
|
||||
|
||||
public async relinkAllVersionsFolders(): Promise<void>{
|
||||
const versions = await this.localVersion.getInstalledVersions();
|
||||
|
||||
for(const version of versions){
|
||||
const linkedFolders = await this.getLinkedFolders(version, { relative: true, ignoreSymlinkTargetError: true });
|
||||
const actions = linkedFolders.map(folder => ({ type: "link", version, relativeFolder: folder } as VersionLinkFolderAction));
|
||||
await Promise.all(actions.map(action => this.doAction(action)));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -9,7 +9,7 @@ import { MapsDownloaderService } from "renderer/services/maps-downloader.service
|
||||
import { VariableSizeList } from "react-window"
|
||||
import { MapsRow } from "./maps-row.component"
|
||||
import { BehaviorSubject } from "rxjs"
|
||||
import { debounceTime, last, map, mergeMap, throttleTime } from "rxjs/operators"
|
||||
import { debounceTime, last, map, mergeMap } from "rxjs/operators"
|
||||
import { BeatSaverService } from "renderer/services/thrird-partys/beat-saver.service"
|
||||
import { OsDiagnosticService } from "renderer/services/os-diagnostic.service"
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook"
|
||||
|
||||
@@ -11,7 +11,6 @@ import dateFormat from "dateformat";
|
||||
import { AudioPlayerService } from "renderer/services/audio-player.service";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { map } from "rxjs/operators";
|
||||
import useDelayedState from "use-delayed-state";
|
||||
import equal from "fast-deep-equal/es6";
|
||||
import { getMapZipUrlFromHash } from "renderer/helpers/maps-utils";
|
||||
import { BsmBasicSpinner } from "../shared/bsm-basic-spinner/bsm-basic-spinner.component";
|
||||
@@ -21,6 +20,7 @@ import { MAP_DIFFICULTIES } from "renderer/partials/maps/map-difficulties/map-di
|
||||
import { MAP_DIFFICULTIES_COLORS } from "renderer/partials/maps/map-difficulties/map-difficulties-colors"
|
||||
import useDoubleClick from "use-double-click";
|
||||
import { GlowEffect } from "../shared/glow-effect.component";
|
||||
import { useDelayedState } from "renderer/hooks/use-delayed-state.hook";
|
||||
|
||||
export type ParsedMapDiff = {type: BsvMapDifficultyType, name: string, stars: number}
|
||||
|
||||
|
||||
+9
-8
@@ -17,7 +17,8 @@ import { BsmIcon } from "../svgs/bsm-icon.component"
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook"
|
||||
import { LinkButton } from "./link-button.component"
|
||||
import { debounceTime } from "rxjs/operators"
|
||||
import { FolderLinkerService } from "renderer/services/folder-linker.service"
|
||||
import { VersionFolderLinkerService, VersionLinkerActionListener } from "renderer/services/version-folder-linker.service"
|
||||
|
||||
|
||||
type Props = {
|
||||
version?: BSVersion
|
||||
@@ -28,7 +29,7 @@ export function MapsPlaylistsPanel({version}: Props) {
|
||||
const mapsService = MapsManagerService.getInstance();
|
||||
const mapsDownloader = MapsDownloaderService.getInstance();
|
||||
const osDiagnostic = OsDiagnosticService.getInstance();
|
||||
const linker = FolderLinkerService.getInstance();
|
||||
const linker = VersionFolderLinkerService.getInstance();
|
||||
|
||||
const [tabIndex, setTabIndex] = useState(0);
|
||||
const [mapFilter, setMapFilter] = useState<MapFilter>({});
|
||||
@@ -46,18 +47,18 @@ export function MapsPlaylistsPanel({version}: Props) {
|
||||
|
||||
const sub = mapsService.$mapsLinkingPending(version).pipe(debounceTime(50)).subscribe(setLinkingPending);
|
||||
|
||||
const onMapsLinked = (folder: string) => {
|
||||
if(!folder.includes("CustomLevels")){ return; }
|
||||
const onMapsLinked: VersionLinkerActionListener = (action, linked) => {
|
||||
if(!action.relativeFolder.includes(MapsManagerService.RELATIVE_MAPS_FOLDER)){ return; }
|
||||
loadMapIsLinked();
|
||||
}
|
||||
|
||||
linker.onFolderLinked(onMapsLinked);
|
||||
linker.onFolderUnlinked(onMapsLinked);
|
||||
linker.onVersionFolderLinked(onMapsLinked);
|
||||
linker.onVersionFolderUnlinked(onMapsLinked);
|
||||
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
linker.removeOnFolderLinked(onMapsLinked);
|
||||
linker.removeOnFolderUnlinked(onMapsLinked);
|
||||
linker.removeVersionFolderLinkedListener(onMapsLinked);
|
||||
linker.removeVersionFolderUnlinkedListener(onMapsLinked);
|
||||
}
|
||||
|
||||
}, [version]);
|
||||
|
||||
@@ -21,7 +21,7 @@ export const GuardModal: ModalComponent<string> = ({resolver}) => {
|
||||
<BsmImage className="mx-auto h-24" image={BeatConflict}/>
|
||||
<div className="mb-4">
|
||||
<label className="block font-bold tracking-wide text-gray-800 dark:text-gray-200" htmlFor="guard">{t("modals.guard.inputs.guard-code.label")}</label>
|
||||
<input className="w-full bg-light-main-color-1 dark:bg-main-color-1 px-2 rounded-md py-[2px]" onChange={e => setGuardCode(e.target.value.toUpperCase())} value={guardCode} type="guard" name="guard" id="guard" placeholder={t("modals.guard.inputs.guard-code.placeholder")}/>
|
||||
<input className="w-full bg-light-main-color-1 dark:bg-main-color-1 px-2 rounded-md py-[2px]" onChange={e => setGuardCode(e.target.value.toUpperCase().replaceAll(" ", ""))} value={guardCode} type="guard" name="guard" id="guard" placeholder={t("modals.guard.inputs.guard-code.placeholder")}/>
|
||||
</div>
|
||||
<div className="grid grid-flow-col grid-cols-2 gap-4">
|
||||
<BsmButton typeColor="cancel" className="rounded-md text-center transition-all" onClick={() => {resolver({exitCode: ModalExitCode.CANCELED})}} withBar={false} text="misc.cancel"/>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Tippy from "@tippyjs/react";
|
||||
import { Variants } from "framer-motion";
|
||||
import { version } from "os";
|
||||
import { useEffect, useState } from "react";
|
||||
import { LinkButton } from "renderer/components/maps-mangement-components/link-button.component";
|
||||
import { BsmBasicSpinner } from "renderer/components/shared/bsm-basic-spinner/bsm-basic-spinner.component";
|
||||
@@ -8,10 +9,11 @@ import { BsmIcon } from "renderer/components/svgs/bsm-icon.component";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { BSVersionManagerService } from "renderer/services/bs-version-manager.service";
|
||||
import { ConfigurationService } from "renderer/services/configuration.service";
|
||||
import { FolderLinkerService } from "renderer/services/folder-linker.service";
|
||||
import { IpcService } from "renderer/services/ipc.service";
|
||||
import { ModalComponent } from "renderer/services/modale.service";
|
||||
import { VersionFolderLinkerService, VersionLinkerActionType } from "renderer/services/version-folder-linker.service";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
|
||||
export const ShareFoldersModal: ModalComponent<void, BSVersion> = ({ data }) => {
|
||||
@@ -20,32 +22,18 @@ export const ShareFoldersModal: ModalComponent<void, BSVersion> = ({ data }) =>
|
||||
|
||||
const config = ConfigurationService.getInstance();
|
||||
const ipc = IpcService.getInstance();
|
||||
const linker = VersionFolderLinkerService.getInstance();
|
||||
const versionManager = BSVersionManagerService.getInstance();
|
||||
|
||||
const t = useTranslation();
|
||||
|
||||
const [folders, setFolders] = useState<string[]>([]);
|
||||
const versionPath = useObservable(ipc.sendV2<string>("get-version-full-path", {args: data}));
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const defaultFolders = config.get<string[]>(SHARED_FOLDERS_KEY);
|
||||
setFolders(prev => Array.from(new Set([...prev, ...config.get<string[]>(SHARED_FOLDERS_KEY)]).values()));
|
||||
|
||||
const promises = Promise.allSettled(defaultFolders.map(async (folder) => {
|
||||
return ipc.sendV2<string>("relative-version-path-to-full", {args: {version: data, relative: folder}}).toPromise();
|
||||
}));
|
||||
|
||||
promises.then((res) => {
|
||||
|
||||
const fullPathFolders = res.reduce((acc, curent) => {
|
||||
if(curent.status !== "fulfilled"){ return acc; }
|
||||
acc.push(curent.value);
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
setFolders(prev => Array.from(new Set([...prev, ...fullPathFolders]).values()));
|
||||
});
|
||||
|
||||
ipc.sendV2<string[]>("get-linked-folders", {args: data}).toPromise().then(linkedFolders => {
|
||||
linker.getLinkedFolders(data, {relative: true}).toPromise().then(linkedFolders => {
|
||||
setFolders(prev => Array.from(new Set([...prev, ...linkedFolders]).values()));
|
||||
});
|
||||
|
||||
@@ -57,66 +45,64 @@ export const ShareFoldersModal: ModalComponent<void, BSVersion> = ({ data }) =>
|
||||
config.delete(SHARED_FOLDERS_KEY);
|
||||
return;
|
||||
}
|
||||
|
||||
const promises = Promise.allSettled(folders.map(async folder => {
|
||||
return ipc.sendV2<string>("full-version-path-to-relative", {args: {version: data, fullPath: folder}}).toPromise();
|
||||
}));
|
||||
|
||||
promises.then((res) => {
|
||||
const sharedFolders = res.reduce((acc, curent) => {
|
||||
if(curent.status !== "fulfilled"){ return acc; }
|
||||
acc.push(curent.value);
|
||||
return acc;
|
||||
}, [] as string[]);
|
||||
config.set(SHARED_FOLDERS_KEY, sharedFolders);
|
||||
});
|
||||
|
||||
config.set(SHARED_FOLDERS_KEY, folders);
|
||||
|
||||
}, [folders]);
|
||||
|
||||
const addFolder = async () => {
|
||||
const versionPath = await versionManager.getVersionPath(data).toPromise();
|
||||
const folder = await ipc.sendV2<{canceled: boolean, filePaths: string[]}, string>("choose-folder", {args: versionPath}).toPromise();
|
||||
|
||||
|
||||
if(!folder || folder.canceled || !folder.filePaths?.length){ return; }
|
||||
const linkedFolder = folder.filePaths[0];
|
||||
|
||||
if(folders.includes(linkedFolder)){ return; }
|
||||
const relativeFolder = await ipc.sendV2<string>("full-version-path-to-relative", {args: {version: data, fullPath: folder.filePaths[0]}}).toPromise();
|
||||
|
||||
setFolders(pre => [...pre, linkedFolder]);
|
||||
if(folders.includes(relativeFolder)){ return; }
|
||||
|
||||
setFolders(pre => [...pre, relativeFolder]);
|
||||
}
|
||||
|
||||
const removeFolder = (index: number) => {
|
||||
setFolders((prev) => prev.filter((_, i) => i !== index));
|
||||
}
|
||||
|
||||
const linkAll = () => {
|
||||
folders.forEach(relativeFolder => linker.linkVersionFolder({ version: data, relativeFolder, type: VersionLinkerActionType.Link }))
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="w-full max-w-md ">
|
||||
<h1 className="text-3xl uppercase tracking-wide w-full text-center text-gray-800 dark:text-gray-200">{t("modals.shared-folders.title")}</h1>
|
||||
<p className="my-3">{t("modals.shared-folders.description")}</p>
|
||||
<ul className="flex flex-col gap-1 mb-2 h-[300px] max-h-[300px] overflow-scroll scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900 px-1">
|
||||
{folders.map((folder, index) => (
|
||||
<FolderItem key={index} path={folder} onDelete={() => {removeFolder(index)}}/>
|
||||
<FolderItem key={index} version={data} relativeFolder={folder} onDelete={() => {removeFolder(index)}}/>
|
||||
))}
|
||||
</ul>
|
||||
<div className="w-full h-12 rounded-md flex justify-center items-center cursor-pointer bg-light-main-color-1 dark:bg-main-color-1 hover:bg-light-main-color-3 hover:dark:bg-main-color-3" onClick={addFolder}>
|
||||
<BsmIcon className="aspect-square h-8" icon="add"/>
|
||||
<span className="font-bold">{t("modals.shared-folders.buttons.add-folder")}</span>
|
||||
<div className="grid grid-flow-col gap-3 grid-cols-2">
|
||||
<BsmButton icon="add" className="h-8 rounded-md flex justify-center items-center font-bold bg-light-main-color-1 dark:bg-main-color-1" iconClassName="h-6 aspect-square text-current" onClick={addFolder} withBar={false} text="modals.shared-folders.buttons.add-folder"/>
|
||||
<BsmButton icon="link" className="h-8 rounded-md flex justify-center items-center font-bold" typeColor="primary" iconClassName="h-6 aspect-square text-current -rotate-45" onClick={linkAll} withBar={false} text="modals.shared-folders.buttons.link-all"/>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
// -------- FOLDER ITEM --------
|
||||
|
||||
type FolderProps = {
|
||||
path: string;
|
||||
version: BSVersion;
|
||||
relativeFolder: string;
|
||||
onDelete?: () => void;
|
||||
}
|
||||
|
||||
const FolderItem = ({path, onDelete}: FolderProps) => {
|
||||
const FolderItem = ({version, relativeFolder, onDelete}: FolderProps) => {
|
||||
|
||||
const linker = FolderLinkerService.getInstance();
|
||||
const linker = VersionFolderLinkerService.getInstance();
|
||||
|
||||
const t = useTranslation();
|
||||
|
||||
const name = path.split("\\").at(-1);
|
||||
const name = relativeFolder.split("\\").at(-1);
|
||||
const color = useThemeColor("first-color");
|
||||
|
||||
const variants: Variants = {
|
||||
@@ -124,35 +110,45 @@ const FolderItem = ({path, onDelete}: FolderProps) => {
|
||||
tap: {rotate: 45}
|
||||
};
|
||||
|
||||
const pending = useObservable(linker.isPending(path));
|
||||
const processing = useObservable(linker.isProcessing(path));
|
||||
const pending = useObservable(linker.isPending(version, relativeFolder));
|
||||
const processing = useObservable(linker.isProcessing(version, relativeFolder));
|
||||
const linkDisabled = pending || processing;
|
||||
|
||||
const [linked, setLinked] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if(pending){ return; }
|
||||
loadFolderIsLinked();
|
||||
}, [path]);
|
||||
}, [version, relativeFolder, pending]);
|
||||
|
||||
const loadFolderIsLinked = () => {
|
||||
linker.isFolderLinked(path).toPromise().then(setLinked);
|
||||
linker.isVersionFolderLinked(version, relativeFolder).toPromise().then(setLinked);
|
||||
}
|
||||
|
||||
const onClickLink = () => {
|
||||
if(linked){
|
||||
return linker.unlinkFolder(path).toPromise().then(loadFolderIsLinked);
|
||||
return linker.unlinkVersionFolder({
|
||||
version,
|
||||
relativeFolder,
|
||||
type: VersionLinkerActionType.Unlink,
|
||||
})
|
||||
}
|
||||
return linker.linkFolder(path).toPromise().then(loadFolderIsLinked);
|
||||
|
||||
return linker.linkVersionFolder({
|
||||
version,
|
||||
relativeFolder,
|
||||
type: VersionLinkerActionType.Link,
|
||||
})
|
||||
}
|
||||
|
||||
const cancelLink = () => {
|
||||
linker.cancelFolder(path);
|
||||
const cancelLink = () => {
|
||||
linker.cancelAction(version, relativeFolder);
|
||||
loadFolderIsLinked();
|
||||
}
|
||||
|
||||
return (
|
||||
<li className="w-full h-12 rounded-md shrink-0 flex flex-row items-center justify-between px-2 font-bold bg-light-main-color-1 dark:bg-main-color-1">
|
||||
<span className="cursor-help" title={path}>{name}</span>
|
||||
<span className="cursor-help" title={relativeFolder}>{name}</span>
|
||||
<div className="flex flex-row gap-1.5">
|
||||
<Tippy placement="left" content={t(`modals.shared-folders.buttons.${linked ? "unlink-folder" : "link-folder"}`)} arrow={false}>
|
||||
<LinkButton variants={variants} linked={linked} disabled={linkDisabled} whileHover="hover" whileTap="tap" className="p-0.5 h-7 shrink-0 aspect-square blur-0 cursor-pointer hover:brightness-75" onClick={onClickLink}/>
|
||||
|
||||
@@ -24,7 +24,7 @@ export function BsmProgressBar() {
|
||||
|
||||
return (
|
||||
<AnimatePresence> { visible &&
|
||||
<motion.div initial={{y: "120%"}} animate={{y:"0%"}} exit={{y:"120%"}} className="w-full absolute h-14 flex justify-center items-center bottom-2 pointer-events-none" style={style}>
|
||||
<motion.div initial={{y: "120%"}} animate={{y:"0%"}} exit={{y:"120%"}} className="w-full absolute h-14 flex justify-center items-center bottom-2 pointer-events-none z-10" style={style}>
|
||||
<div className={`flex items-center content-center justify-center bottom-9 z-10 rounded-full bg-light-main-color-2 dark:bg-main-color-2 shadow-center shadow-black transition-all duration-300 ${!progressValue && "h-14 w-14 "} ${!!progressValue && "h-5 w-3/4 p-[6px]"}`}>
|
||||
{ !!progressValue && (
|
||||
<div className="relative h-full w-full rounded-full bg-black">
|
||||
|
||||
@@ -15,19 +15,19 @@ export function SupporterItem({supporter, delay}: Props) {
|
||||
const additionnalStyles: CSSProperties = (() => {
|
||||
const commonStyles: CSSProperties = {backgroundImage: `url(${txtBg})`, backgroundRepeat: "no-repeat", backgroundPosition: "center"}
|
||||
if(supporter.type === "gold"){ return {color: "#ffe270", textShadow: "0px 0px 10px #ffdd59", backgroundSize: "70% 15px", ...commonStyles}; }
|
||||
if(supporter.type === "diamond"){ return {color: "#e574fc", textShadow: "0px 0px 15px #e056fd", backgroundSize: "70% 19px", ...commonStyles}; }
|
||||
if(supporter.type === "diamond"){ return {color: "#0ef2f8", textShadow: "0px 0px 15px #0ef2f8", backgroundSize: "70% 19px", ...commonStyles}; }
|
||||
if(supporter.type === "sponsor"){ return {color: "#0be881", textShadow: "0px 0px 15px #0be881", backgroundSize: "70% 19px", ...commonStyles}; }
|
||||
return {};
|
||||
})();
|
||||
|
||||
const renderSpan = () => {
|
||||
return <motion.span className="text-2xl font-bold px-3 pb-1" style={additionnalStyles} initial={{y: "100%", opacity: 0}} animate={{y: 0, opacity: 1}} transition={{delay: delay}}>{supporter.username}</motion.span>;
|
||||
return <motion.span className={`text-2xl font-bold px-3 pb-1 ${supporter.link && "cursor-pointer underline"}`} style={additionnalStyles} onClick={openSupporterLink} initial={{y: "100%", opacity: 0}} animate={{y: 0, opacity: 1}} transition={{delay: delay}}>{supporter.username}</motion.span>;
|
||||
}
|
||||
|
||||
const renderItem = () => {
|
||||
if(supporter.type !== "sponsor"){ return renderSpan(); }
|
||||
return (
|
||||
<motion.div className={`flex flex-col justify-center items-center mx-4 ${supporter.link && "cursor-pointer"}`} onClick={openSupporterLink} initial={{y: "100%", opacity: 0}} animate={{y: 0, opacity: 1}} transition={{delay: delay}}>
|
||||
<motion.div className={`flex flex-col justify-center items-center mx-4 ${supporter.link && "cursor-pointer underline"}`} onClick={openSupporterLink} initial={{y: "100%", opacity: 0}} animate={{y: 0, opacity: 1}} transition={{delay: delay}}>
|
||||
<img className="max-w-xs max-h-52 mb-2" src={supporter.img}/>
|
||||
{renderSpan()}
|
||||
</motion.div>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useClickOutside } from "renderer/hooks/use-click-outside.hook";
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
import { getCorrectTextColor } from "renderer/helpers/correct-text-color";
|
||||
|
||||
type BsmButtonType = "primary"|"success"|"cancel"|"error";
|
||||
type BsmButtonType = "primary"|"secondary"|"success"|"cancel"|"error";
|
||||
|
||||
type Props = {
|
||||
className?: string,
|
||||
@@ -36,7 +36,7 @@ export function BsmButton({className, style, imgClassName, iconClassName, icon,
|
||||
const ref = useRef(null);
|
||||
useClickOutside(ref, onClickOutside);
|
||||
|
||||
const primaryColor = typeColor === "primary" && useThemeColor("first-color");
|
||||
const primaryColor = typeColor === "primary" ? useThemeColor("first-color") : typeColor === "secondary" ? secondColor : undefined;
|
||||
|
||||
const textColor = (() => {
|
||||
if(primaryColor){ return getCorrectTextColor(primaryColor); }
|
||||
@@ -57,7 +57,7 @@ export function BsmButton({className, style, imgClassName, iconClassName, icon,
|
||||
return (
|
||||
<div ref={ref} onClick={handleClick} title={t(title)} className={`${className} overflow-hidden cursor-pointer group ${(!withBar && !disabled && (!!typeColor || !!color)) && "hover:brightness-[1.15]"} ${disabled && "brightness-75 cursor-not-allowed"} ${renderTypeColor}`} style={{...style, backgroundColor: primaryColor || color}}>
|
||||
{ image && <BsmImage image={image} className={imgClassName}/> }
|
||||
{ icon && <BsmIcon icon={icon} className={iconClassName ?? "h-full w-full text-gray-800 dark:text-white"} style={{color: iconColor}}/> }
|
||||
{ icon && <BsmIcon icon={icon} className={iconClassName ?? "h-full w-full text-gray-800 dark:text-white"} style={{color: iconColor || textColor}}/> }
|
||||
{text && (type === "submit" ? <button type="submit" className={textClassName || "h-full w-full"} style={{...(!!textColor && {color: textColor})}}>{t(text)}</button> : <span className={textClassName} style={{...(!!textColor && {color: `${textColor}`})}}>{t(text)}</span>)}
|
||||
{ withBar && (
|
||||
<div className="absolute bottom-0 left-0 w-full h-1 bg-current" style={{color: secondColor}}>
|
||||
|
||||
@@ -51,12 +51,13 @@ import { SyncIcon } from "./icons/sync-icon.component";
|
||||
import { VolumeUpIcon } from "./icons/volume-up-icon.component";
|
||||
import { VolumeOffIcon } from "./icons/volume-off-icon.component";
|
||||
import { VolumeDownIcon } from "./icons/volume-down-icon.component";
|
||||
import { GermanIcon } from "./flags/german-icon.component";
|
||||
|
||||
export type BsmIconType = BsvMapCharacteristic | (
|
||||
"settings"|"trash"|"favorite"|"folder"|"bsNote"|"check"|"three-dots"|"twitch"|"eye"|"play"|"checkCircleIcon"|"discord"|"info"|
|
||||
"terminal"|"desktop"|"oculus"|"add"|"cross"|"task"|"github"|"close"|"thumbUpFill"|"timerFill"|"pause"|"twitter"|"sync"|
|
||||
"copy"|"steam"|"edit"|"export"|"patreon"|"search"|"bsMapDifficulty"|"link"|"unlink"|"download"|"filter"|"mee6"|"volume-up"|"volume-off"|"volume-down"|
|
||||
"fr-FR-flag"|"es-ES-flag"|"en-US-flag"|"en-EN-flag"
|
||||
"fr-FR-flag"|"es-ES-flag"|"en-US-flag"|"en-EN-flag"|"de-DE-flag"
|
||||
);
|
||||
|
||||
export const BsmIcon = memo(({className, icon, style}: {className?: string, icon: BsmIconType, style?: CSSProperties}) => {
|
||||
@@ -76,6 +77,7 @@ export const BsmIcon = memo(({className, icon, style}: {className?: string, icon
|
||||
if(icon === "es-ES-flag"){ return <SpainIcon className={className} style={style}/> }
|
||||
if(icon === "en-US-flag"){ return <UsaIcon className={className} style={style}/> }
|
||||
if(icon === "en-EN-flag"){ return <UkIcon className={className} style={style}/> }
|
||||
if(icon === "de-DE-flag"){ return <GermanIcon className={className} style={style}/> }
|
||||
if(icon === "task"){ return <TaskIcon className={className} style={style}/> }
|
||||
if(icon === "copy"){ return <CopyIcon className={className} style={style}/> }
|
||||
if(icon === "steam"){ return <SteamIcon className={className} style={style}/> }
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { CSSProperties } from "react";
|
||||
|
||||
export function GermanIcon(props: {className?: string, style?: CSSProperties}) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="71px" height="48px" viewBox="0 0 71 48" version="1.1" className={props.className} style={props.style}>
|
||||
<defs/>
|
||||
<g id="Flags" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(-239.000000, -805.000000)">
|
||||
<g transform="translate(70.000000, 70.000000)" fill-rule="nonzero" id="Germany">
|
||||
<g transform="translate(169.000000, 735.000000)">
|
||||
<g id="Bolivia-9">
|
||||
<path d="M0.52,7 L0.52,16 L70.52,16 L70.52,7 C70.52,3.33282926 67.5471707,0.36 63.88,0.36 L7.17,0.36 C5.40723053,0.357345224 3.71575038,1.05574249 2.46834517,2.3012719 C1.22093997,3.54680131 0.519998001,5.23722853 0.52,7 Z" id="Shape" fill="#434A54"/>
|
||||
<polygon id="Shape" fill="#ED5565" points="0.52 31.74 70.52 31.74 70.52 16.07 0.52 16.07"/>
|
||||
<path d="M0.52,40.76 C0.52,44.4271707 3.49282926,47.4 7.16,47.4 L63.88,47.4 C67.5471707,47.4 70.52,44.4271707 70.52,40.76 L70.52,31.76 L0.52,31.76 L0.52,40.76 Z" id="Shape" fill="#F6BB42"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { CSSProperties } from "react";
|
||||
|
||||
export function LinkIcon(props: {className?: string, style?: CSSProperties}) {
|
||||
return (
|
||||
<svg className={props.className} style={props.style} stroke="currentColor" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" strokeWidth="48" strokeLinecap="round" strokeLinejoin="round">
|
||||
<svg className={props.className} style={props.style} stroke="currentColor" fill="none" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" strokeWidth="48" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M200.66,352H144a96,96,0,0,1,0-192h55.41"/>
|
||||
<path d="M312.59,160H368a96,96,0,0,1,0,192H311.34"/>
|
||||
<line x1="169.07" x2="344.93" y1="256" y2="256"/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useObservable } from 'renderer/hooks/use-observable.hook';
|
||||
import { useThemeColor } from 'renderer/hooks/use-theme-color.hook';
|
||||
import { AudioPlayerService } from 'renderer/services/audio-player.service';
|
||||
@@ -17,9 +17,17 @@ export default function TitleBar({template = "index.html"} : {template: AppWindo
|
||||
const audio = AudioPlayerService.getInstance();
|
||||
|
||||
const volume = useObservable(audio.volume$, audio.volume);
|
||||
const playing = useObservable(audio.playing$);
|
||||
const color = useThemeColor("first-color");
|
||||
|
||||
const [previewVersion, setPreviewVersion] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
ipcService.send<string>("current-version").then(res => {
|
||||
if(res.data.toLocaleLowerCase().includes("alpha")){ return setPreviewVersion("ALPHA"); }
|
||||
if(res.data.toLocaleLowerCase().includes("beta")){ return setPreviewVersion("BETA"); }
|
||||
});
|
||||
})
|
||||
|
||||
const [maximized, setMaximized] = useState(false);
|
||||
|
||||
const closeWindow = () => {
|
||||
@@ -57,6 +65,7 @@ export default function TitleBar({template = "index.html"} : {template: AppWindo
|
||||
<div id="drag-region" className='grow basis-0 h-full'>
|
||||
<div id="window-title" className='pl-1'>
|
||||
<span className='text-gray-800 dark:text-gray-100 font-bold text-xs italic'>BSManager</span>
|
||||
{ previewVersion && <span className='bg-main-color-1 text-white dark:text-black dark:bg-white rounded-full ml-1 text-[10px] italic px-1 uppercase h-3.5 font-bold'>{previewVersion}</span> }
|
||||
</div>
|
||||
</div>
|
||||
<div id="window-controls" className="h-full flex shrink-0 items-center">
|
||||
|
||||
@@ -17,6 +17,7 @@ import { useInView } from "framer-motion";
|
||||
import { ModalExitCode, ModalService } from "renderer/services/modale.service";
|
||||
import { ModsDisclaimerModal } from "renderer/components/modal/modal-types/mods-disclaimer-modal.component";
|
||||
import { OsDiagnosticService } from "renderer/services/os-diagnostic.service";
|
||||
import { lt } from "semver";
|
||||
|
||||
export function ModsSlide({version, onDisclamerDecline}: {version: BSVersion, onDisclamerDecline: () => void}) {
|
||||
|
||||
@@ -68,7 +69,12 @@ export function ModsSlide({version, onDisclamerDecline}: {version: BSVersion, on
|
||||
|
||||
const installMods = () => {
|
||||
if(installing){ return; }
|
||||
modsManager.installMods(modsSelected, version).then(() => {
|
||||
const modsToInstall = modsSelected.filter(mod => {
|
||||
const corespondingMod = modsAvailable.get(mod.category).find(availabeMod => availabeMod._id === mod._id);
|
||||
if(lt(corespondingMod.version, mod.version)){ return false; }
|
||||
return true;
|
||||
})
|
||||
modsManager.installMods(modsToInstall, version).then(() => {
|
||||
loadMods();
|
||||
});
|
||||
}
|
||||
@@ -129,32 +135,49 @@ export function ModsSlide({version, onDisclamerDecline}: {version: BSVersion, on
|
||||
if(modsAvailable){
|
||||
setDownloadWidth(downloadRef?.current?.offsetWidth)
|
||||
}
|
||||
}, [modsAvailable])
|
||||
}, [modsAvailable]);
|
||||
|
||||
const renderContent = () => {
|
||||
if(!isOnline){ return <ModStatus text="pages.version-viewer.mods.no-internet" image={BeatConflictImg}/> }
|
||||
if(!modsAvailable){ return <ModStatus text="pages.version-viewer.mods.loading-mods" image={BeatWaitingImg} spin/> }
|
||||
if(!modsAvailable.size){ return <ModStatus text="pages.version-viewer.mods.mods-not-available" image={BeatConflictImg}/> }
|
||||
return (
|
||||
<>
|
||||
<div className="grow overflow-scroll w-full min-h-0 scrollbar-thin scrollbar-thumb-neutral-900 scrollbar-thumb-rounded-full">
|
||||
<ModsGrid modsMap={modsAvailable} installed={modsInstalled} modsSelected={modsSelected} onModChange={handleModChange} moreInfoMod={moreInfoMod} onWantInfos={handleMoreInfo}/>
|
||||
</div>
|
||||
<div className="h-10 shrink-0 flex items-center justify-between px-3">
|
||||
<BsmButton className="text-center rounded-md px-2 py-[2px]" text="pages.version-viewer.mods.buttons.more-infos" typeColor="cancel" withBar={false} disabled={!moreInfoMod} onClick={handleOpenMoreInfo} style={{width: downloadWith}}/>
|
||||
<div ref={downloadRef}>
|
||||
<BsmButton className="text-center rounded-md px-2 py-[2px]" text="pages.version-viewer.mods.buttons.install-or-update" withBar={false} disabled={installing} typeColor="primary" onClick={installMods}/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div ref={ref} className='shrink-0 w-full h-full px-8 pb-7 flex justify-center'>
|
||||
<div className='relative flex flex-col grow-0 bg-light-main-color-2 dark:bg-main-color-2 h-full w-full rounded-md shadow-black shadow-center overflow-hidden'>
|
||||
{isOnline && modsAvailable ? (
|
||||
<>
|
||||
<div className="grow overflow-scroll w-full min-h-0 scrollbar-thin scrollbar-thumb-neutral-900 scrollbar-thumb-rounded-full">
|
||||
<ModsGrid modsMap={modsAvailable} installed={modsInstalled} modsSelected={modsSelected} onModChange={handleModChange} moreInfoMod={moreInfoMod} onWantInfos={handleMoreInfo}/>
|
||||
</div>
|
||||
<div className="h-10 shrink-0 flex items-center justify-between px-3">
|
||||
<BsmButton className="text-center rounded-md px-2 py-[2px]" text="pages.version-viewer.mods.buttons.more-infos" typeColor="cancel" withBar={false} disabled={!moreInfoMod} onClick={handleOpenMoreInfo} style={{width: downloadWith}}/>
|
||||
<div ref={downloadRef}>
|
||||
<BsmButton className="text-center rounded-md px-2 py-[2px]" text="pages.version-viewer.mods.buttons.install-or-update" withBar={false} disabled={installing} typeColor="primary" onClick={installMods}/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="w-full h-full flex flex-col items-center justify-center text-gray-800 dark:text-gray-200">
|
||||
<img className={`w-32 h-32 ${isOnline && "spin-loading"}`} src={isOnline ? BeatWaitingImg : BeatConflictImg} alt=" "/>
|
||||
<span className="text-xl mt-3 h-0 italic">{t(isOnline ? "pages.version-viewer.mods.loading-mods" : "pages.version-viewer.mods.no-internet")}</span>
|
||||
</div>
|
||||
)}
|
||||
{renderContent()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
function ModStatus({text, image, spin = false}: {text: string, image: string, spin?: boolean}) {
|
||||
|
||||
const t = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col items-center justify-center text-gray-800 dark:text-gray-200">
|
||||
<img className={`w-32 h-32 ${spin ? "spin-loading": ""}`} src={image} alt=" "/>
|
||||
<span className="text-xl mt-3 h-0 italic">{t(text)}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ export const defaultConfiguration: {[key in DefaultConfigKey]: any} = {
|
||||
"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"],
|
||||
"supported_languages": ["en-US", "en-EN", "fr-FR", "es-ES", "de-DE"],
|
||||
"default_mods": ["SongCore", "WhyIsThereNoLeaderboard", "BeatSaverDownloader", "BeatSaverVoting", "PlaylistManager"],
|
||||
"default-shared-folders": [
|
||||
"Beat Saber_Data\\CustomLevels",
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
|
||||
// Code improved from : https://www.npmjs.com/package/use-delayed-state
|
||||
|
||||
export function useDelayedState<T = unknown>(initialState: T): [T, (newState: T, delay: number) => void, () => void] {
|
||||
|
||||
const [state, setState] = useState(initialState);
|
||||
const timeoutRef = useRef<NodeJS.Timeout>();
|
||||
|
||||
const setStateAfter = (newState: T, delay: number) => {
|
||||
if (delay === 0 || delay === undefined) {
|
||||
return setState(newState);
|
||||
}
|
||||
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setState(newState);
|
||||
timeoutRef.current = null;
|
||||
}, delay);
|
||||
}
|
||||
|
||||
const cancelSetState = () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (timeoutRef.current) { clearTimeout(timeoutRef.current); }
|
||||
}
|
||||
}, []);
|
||||
|
||||
return [state, setStateAfter, cancelSetState]
|
||||
}
|
||||
@@ -2,12 +2,19 @@ import { createRoot } from 'react-dom/client';
|
||||
import { HashRouter } from 'react-router-dom';
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import './index.css'
|
||||
import { IpcService } from './services/ipc.service';
|
||||
|
||||
const launcherContainer = document.getElementById('launcher');
|
||||
const oneclickDownloadMapContainer = document.getElementById('oneclick-download-map');
|
||||
const oneclickDownloadPlaylistContainer = document.getElementById('oneclick-download-playlist');
|
||||
const oneclickDownloadModelContainer = document.getElementById('oneclick-download-model');
|
||||
|
||||
const ipc = IpcService.getInstance();
|
||||
|
||||
window.onerror = (...data) => {
|
||||
ipc.sendLazy('log-error', {args: data});
|
||||
}
|
||||
|
||||
if(launcherContainer){
|
||||
import("./windows/Launcher").then(reactWindow => {
|
||||
createRoot(launcherContainer).render(<reactWindow.default/>);
|
||||
|
||||
@@ -5,9 +5,7 @@ import { RadioItem, SettingRadioArray } from "renderer/components/settings/setti
|
||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import { BsmIconType } from "renderer/components/svgs/bsm-icon.component";
|
||||
import { DefaultConfigKey, ThemeConfig } from "renderer/config/default-configuration.config";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
import { AuthUserService } from "renderer/services/auth-user.service";
|
||||
import { BsDownloaderService } from "renderer/services/bs-downloader.service";
|
||||
import { ConfigurationService } from "renderer/services/configuration.service"
|
||||
import { I18nService } from "renderer/services/i18n.service";
|
||||
@@ -26,11 +24,13 @@ import modelSaberIcon from "../../../assets/images/third-party-icons/model-saber
|
||||
import beatSaverIcon from "../../../assets/images/third-party-icons/beat-saver.png";
|
||||
import beastSaberIcon from "../../../assets/images/third-party-icons/beast-saber.png";
|
||||
import scoreSaberIcon from "../../../assets/images/third-party-icons/score-saber.png";
|
||||
import beatleaderIcon from "../../../assets/images/third-party-icons/beat-leader.png";
|
||||
import Tippy from '@tippyjs/react';
|
||||
import { MapsManagerService } from "renderer/services/maps-manager.service";
|
||||
import { PlaylistsManagerService } from "renderer/services/playlists-manager.service";
|
||||
import { ModelsManagerService } from "renderer/services/models-manager.service";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { VersionFolderLinkerService } from "renderer/services/version-folder-linker.service";
|
||||
|
||||
export function SettingsPage() {
|
||||
|
||||
@@ -46,6 +46,7 @@ export function SettingsPage() {
|
||||
const mapsManager = MapsManagerService.getInstance();
|
||||
const playlistsManager = PlaylistsManagerService.getInstance();
|
||||
const modelsManager = ModelsManagerService.getInstance();
|
||||
const versionLinker = VersionFolderLinkerService.getInstance();
|
||||
|
||||
const {firstColor, secondColor} = useThemeColor();
|
||||
|
||||
@@ -103,40 +104,55 @@ export function SettingsPage() {
|
||||
setLanguageSelected(languagesItems.find(l => l.value === i18nService.currentLanguage).id);
|
||||
}
|
||||
|
||||
const setDefaultInstallationFolder = () => {
|
||||
if(!progressBarService.require()){ return; }
|
||||
const setDefaultInstallationFolder = () => {
|
||||
if(!progressBarService.require()){ return; }
|
||||
|
||||
modalService.openModal(InstallationFolderModal).then(async res => {
|
||||
if(res.exitCode !== ModalExitCode.COMPLETED){ return; }
|
||||
modalService.openModal(InstallationFolderModal).then(async res => {
|
||||
if(res.exitCode !== ModalExitCode.COMPLETED){ return; }
|
||||
|
||||
const fileChooserRes = await ipcService.sendV2<{canceled: boolean, filePaths: string[]}>("choose-folder").toPromise();
|
||||
const fileChooserRes = await ipcService.sendV2<{canceled: boolean, filePaths: string[]}>("choose-folder").toPromise();
|
||||
|
||||
if(!fileChooserRes.canceled && fileChooserRes.filePaths?.length){
|
||||
progressBarService.showFake(.008);
|
||||
downloaderService.setInstallationFolder(fileChooserRes.filePaths[0]).then(res => {
|
||||
setTimeout(() => {
|
||||
progressBarService.complete();
|
||||
setTimeout(() => progressBarService.hide(true), 1000);
|
||||
}, 1000);
|
||||
if(res.success){
|
||||
setInstallationFolder(res.data);
|
||||
notificationService.notifySuccess({title: "notifications.settings.move-folder.success.titles.transfer-finished", duration: 3000});
|
||||
}
|
||||
else{
|
||||
notificationService.notifyError({title: "notifications.settings.move-folder.errors.titles.transfer-failed"});
|
||||
}
|
||||
});
|
||||
}
|
||||
if(!fileChooserRes.canceled && fileChooserRes.filePaths?.length){
|
||||
progressBarService.showFake(.008);
|
||||
|
||||
});
|
||||
}
|
||||
notificationService.notifySuccess({title: "notifications.settings.move-folder.success.titles.transfer-started", desc: "notifications.settings.move-folder.success.descs.transfer-started"});
|
||||
|
||||
downloaderService.setInstallationFolder(fileChooserRes.filePaths[0]).then(async res => {
|
||||
setTimeout(() => {
|
||||
progressBarService.complete();
|
||||
setTimeout(() => progressBarService.hide(true), 1000);
|
||||
}, 1000);
|
||||
|
||||
if(res.success){
|
||||
setInstallationFolder(res.data);
|
||||
|
||||
notificationService.notifySuccess({title: "notifications.settings.move-folder.success.titles.transfer-finished", duration: 3000});
|
||||
|
||||
versionLinker.relinkAllVersionsFolders().toPromise().catch(() => {
|
||||
notificationService.notifyError({title: "notifications.types.error", desc: "notifications.settings.move-folder.errors.descs.restore-linked-folders", duration: 15_000});
|
||||
});
|
||||
}
|
||||
else{
|
||||
|
||||
if(res?.error?.code === "COPY_TO_SUBPATH"){
|
||||
notificationService.notifyError({title: "notifications.settings.move-folder.errors.titles.transfer-failed", desc: "notifications.settings.move-folder.errors.descs.COPY_TO_SUBPATH", duration: 10_000});
|
||||
return;
|
||||
}
|
||||
|
||||
notificationService.notifyError({title: "notifications.settings.move-folder.errors.titles.transfer-failed"});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const toogleShowSupporters = () => {
|
||||
setShowSupporters(show => !show);
|
||||
}
|
||||
|
||||
const openSupportPage = () => linkOpener.open("https://mee6.gg/m/bsmanager");
|
||||
const openSupportPage = () => linkOpener.open("https://www.patreon.com/bsmanager");
|
||||
const openGithub = () => linkOpener.open("https://github.com/Zagrios/bs-manager");
|
||||
const openReportBug = () => linkOpener.open("https://github.com/Zagrios/bs-manager/issues/new?assignees=Zagrios&labels=bug&template=-bug--bug-report.md&title=%5BBUG%5D+%3A+");
|
||||
const openRequestFeatures = () => linkOpener.open("https://github.com/Zagrios/bs-manager/issues/new?assignees=Zagrios&labels=enhancement&template=-feat---feature-request.md&title=%5BFEAT.%5D+%3A+");
|
||||
@@ -226,6 +242,9 @@ export function SettingsPage() {
|
||||
<Tippy content="ScoreSaber" placement="top" className="font-bold bg-main-color-3" arrow={false} duration={[200, 0]}>
|
||||
<BsmImage className="h-8 cursor-pointer" image={scoreSaberIcon} onClick={e => {e.stopPropagation(); linkOpener.open("https://scoresaber.com/")}}/>
|
||||
</Tippy>
|
||||
<Tippy content="BeatLeader" placement="top" className="font-bold bg-main-color-3" arrow={false} duration={[200, 0]}>
|
||||
<BsmImage className="h-8 cursor-pointer" image={beatleaderIcon} onClick={e => {e.stopPropagation(); linkOpener.open("https://www.beatleader.xyz/")}}/>
|
||||
</Tippy>
|
||||
<Tippy content="ModelSaber" placement="top" className="font-bold bg-main-color-3" arrow={false} duration={[200, 0]}>
|
||||
<BsmImage className="h-8 cursor-pointer" image={modelSaberIcon} onClick={e => {e.stopPropagation(); linkOpener.open("https://modelsaber.com/")}}/>
|
||||
</Tippy>
|
||||
@@ -247,6 +266,9 @@ export function SettingsPage() {
|
||||
<Tippy content="ScoreSaber" placement="top" className="font-bold bg-main-color-3" arrow={false} duration={[200, 0]}>
|
||||
<BsmImage className="h-8 cursor-pointer" image={scoreSaberIcon} onClick={e => {e.stopPropagation(); linkOpener.open("https://scoresaber.com/")}}/>
|
||||
</Tippy>
|
||||
<Tippy content="BeatLeader" placement="top" className="font-bold bg-main-color-3" 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={() => tooglePlaylistsDeepLinks()}>
|
||||
@@ -282,7 +304,7 @@ export function SettingsPage() {
|
||||
|
||||
<SettingContainer title="pages.settings.patreon.title" description="pages.settings.patreon.description">
|
||||
<div className="flex gap-2">
|
||||
<BsmButton disabled color="#ef4444" className="flex w-fit rounded-md h-8 px-2 font-bold py-1 whitespace-nowrap !text-white" text="pages.settings.patreon.buttons.support" withBar={false} onClick={openSupportPage}/>
|
||||
<BsmButton color="#ef4444" className="flex w-fit rounded-md h-8 px-2 font-bold py-1 whitespace-nowrap !text-white" text="pages.settings.patreon.buttons.support" withBar={false} onClick={openSupportPage}/>
|
||||
<BsmButton className="flex w-fit rounded-md h-8 px-2 font-bold py-1 !text-white" withBar={false} text="pages.settings.patreon.buttons.supporters" color="#6c5ce7" onClick={toogleShowSupporters}/>
|
||||
</div>
|
||||
<SettingContainer className="mt-3" description="pages.settings.discord.description">
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ModalExitCode, ModalService } from './modale.service';
|
||||
import { NotificationService } from './notification.service';
|
||||
import { ProgressBarService } from './progress-bar.service';
|
||||
import { EditVersionModal } from 'renderer/components/modal/modal-types/edit-version-modal.component';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
export class BSVersionManagerService {
|
||||
|
||||
@@ -83,7 +84,7 @@ export class BSVersionManagerService {
|
||||
if(!res.success){
|
||||
this.notificationService.notifyError({
|
||||
title: `notifications.custom-version.errors.titles.${res.error.title}`,
|
||||
...(res.error.msg && {desc: `notifications.custom-version.errors.msg.${res.error.msg}`})
|
||||
...(res.error.message && {desc: `notifications.custom-version.errors.msg.${res.error.message}`})
|
||||
});
|
||||
return null;
|
||||
}
|
||||
@@ -103,7 +104,7 @@ export class BSVersionManagerService {
|
||||
if(!res.success){
|
||||
this.notificationService.notifyError({
|
||||
title: `notifications.custom-version.errors.titles.${res.error.title}`,
|
||||
...(res.error.msg && {desc: `notifications.custom-version.errors.msg.${res.error.msg}`})
|
||||
...(res.error.message && {desc: `notifications.custom-version.errors.msg.${res.error.message}`})
|
||||
});
|
||||
return null;
|
||||
}
|
||||
@@ -113,4 +114,8 @@ export class BSVersionManagerService {
|
||||
})
|
||||
}
|
||||
|
||||
public getVersionPath(version: BSVersion): Observable<string>{
|
||||
return this.ipcService.sendV2("get-version-full-path", {args: version});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { map, distinctUntilChanged, filter } from "rxjs/operators";
|
||||
import { from } from "rxjs";
|
||||
import { Observable } from "rxjs";
|
||||
import { IpcService } from "./ipc.service";
|
||||
import { ProgressBarService } from "./progress-bar.service";
|
||||
import { NotificationService } from "./notification.service";
|
||||
import { LinkOptions } from "main/services/folder-linker.service";
|
||||
|
||||
export class FolderLinkerService {
|
||||
|
||||
private static instance: FolderLinkerService;
|
||||
|
||||
public static getInstance(): FolderLinkerService {
|
||||
if (!FolderLinkerService.instance) {
|
||||
FolderLinkerService.instance = new FolderLinkerService();
|
||||
}
|
||||
return FolderLinkerService.instance;
|
||||
}
|
||||
|
||||
private readonly ipc: IpcService;
|
||||
private readonly progress: ProgressBarService;
|
||||
private readonly notification: NotificationService;
|
||||
|
||||
private readonly onLinkedListeners = new Map<string, () => void>();
|
||||
private readonly onUnlinkedListeners = new Map<string, () => void>();
|
||||
private readonly onRemovedFromQueueListeners = new Map<string, () => void>();
|
||||
|
||||
private readonly publicOnLinkedListeners = new Set<(folder: string) => void>();
|
||||
private readonly publicOnUnlinkedListeners = new Set<(folder: string) => void>();
|
||||
|
||||
private readonly _currentAction$ = new BehaviorSubject<LinkAction>(null);
|
||||
private readonly _queue$ = new BehaviorSubject<LinkAction[]>([]);
|
||||
|
||||
private constructor() {
|
||||
this.ipc = IpcService.getInstance();
|
||||
this.progress = ProgressBarService.getInstance();
|
||||
this.notification = NotificationService.getInstance();
|
||||
|
||||
this.queue$.pipe(map(queue => !!queue.length), distinctUntilChanged(), filter(haveAction => haveAction)).subscribe(() => {
|
||||
this.startQueue();
|
||||
});
|
||||
}
|
||||
|
||||
private async startQueue(): Promise<void>{
|
||||
|
||||
while(this._queue$.value.at(0)){
|
||||
|
||||
const toDo = this._queue$.value.at(0);
|
||||
|
||||
let progressOpened = false;
|
||||
|
||||
if(!this.progress.isVisible){
|
||||
this.progress.showFake(.01, null, toDo.folder.split("\\").at(-1));
|
||||
progressOpened = true;
|
||||
}
|
||||
|
||||
this._currentAction$.next(toDo);
|
||||
await this.doAction(toDo).toPromise();
|
||||
|
||||
this.specialFolderNotification(toDo);
|
||||
|
||||
if(toDo.type === LinkActionType.Link){
|
||||
this.onLinkedListeners.get(toDo.folder)?.();
|
||||
this.publicOnLinkedListeners.forEach(listener => listener(toDo.folder));
|
||||
this.onLinkedListeners.delete(toDo.folder);
|
||||
}
|
||||
else{
|
||||
this.onUnlinkedListeners.get(toDo.folder)?.();
|
||||
this.publicOnUnlinkedListeners.forEach(listener => listener(toDo.folder));
|
||||
this.onUnlinkedListeners.delete(toDo.folder);
|
||||
}
|
||||
|
||||
const newArr = [...this._queue$.value];
|
||||
newArr.shift();
|
||||
this._queue$.next(newArr);
|
||||
|
||||
if(progressOpened){
|
||||
this.progress.hide(true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
this._currentAction$.next(null);
|
||||
}
|
||||
|
||||
private specialFolderNotification(action: LinkAction): void{
|
||||
if(action.type === LinkActionType.Link && action.folder.includes("UserData")){
|
||||
this.notification.notifyInfo({
|
||||
title: "notifications.shared-folder.info.userdata-backup-created.title",
|
||||
desc: "notifications.shared-folder.info.userdata-backup-created.msg",
|
||||
duration: 7000
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private doAction(action: LinkAction): Observable<void>{
|
||||
return this.ipc.sendV2<void, {folder: string, options?: LinkOptions}>(action.type === LinkActionType.Link ? "link-folder" : "unlink-folder", { args: { folder: action.folder, options: action.options } });
|
||||
}
|
||||
|
||||
private addFolderToQueue(folder: string, type: LinkActionType, options: LinkOptions){
|
||||
if(this._queue$.value.some(action => action.folder === folder)){ return; }
|
||||
this._queue$.next([...this._queue$.value, {folder, type, options}]);
|
||||
}
|
||||
|
||||
private removeFolderFromQueue(folder: string){
|
||||
const queue = this._queue$.value;
|
||||
const index = queue.findIndex(action => action.folder === folder);
|
||||
if(index === -1){ return; }
|
||||
this.onRemovedFromQueueListeners.get(folder)?.();
|
||||
queue.splice(index, 1);
|
||||
this._queue$.next(queue);
|
||||
}
|
||||
|
||||
private onLinked(folder: string, callback: () => void){ this.onLinkedListeners.set(folder, callback); }
|
||||
private onUnlinked(folder: string, callback: () => void){ this.onUnlinkedListeners.set(folder, callback); }
|
||||
private onRemovedFromQueue(folder: string, callback: () => void){ this.onRemovedFromQueueListeners.set(folder, callback); }
|
||||
|
||||
public isFolderLinked(path: string): Observable<boolean> {
|
||||
return this.ipc.sendV2<boolean>("is-folder-symlink", {args: path});
|
||||
}
|
||||
|
||||
public linkFolders(...folders: string[]): Map<string, Observable<void>> {
|
||||
return new Map(folders.map(folder => [folder, this.linkFolder(folder)]));
|
||||
}
|
||||
|
||||
public linkFolder(folder: string, options: LinkOptions = {}): Observable<void> {
|
||||
options.keepContents ??= true;
|
||||
const promise = new Promise<void>(resolve => {
|
||||
this.onLinked(folder, resolve);
|
||||
this.onRemovedFromQueue(folder, resolve);
|
||||
});
|
||||
|
||||
this.addFolderToQueue(folder, LinkActionType.Link, options);
|
||||
return from(promise);
|
||||
}
|
||||
|
||||
public unlinkFolders(...folders: string[]): Map<string, Observable<void>> {
|
||||
return new Map(folders.map(folder => [folder, this.unlinkFolder(folder)]));
|
||||
}
|
||||
|
||||
public unlinkFolder(folder: string, options: LinkOptions = {}): Observable<void> {
|
||||
options.keepContents ??= true;
|
||||
const promise = new Promise<void>(resolve => {
|
||||
this.onUnlinked(folder, resolve);
|
||||
this.onRemovedFromQueue(folder, resolve);
|
||||
});
|
||||
|
||||
this.addFolderToQueue(folder, LinkActionType.Unlink, options);
|
||||
return from(promise);
|
||||
}
|
||||
|
||||
public cancelFolder(folder: string): void {
|
||||
this.removeFolderFromQueue(folder);
|
||||
}
|
||||
|
||||
public get queue$(): Observable<LinkAction[]> { return this._queue$.asObservable(); }
|
||||
public get currentAction$(): Observable<LinkAction> { return this._currentAction$.asObservable(); }
|
||||
|
||||
public isPending(folder: string): Observable<boolean> {
|
||||
return this.queue$.pipe(map(queue => queue.some(action => action.folder === folder), distinctUntilChanged()));
|
||||
}
|
||||
|
||||
public isProcessing(folder: string): Observable<boolean> {
|
||||
return this.currentAction$.pipe(distinctUntilChanged(), map(action => action?.folder === folder));
|
||||
}
|
||||
|
||||
public onFolderLinked(callback: (folder: string) => void): void { this.publicOnLinkedListeners.add(callback); }
|
||||
public removeOnFolderLinked(callback: (folder: string) => void): void { this.publicOnLinkedListeners.delete(callback); }
|
||||
public onFolderUnlinked(callback: (folder: string) => void): void { this.publicOnUnlinkedListeners.add(callback); }
|
||||
public removeOnFolderUnlinked(callback: (folder: string) => void): void { this.publicOnUnlinkedListeners.delete(callback); }
|
||||
|
||||
}
|
||||
|
||||
export interface LinkAction {
|
||||
folder: string;
|
||||
type: LinkActionType;
|
||||
options?: LinkOptions;
|
||||
}
|
||||
|
||||
export enum LinkActionType {
|
||||
Link,
|
||||
Unlink
|
||||
}
|
||||
@@ -13,10 +13,13 @@ import { ConfigurationService } from "./configuration.service";
|
||||
import { ArchiveProgress } from "shared/models/archive.interface";
|
||||
import { map, last, catchError, distinctUntilChanged, mergeMap } from "rxjs/operators";
|
||||
import { ProgressionInterface } from "shared/models/progress-bar";
|
||||
import { FolderLinkerService } from "./folder-linker.service";
|
||||
import { VersionFolderLinkerService, VersionLinkerActionType } from "./version-folder-linker.service";
|
||||
import equal from "fast-deep-equal";
|
||||
|
||||
export class MapsManagerService {
|
||||
|
||||
|
||||
|
||||
private static instance: MapsManagerService;
|
||||
|
||||
public static getInstance(): MapsManagerService{
|
||||
@@ -25,13 +28,14 @@ export class MapsManagerService {
|
||||
}
|
||||
|
||||
public static readonly REMEMBER_CHOICE_DELETE_MAP_KEY = "not-confirm-delete-map"
|
||||
public static readonly RELATIVE_MAPS_FOLDER = "Beat Saber_Data\\CustomLevels";
|
||||
|
||||
private readonly ipcService: IpcService;
|
||||
private readonly modal: ModalService;
|
||||
private readonly progressBar: ProgressBarService;
|
||||
private readonly notifications: NotificationService;
|
||||
private readonly config: ConfigurationService;
|
||||
private readonly linker: FolderLinkerService;
|
||||
private readonly linker: VersionFolderLinkerService;
|
||||
|
||||
private readonly lastLinkedVersion$: Subject<BSVersion> = new Subject();
|
||||
private readonly lastUnlinkedVersion$: Subject<BSVersion> = new Subject();
|
||||
@@ -42,11 +46,7 @@ export class MapsManagerService {
|
||||
this.progressBar = ProgressBarService.getInstance();
|
||||
this.notifications = NotificationService.getInstance();
|
||||
this.config = ConfigurationService.getInstance();
|
||||
this.linker = FolderLinkerService.getInstance();
|
||||
}
|
||||
|
||||
private async getVersionMapsPath(version: BSVersion): Promise<string>{
|
||||
return this.ipcService.sendV2<string>("get-version-maps-path", {args: version}).toPromise();
|
||||
this.linker = VersionFolderLinkerService.getInstance();
|
||||
}
|
||||
|
||||
public getMaps(version?: BSVersion): Observable<BsmLocalMapsProgress>{
|
||||
@@ -54,30 +54,35 @@ export class MapsManagerService {
|
||||
}
|
||||
|
||||
public async versionHaveMapsLinked(version: BSVersion): Promise<boolean>{
|
||||
const versionMapsPath = await this.getVersionMapsPath(version);
|
||||
return this.linker.isFolderLinked(versionMapsPath).toPromise();
|
||||
return this.linker.isVersionFolderLinked(version, MapsManagerService.RELATIVE_MAPS_FOLDER).toPromise();
|
||||
}
|
||||
|
||||
public async linkVersion(version: BSVersion): Promise<void>{
|
||||
public async linkVersion(version: BSVersion): Promise<boolean>{
|
||||
|
||||
const modalRes = await this.modal.openModal(LinkMapsModal);
|
||||
|
||||
if(modalRes.exitCode !== ModalExitCode.COMPLETED){ return; }
|
||||
if(modalRes.exitCode !== ModalExitCode.COMPLETED){ return Promise.resolve(false); }
|
||||
|
||||
const versionMapsPath = await this.getVersionMapsPath(version);
|
||||
|
||||
return this.linker.linkFolder(versionMapsPath, {keepContents: !!modalRes.data}).toPromise();
|
||||
return this.linker.linkVersionFolder({
|
||||
version,
|
||||
type: VersionLinkerActionType.Link,
|
||||
relativeFolder: MapsManagerService.RELATIVE_MAPS_FOLDER,
|
||||
options: { keepContents: !!modalRes.data }
|
||||
});
|
||||
}
|
||||
|
||||
public async unlinkVersion(version: BSVersion): Promise<void>{
|
||||
public async unlinkVersion(version: BSVersion): Promise<boolean>{
|
||||
|
||||
const modalRes = await this.modal.openModal(UnlinkMapsModal);
|
||||
|
||||
if(modalRes.exitCode !== ModalExitCode.COMPLETED){ return; }
|
||||
if(modalRes.exitCode !== ModalExitCode.COMPLETED){ return Promise.resolve(false); }
|
||||
|
||||
const versionMapsPath = await this.getVersionMapsPath(version);
|
||||
|
||||
return this.linker.unlinkFolder(versionMapsPath, {keepContents: !!modalRes.data}).toPromise();
|
||||
return this.linker.unlinkVersionFolder({
|
||||
version,
|
||||
type: VersionLinkerActionType.Unlink,
|
||||
relativeFolder: MapsManagerService.RELATIVE_MAPS_FOLDER,
|
||||
options: { keepContents: !!modalRes.data }
|
||||
});
|
||||
}
|
||||
|
||||
public async deleteMaps(maps: BsmLocalMap[], version?: BSVersion): Promise<boolean>{
|
||||
@@ -155,8 +160,7 @@ export class MapsManagerService {
|
||||
|
||||
public $mapsLinkingPending(version: BSVersion): Observable<boolean>{
|
||||
return this.linker.queue$.pipe(mergeMap(async queue => {
|
||||
const versionMapsPath = await this.getVersionMapsPath(version);
|
||||
return queue.some(q => q.folder.includes(versionMapsPath));
|
||||
return queue.some(q => q.relativeFolder.includes(MapsManagerService.RELATIVE_MAPS_FOLDER) && equal(q.version, version));
|
||||
}), distinctUntilChanged());
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
import { LinkOptions, UnlinkOptions } from "main/services/folder-linker.service";
|
||||
import { map, distinctUntilChanged, filter } from "rxjs/operators";
|
||||
import { BehaviorSubject, Observable } from "rxjs";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { IpcService } from "./ipc.service";
|
||||
import { ProgressBarService } from "./progress-bar.service";
|
||||
|
||||
export class VersionFolderLinkerService{
|
||||
|
||||
private static instance: VersionFolderLinkerService;
|
||||
|
||||
public static getInstance(): VersionFolderLinkerService{
|
||||
if(!VersionFolderLinkerService.instance){ VersionFolderLinkerService.instance = new VersionFolderLinkerService() }
|
||||
return VersionFolderLinkerService.instance;
|
||||
}
|
||||
|
||||
private readonly ipcService: IpcService;
|
||||
private readonly progress: ProgressBarService;
|
||||
|
||||
private readonly _queue$ = new BehaviorSubject<VersionLinkerAction[]>([]);
|
||||
|
||||
private readonly linkListeners = new Set<VersionLinkerActionListener>();
|
||||
private readonly unlinkListeners = new Set<VersionLinkerActionListener>();
|
||||
|
||||
private constructor(){
|
||||
this.ipcService = IpcService.getInstance();
|
||||
this.progress = ProgressBarService.getInstance();
|
||||
|
||||
this.currentAction$.pipe(filter(action => !!action)).subscribe(action => this.processAction(action));
|
||||
}
|
||||
|
||||
private async processAction(action: VersionLinkerAction){
|
||||
|
||||
let progressOpened = false;
|
||||
|
||||
if(!this.progress.isVisible){
|
||||
this.progress.showFake(.01, null, action.relativeFolder.split("\\").at(-1));
|
||||
progressOpened = true;
|
||||
}
|
||||
|
||||
const linked = await this.doAction(action).toPromise();
|
||||
|
||||
// Spécial notification
|
||||
|
||||
if(action.type === VersionLinkerActionType.Link){
|
||||
this.linkListeners.forEach(listener => listener(action, linked));
|
||||
}
|
||||
else{
|
||||
this.unlinkListeners.forEach(listener => listener(action, linked));
|
||||
}
|
||||
|
||||
if(progressOpened){
|
||||
this.progress.hide(true);
|
||||
}
|
||||
|
||||
const newArr = [...this._queue$.value];
|
||||
newArr.shift();
|
||||
this._queue$.next(newArr);
|
||||
|
||||
}
|
||||
|
||||
private doAction(action: VersionLinkerAction): Observable<boolean>{
|
||||
return this.ipcService.sendV2<boolean, VersionLinkerAction>("link-version-folder-action", { args: action });
|
||||
}
|
||||
|
||||
public linkVersionFolder(action: VersionLinkFolderAction): Promise<boolean>{
|
||||
const promise = new Promise<boolean>((resolve) => {
|
||||
|
||||
const callBack: VersionLinkerActionListener = (performedAction, linked) => {
|
||||
if(performedAction !== action){ return; }
|
||||
this.removeVersionFolderLinkedListener(callBack);
|
||||
resolve(linked);
|
||||
}
|
||||
|
||||
this.onVersionFolderLinked(callBack);
|
||||
});
|
||||
|
||||
this._queue$.next([...this._queue$.value, action]);
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
public unlinkVersionFolder(action: VersionUnlinkFolderAction): Promise<boolean>{
|
||||
const promise = new Promise<boolean>((resolve) => {
|
||||
|
||||
const callBack: VersionLinkerActionListener = (performedAction, linked) => {
|
||||
if(performedAction !== action){ return; }
|
||||
this.removeVersionFolderUnlinkedListener(callBack);
|
||||
resolve(linked);
|
||||
}
|
||||
|
||||
this.onVersionFolderUnlinked(callBack);
|
||||
});
|
||||
|
||||
this._queue$.next([...this._queue$.value, action]);
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
public onVersionFolderLinked(listener: VersionLinkerActionListener): void{
|
||||
this.linkListeners.add(listener);
|
||||
}
|
||||
|
||||
public removeVersionFolderLinkedListener(listener: VersionLinkerActionListener): void{
|
||||
this.linkListeners.delete(listener);
|
||||
}
|
||||
|
||||
public onVersionFolderUnlinked(listener: VersionLinkerActionListener): void{
|
||||
this.unlinkListeners.add(listener);
|
||||
}
|
||||
|
||||
public removeVersionFolderUnlinkedListener(listener: VersionLinkerActionListener): void{
|
||||
this.unlinkListeners.delete(listener);
|
||||
}
|
||||
|
||||
public cancelAction(version: BSVersion, relativeFolder: string): void{
|
||||
this._queue$.next(this._queue$.value.filter(a => a.version !== version || a.relativeFolder !== relativeFolder));
|
||||
}
|
||||
|
||||
public isVersionFolderLinked(version: BSVersion, relativeFolder: string): Observable<boolean>{
|
||||
return this.ipcService.sendV2("is-version-folder-linked", { args: { version, relativeFolder } });
|
||||
}
|
||||
|
||||
public isPending(version: BSVersion, relativeFolder: string): Observable<boolean> {
|
||||
return this.queue$.pipe(map(queue => queue.length > 0 && queue.some(action => action.version === version && action.relativeFolder === relativeFolder), distinctUntilChanged()));
|
||||
}
|
||||
|
||||
public isProcessing(version: BSVersion, relativeFolder: string): Observable<boolean> {
|
||||
return this.currentAction$.pipe(map(action => ( action && action.version === version && action.relativeFolder === relativeFolder)));
|
||||
}
|
||||
|
||||
public getLinkedFolders(version: BSVersion, options?: { relative?: boolean }): Observable<string[]>{
|
||||
return this.ipcService.sendV2("get-linked-folders", { args: { version, options } });
|
||||
}
|
||||
|
||||
public relinkAllVersionsFolders(): Observable<void>{
|
||||
return this.ipcService.sendV2("relink-all-versions-folders");
|
||||
}
|
||||
|
||||
public get currentAction$(): Observable<VersionLinkerAction>{
|
||||
return this._queue$.pipe(map(actions => actions.at(0)), distinctUntilChanged());
|
||||
}
|
||||
|
||||
public get queue$(): Observable<VersionLinkerAction[]>{
|
||||
return this._queue$.asObservable();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const enum VersionLinkerActionType {
|
||||
Link = "link",
|
||||
Unlink = "unlink"
|
||||
}
|
||||
|
||||
export interface VersionLinkerAction {
|
||||
version: BSVersion;
|
||||
relativeFolder: string;
|
||||
type: VersionLinkerActionType
|
||||
options?: LinkOptions
|
||||
}
|
||||
|
||||
export interface VersionLinkFolderAction extends VersionLinkerAction {
|
||||
type: VersionLinkerActionType.Link
|
||||
}
|
||||
|
||||
export interface VersionUnlinkFolderAction extends VersionLinkerAction {
|
||||
type: VersionLinkerActionType.Unlink
|
||||
options?: UnlinkOptions
|
||||
}
|
||||
|
||||
export type VersionLinkerActionListener = (action: VersionLinkerAction, linked: boolean) => void;
|
||||
@@ -1,5 +1,4 @@
|
||||
export interface BsmException {
|
||||
title: string,
|
||||
msg?: string,
|
||||
error?: Error,
|
||||
}
|
||||
export interface BsmException extends Readonly<Omit<NodeJS.ErrnoException, "message">>{
|
||||
readonly title?: string,
|
||||
readonly message?: string,
|
||||
};
|
||||
Reference in New Issue
Block a user