Merge branch 'Zagrios:master' into patch-1

This commit is contained in:
Ika
2025-07-03 22:05:29 +02:00
committed by GitHub
39 changed files with 694 additions and 240 deletions
+19 -1
View File
@@ -847,7 +847,25 @@
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/510702041542166484",
"ReleaseImg": "https://clan.fastly.steamstatic.com/images/32055887/c27dac7f96e53672d020d97838cec1d5d28e3180.png",
"ReleaseDate": "1743515622",
"year": "2025"
},
{
"BSVersion": "1.40.5",
"BSManifest": "2447038720841508806",
"OculusBinaryId": "9120095564756808",
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/502825275922317447",
"ReleaseImg": "https://clan.fastly.steamstatic.com/images/32055887/2b5845893d915902106aaf30b6d1a95f68a4b226.png",
"ReleaseDate": "1747321305",
"year": "2025"
},
{
"BSVersion": "1.40.6",
"BSManifest": "3050696685250334115",
"OculusBinaryId": "9315734585192904",
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/510708375093248769",
"ReleaseImg": "https://clan.fastly.steamstatic.com/images/32055887/0f658ed39f480446e0dc5807b1591e8a18aef2f5.png",
"ReleaseDate": "1749135144",
"year": "2025",
"recommended": true
}
]
]
+8
View File
@@ -161,6 +161,14 @@
},
{
"username": "KingCrocman",
"type": "diamond",
"link": "https://www.youtube.com/KingCrocman"
},
{
"username": "bosspie",
"type": "diamond"
},
{
"username": "Camryn Jackson"
}
]
+11 -1
View File
@@ -37,7 +37,10 @@
}
},
"title-bar": {
"outdated": "veraltet"
"outdated": "veraltet",
"update-text": "BSManager {version} ist verfügbar!",
"update-button": "Aktualisieren und neu starten",
"see-changelog": "Änderungen ansehen"
},
"nav-bar": {
"add-version": "Version hinzufügen",
@@ -337,6 +340,13 @@
"error-notification": {
"message": "Es ist ein Fehler aufgetreten, die Systemproxyeinstellungen können nicht geändert werden."
}
},
"auto-update": {
"title": "Automatische Aktualisierung",
"description": "BSManager wird beim Start der Anwendung automatisch aktualisiert.",
"error-notification": {
"message": "Ein Fehler ist aufgetreten, die Einstellungen für die automatische Aktualisierung können nicht geändert werden."
}
}
}
}
+11 -1
View File
@@ -37,7 +37,10 @@
}
},
"title-bar": {
"outdated": "outdated"
"outdated": "outdated",
"update-text": "BSManager {version} is available!",
"update-button": "Update and Restart",
"see-changelog": "See changelog"
},
"nav-bar": {
"add-version": "Add a version",
@@ -337,6 +340,13 @@
"error-notification": {
"message": "An error occurred, unable to change system proxy settings."
}
},
"auto-update": {
"title": "Auto Update",
"description": "BSManager will automatically update when you launch the application.",
"error-notification": {
"message": "An error occurred, unable to change auto update settings."
}
}
}
}
+11 -1
View File
@@ -37,7 +37,10 @@
}
},
"title-bar": {
"outdated": "obsoleto"
"outdated": "obsoleto",
"update-text": "¡BSManager {version} está disponible!",
"update-button": "Actualizar y reiniciar",
"see-changelog": "Ver los cambios"
},
"nav-bar": {
"add-version": "Agregar una versión",
@@ -337,6 +340,13 @@
"error-notification": {
"message": "Ocurrió un error, no se pueden cambiar los ajustes del proxy del sistema."
}
},
"auto-update": {
"title": "Actualización automática",
"description": "BSManager se actualizará automáticamente al iniciar la aplicación.",
"error-notification": {
"message": "Ocurrió un error, no se pudo cambiar la configuración de actualización automática."
}
}
}
}
+11 -1
View File
@@ -37,7 +37,10 @@
}
},
"title-bar": {
"outdated": "obsolète"
"outdated": "obsolète",
"update-text": "BSManager {version} est disponible!",
"update-button": "Mettre à jour et redémarrer",
"see-changelog": "Voir les changements"
},
"nav-bar": {
"add-version": "Ajouter une version",
@@ -337,6 +340,13 @@
"error-notification": {
"message": "Une erreur est survenue, impossible de modifier les paramètres du proxy système."
}
},
"auto-update": {
"title": "Mise à jour automatique",
"description": "BSManager se mettra automatiquement à jour au lancement de l'application.",
"error-notification": {
"message": "Une erreur est survenue, impossible de modifier les paramètres de mise à jour automatique."
}
}
}
}
+11 -1
View File
@@ -37,7 +37,10 @@
}
},
"title-bar": {
"outdated": "obsoleta"
"outdated": "obsoleta",
"update-text": "BSManager {version} è disponibile!",
"update-button": "Aggiorna e riavvia",
"see-changelog": "Vedi i cambiamenti"
},
"nav-bar": {
"add-version": "Aggiungi una versione",
@@ -337,6 +340,13 @@
"error-notification": {
"message": "C'è stato un errore, impossibile cambiare le impostazioni del proxy di sistema."
}
},
"auto-update": {
"title": "Aggiornamento automatico",
"description": "BSManager si aggiornerà automaticamente all'avvio dell'applicazione.",
"error-notification": {
"message": "Si è verificato un errore, impossibile modificare le impostazioni di aggiornamento automatico."
}
}
}
}
+11 -1
View File
@@ -37,7 +37,10 @@
}
},
"title-bar": {
"outdated": "時代遅れ"
"outdated": "時代遅れ",
"update-text": "BSManager {version} が利用可能です!",
"update-button": "更新して再起動",
"see-changelog": "変更点を確認"
},
"nav-bar": {
"add-version": "バージョンを追加",
@@ -337,6 +340,13 @@
"error-notification": {
"message": "エラーが発生しました。システムプロキシ設定を変更できません。"
}
},
"auto-update": {
"title": "自動更新",
"description": "アプリケーション起動時にBSManagerが自動的に更新されます。",
"error-notification": {
"message": "エラーが発生し、自動更新の設定を変更できませんでした。"
}
}
}
}
+11 -1
View File
@@ -37,7 +37,10 @@
}
},
"title-bar": {
"outdated": "구식"
"outdated": "구식",
"update-text": "BSManager {version} 가 사용 가능합니다!",
"update-button": "업데이트 후 재시작",
"see-changelog": "변경 사항 보기"
},
"nav-bar": {
"add-version": "버전 추가",
@@ -337,6 +340,13 @@
"error-notification": {
"message": "오류가 발생했습니다. 시스템 프록시 설정을 변경할 수 없습니다."
}
},
"auto-update": {
"title": "자동 업데이트",
"description": "애플리케이션을 실행할 때 BSManager가 자동으로 업데이트됩니다.",
"error-notification": {
"message": "오류가 발생하여 자동 업데이트 설정을 변경할 수 없습니다."
}
}
}
}
+11 -1
View File
@@ -37,7 +37,10 @@
}
},
"title-bar": {
"outdated": "Desatualizado"
"outdated": "Desatualizado",
"update-text": "BSManager {version} está disponível!",
"update-button": "Atualizar e reiniciar",
"see-changelog": "Ver os changelogs"
},
"nav-bar": {
"add-version": "Adicionar uma versão",
@@ -337,6 +340,13 @@
"error-notification": {
"message": "Um erro aconteceu, não foi possível alterar a configuração de proxy de sistema."
}
},
"auto-update": {
"title": "Atualização automática",
"description": "O BSManager será atualizado automaticamente ao iniciar o aplicativo.",
"error-notification": {
"message": "Ocorreu um erro, não foi possível alterar as configurações de atualização automática."
}
}
}
}
+11 -1
View File
@@ -37,7 +37,10 @@
}
},
"title-bar": {
"outdated": "устаревший"
"outdated": "устаревший",
"update-text": "BSManager {version} доступен!",
"update-button": "Обновить и перезапустить",
"see-changelog": "Смотреть изменения"
},
"nav-bar": {
"add-version": "Добавить версию игры",
@@ -337,6 +340,13 @@
"error-notification": {
"message": "Произошла ошибка, не удалось изменить настройки системного прокси."
}
},
"auto-update": {
"title": "Автообновление",
"description": "BSManager будет автоматически обновляться при запуске приложения.",
"error-notification": {
"message": "Произошла ошибка, не удалось изменить настройки автообновления."
}
}
}
}
+11 -1
View File
@@ -37,7 +37,10 @@
}
},
"title-bar": {
"outdated": "lipas na"
"outdated": "lipas na",
"update-text": "Ang BSManager {version} ay available!",
"update-button": "I-update at i-restart",
"see-changelog": "Tingnan ang mga pagbabago"
},
"nav-bar": {
"add-version": "Magdagdag ng bersyon",
@@ -337,6 +340,13 @@
"error-notification": {
"message": "Nagkaroon ng error, hindi ma-bago ang setting ng system proxy."
}
},
"auto-update": {
"title": "Awtomatikong Pag-update",
"description": "Ang BSManager ay awtomatikong mag-uupdate kapag binuksan mo ang aplikasyon.",
"error-notification": {
"message": "Nagkaroon ng error, hindi mababago ang mga setting ng awtomatikong pag-update."
}
}
}
}
+11 -1
View File
@@ -37,7 +37,10 @@
}
},
"title-bar": {
"outdated": "застарілий"
"outdated": "застарілий",
"update-text": "BSManager {version} доступний!",
"update-button": "Оновити та перезапустити",
"see-changelog": "Переглянути зміни"
},
"nav-bar": {
"add-version": "Додати версію",
@@ -337,6 +340,13 @@
"error-notification": {
"message": "Сталася помилка, неможливо змінити налаштування системного проксі."
}
},
"auto-update": {
"title": "Автоматичне оновлення (Avtomatychne onovlennya)",
"description": "BSManager автоматично оновлюватиметься, коли ви запустите програму.",
"error-notification": {
"message": "Сталася помилка, неможливо змінити налаштування автоматичного оновлення."
}
}
}
}
+11 -1
View File
@@ -37,7 +37,10 @@
}
},
"title-bar": {
"outdated": "過時"
"outdated": "過時",
"update-text": "BSManager {version} 可用!",
"update-button": "更新並重新啟動",
"see-changelog": "查看變更"
},
"nav-bar": {
"add-version": "新增版本",
@@ -337,6 +340,13 @@
"error-notification": {
"message": "發生錯誤,無法更改系統代理設置。"
}
},
"auto-update": {
"title": "自動更新",
"description": "啟動應用程式時,BSManager 將自動更新。",
"error-notification": {
"message": "發生錯誤,無法更改自動更新設定。"
}
}
}
}
+11 -1
View File
@@ -37,7 +37,10 @@
}
},
"title-bar": {
"outdated": "过时"
"outdated": "过时",
"update-text": "BSManager {version} 可用!",
"update-button": "更新并重新启动",
"see-changelog": "查看变更"
},
"nav-bar": {
"add-version": "添加版本",
@@ -337,6 +340,13 @@
"error-notification": {
"message": "发生错误,无法启用系统代理。"
}
},
"auto-update": {
"title": "自动更新",
"description": "启动应用程序时,BSManager 将自动更新。",
"error-notification": {
"message": "发生错误,无法更改自动更新设置。"
}
}
}
}
@@ -30,11 +30,12 @@
In the developer tools:
1. Open the __`Network`__ tab.
2. Filter for __`graph`__.
3. Click on the first request that appears.
4. Open the __`Payload`__ tab.
5. Scroll to the bottom to locate your token. It should start with __`FRL`__.
6. Select the token using your mouse and press `Ctrl` + `C` to copy it.
2. Press CTRL+R.
3. Filter for __`graph`__.
4. Click on the first request that appears.
5. Open the __`Payload`__ tab.
6. Scroll to the bottom to locate your token. It should start with __`FRL`__.
7. Select the token using your mouse and press `Ctrl` + `C` to copy it.
<div align="center">
<img src="../wiki/Guides/Login/How-to-obtain-your-Oculus-Token/GetYourOculusToken.png" alt="GetYourOculusToken.png" />
+27 -27
View File
@@ -1,12 +1,12 @@
{
"name": "bs-manager",
"version": "1.5.2",
"version": "1.5.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "bs-manager",
"version": "1.5.2",
"version": "1.5.3",
"hasInstallScript": true,
"license": "GPL-3.0-only",
"dependencies": {
@@ -27,7 +27,7 @@
"electron-updater": "^6.3.9",
"fast-deep-equal": "^3.1.3",
"format-duration": "^3.0.2",
"framer-motion": "^12.6.3",
"framer-motion": "^12.17.0",
"fs-extra": "^11.3.0",
"global-agent": "^3.0.0",
"got": "^14.4.7",
@@ -37,7 +37,7 @@
"node-abi": "^4.2.0",
"node-fetch": "^3.3.2",
"pako": "^2.1.0",
"protobufjs": "^7.4.0",
"protobufjs": "^7.5.3",
"qrcode.react": "^4.2.0",
"query-process": "^0.0.3",
"react": "^18.2.0",
@@ -52,7 +52,7 @@
"rfdc": "^1.4.1",
"rxjs": "^7.8.2",
"sanitize-filename": "^1.6.3",
"semver": "^7.7.1",
"semver": "^7.7.2",
"serialize-error": "^12.0.0",
"striptags": "^4.0.0-alpha.4",
"tailwind-merge": "^3.0.2",
@@ -105,7 +105,7 @@
"css-loader": "^6.10.0",
"css-minimizer-webpack-plugin": "^6.0.0",
"detect-port": "^1.5.1",
"electron": "^35.1.4",
"electron": "^36.4.0",
"electron-builder": "^25.1.8",
"electron-devtools-installer": "^4.0.0",
"electronmon": "^2.0.3",
@@ -11870,9 +11870,9 @@
}
},
"node_modules/electron": {
"version": "35.1.4",
"resolved": "https://registry.npmjs.org/electron/-/electron-35.1.4.tgz",
"integrity": "sha512-8HjE2wqxY//T09Of8k1eTpK/NeTG2FkTyRD+fyKXmec4wZVscGgZcmWFC0HYN4ktyHAjtplpxdFXjtqRnvzBMg==",
"version": "36.4.0",
"resolved": "https://registry.npmjs.org/electron/-/electron-36.4.0.tgz",
"integrity": "sha512-LLOOZEuW5oqvnjC7HBQhIqjIIJAZCIFjQxltQGLfEC7XFsBoZgQ3u3iFj+Kzw68Xj97u1n57Jdt7P98qLvUibQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -14433,13 +14433,13 @@
}
},
"node_modules/framer-motion": {
"version": "12.6.3",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.6.3.tgz",
"integrity": "sha512-2hsqknz23aloK85bzMc9nSR2/JP+fValQ459ZTVElFQ0xgwR2YqNjYSuDZdFBPOwVCt4Q9jgyTt6hg6sVOALzw==",
"version": "12.17.0",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.17.0.tgz",
"integrity": "sha512-2hISKgDk49yCLStwG1wf4Kdy/D6eBw9/eRNaWFIYoI9vMQ/Mqd1Fz+gzVlEtxJmtQ9y4IWnXm19/+UXD3dAYAA==",
"license": "MIT",
"dependencies": {
"motion-dom": "^12.6.3",
"motion-utils": "^12.6.3",
"motion-dom": "^12.17.0",
"motion-utils": "^12.12.1",
"tslib": "^2.4.0"
},
"peerDependencies": {
@@ -19233,18 +19233,18 @@
}
},
"node_modules/motion-dom": {
"version": "12.6.3",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.6.3.tgz",
"integrity": "sha512-gRY08RjcnzgFYLemUZ1lo/e9RkBxR+6d4BRvoeZDSeArG4XQXERSPapKl3LNQRu22Sndjf1h+iavgY0O4NrYqA==",
"version": "12.17.0",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.17.0.tgz",
"integrity": "sha512-FA6/c70R9NKs3g41XDVONzmUUrEmyaifLVGCWtAmHP0usDnX9W+RN/tmbC4EUl0w6yLGvMTOwnWCFVgA5luhRg==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.6.3"
"motion-utils": "^12.12.1"
}
},
"node_modules/motion-utils": {
"version": "12.6.3",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.6.3.tgz",
"integrity": "sha512-R/b3Ia2VxtTNZ4LTEO5pKYau1OUNHOuUfxuP0WFCTDYdHkeTBR9UtxR1cc8mDmKr8PEhmmfnTKGz3rSMjNRoRg==",
"version": "12.12.1",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.12.1.tgz",
"integrity": "sha512-f9qiqUHm7hWSLlNW8gS9pisnsN7CRFRD58vNjptKdsqFLpkVnX00TNeD6Q0d27V9KzT7ySFyK1TZ/DShfVOv6w==",
"license": "MIT"
},
"node_modules/mrmime": {
@@ -21153,9 +21153,9 @@
}
},
"node_modules/protobufjs": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz",
"integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==",
"version": "7.5.3",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.3.tgz",
"integrity": "sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw==",
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -22470,9 +22470,9 @@
}
},
"node_modules/semver": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
+5 -5
View File
@@ -2,7 +2,7 @@
"name": "bs-manager",
"description": "Manage maps, mods and more for Beat Saber",
"main": "./.erb/dll/main.bundle.dev.js",
"version": "1.5.3-alpha.1",
"version": "1.5.3",
"scripts": {
"build-rust-scripts": "ts-node ./.erb/scripts/build-rust-scripts.js",
"build": "concurrently \"npm run build:main\" \"npm run build:renderer\"",
@@ -108,7 +108,7 @@
"css-loader": "^6.10.0",
"css-minimizer-webpack-plugin": "^6.0.0",
"detect-port": "^1.5.1",
"electron": "^35.1.4",
"electron": "^36.4.0",
"electron-builder": "^25.1.8",
"electron-devtools-installer": "^4.0.0",
"electronmon": "^2.0.3",
@@ -170,7 +170,7 @@
"electron-updater": "^6.3.9",
"fast-deep-equal": "^3.1.3",
"format-duration": "^3.0.2",
"framer-motion": "^12.6.3",
"framer-motion": "^12.17.0",
"fs-extra": "^11.3.0",
"global-agent": "^3.0.0",
"got": "^14.4.7",
@@ -180,7 +180,7 @@
"node-abi": "^4.2.0",
"node-fetch": "^3.3.2",
"pako": "^2.1.0",
"protobufjs": "^7.4.0",
"protobufjs": "^7.5.3",
"qrcode.react": "^4.2.0",
"query-process": "^0.0.3",
"react": "^18.2.0",
@@ -195,7 +195,7 @@
"rfdc": "^1.4.1",
"rxjs": "^7.8.2",
"sanitize-filename": "^1.6.3",
"semver": "^7.7.1",
"semver": "^7.7.2",
"serialize-error": "^12.0.0",
"striptags": "^4.0.0-alpha.4",
"tailwind-merge": "^3.0.2",
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "bs-manager",
"version": "1.5.3-alpha.1",
"version": "1.5.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "bs-manager",
"version": "1.5.3-alpha.1",
"version": "1.5.3",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "bs-manager",
"version": "1.5.3-alpha.1",
"version": "1.5.3",
"description": "BSManager",
"main": "./dist/main/main.js",
"author": {
+9 -1
View File
@@ -51,7 +51,15 @@ async function enableWindowsProxy(enable: boolean): Promise<void> {
}
if (enable) {
const httpProxyUrl = `http://${await getProxyServer().catch(err => log.error(err))}`
const httpProxyServer = await getProxyServer().catch(err => log.error(err));
let httpProxyUrl: string | null = null;
if (httpProxyServer) {
// Prepend the http protocol
httpProxyUrl = httpProxyServer.startsWith("http://")
? httpProxyServer
: `http://${httpProxyServer}`;
}
globalProxyAgent.HTTP_PROXY = httpProxyUrl;
globalProxyAgent.HTTPS_PROXY = httpProxyUrl;
globalProxyAgent.NO_PROXY = `${await getProxyOverride().catch(err => log.error(err))}`;
+5
View File
@@ -14,6 +14,11 @@ ipc.on("check-update", (_, reply) => {
reply(from(updaterService.isUpdateAvailable()));
});
ipc.on("get-available-update", (_, reply) => {
const updaterService = AutoUpdaterService.getInstance();
reply(from(updaterService.getAvailableUpdate()));
});
ipc.on("install-update", (_, reply) => {
const updaterService = AutoUpdaterService.getInstance();
reply(of(updaterService.quitAndInstall()));
+12 -5
View File
@@ -28,6 +28,7 @@ import { StaticConfigurationService } from "./services/static-configuration.serv
import { configureProxy } from './helpers/proxy.helpers';
import { deleteFileSync, deleteFolderSync } from "./helpers/fs.helpers";
import { tryit } from "shared/helpers/error.helpers";
import { AutoUpdate } from "shared/models/config";
const isDebug = process.env.NODE_ENV === "development" || process.env.DEBUG_PROD === "true";
const staticConfig = StaticConfigurationService.getInstance();
@@ -121,8 +122,6 @@ if (!gotTheLock) {
app.whenReady().then(() => {
app.setAppUserModelId(APP_NAME);
initServicesMustBeInitialized();
@@ -134,10 +133,18 @@ if (!gotTheLock) {
DeepLinkService.getInstance().dispatchLinkOpened(deepLink);
} else if (associatedFile) {
FileAssociationService.getInstance().handleFileAssociation(associatedFile);
} else if (process.platform === "linux") {
createWindow("index.html");
} else {
createWindow(process.platform === "linux"
? "index.html" : "launcher.html"
);
const configService = StaticConfigurationService.getInstance();
const autoUpdate = configService.get("auto-update", AutoUpdate.ALWAYS);
const update = autoUpdate !== AutoUpdate.NEVER;
if (autoUpdate === AutoUpdate.ONCE) {
configService.set("auto-update", AutoUpdate.NEVER);
}
// Skip launcher only if autoUpdate is strictly false
createWindow(update ? "launcher.html" : "index.html");
}
SteamLauncherService.getInstance().restoreSteamVR();
@@ -359,7 +359,18 @@ export class LocalPlaylistsManagerService {
continue;
}
const downloadedMap = await this.maps.downloadMap(mapDetail, version);
const downloadedMap = await this.maps.downloadMap(mapDetail, version)
.catch(error => {
log.error(
"Could not download map for playlist",
`[${mapDetail.id}] "${mapDetail.name}"`,
error
);
return null;
});
if (!downloadedMap) {
continue;
}
progress.data.downloadedMaps.push(downloadedMap);
progress.data.currentDownload = mapDetail;
+14 -1
View File
@@ -1,8 +1,9 @@
import { autoUpdater, CancellationToken, ProgressInfo } from "electron-updater";
import { autoUpdater, CancellationToken, ProgressInfo, UpdateInfo } from "electron-updater";
import log from "electron-log";
import { gt } from "semver";
import { Progression } from "main/helpers/fs.helpers";
import { Observable } from "rxjs";
import { safeGt } from "shared/helpers/semver.helpers";
export class AutoUpdaterService {
private static instance: AutoUpdaterService;
@@ -31,6 +32,18 @@ export class AutoUpdaterService {
});
}
public async getAvailableUpdate(): Promise<UpdateInfo | null> {
return autoUpdater.checkForUpdates().then(info => {
if (info?.updateInfo && safeGt(info.updateInfo.version, autoUpdater.currentVersion.version)) {
return info.updateInfo;
}
return null;
}).catch((error: Error): UpdateInfo | null => {
log.error("Could not get update", error);
return null;
});
}
public downloadUpdate(): Observable<Progression> {
return new Observable<Progression>(observer => {
@@ -244,8 +244,12 @@ export class BSLocalVersionService {
private async getSteamVersion(): Promise<BSVersion> {
const steamBsFolder = await this.steamService.getGameFolder(BS_APP_ID, "Beat Saber");
if (!steamBsFolder || !(await pathExists(steamBsFolder))) {
return null;
if (!steamBsFolder) {
throw new Error("No Beat Saber Steam version found");
}
if (!(await pathExists(steamBsFolder))) {
throw new Error(`Beat Saber Steam version not found in "${steamBsFolder}"`);
}
return this.getVersionOfBSFolder(steamBsFolder, { steam: true });
@@ -265,7 +269,8 @@ export class BSLocalVersionService {
const versions: BSVersion[] = [];
const steamVersion = await this.getSteamVersion().catch(e => {
log.error("unable to get original Steam version", e);
log.error("Unable to get original Steam version", e);
return null;
});
if (steamVersion) {
+1 -6
View File
@@ -129,12 +129,7 @@ export class LinuxService {
return fs.pathExistsSync(protonPath) && fs.pathExistsSync(winePath);
}
public async getWinePath(): Promise<string> {
if (await this.isNixOS()) {
// Use system wine for nixos
return "wine";
}
public getWinePath(): string {
if (!this.staticConfig.has("proton-folder")) {
throw new Error("proton-folder variable not set");
}
@@ -76,8 +76,7 @@ export class BeatModsApiService {
}
return this.requestService.getJSON<{ modVersions: BbmModVersion[] }>(
`${this.MODS_REPO_API_URL}/hashlookup?hash=${hash}`,
{ silentError: true }
`${this.MODS_REPO_API_URL}/hashlookup?hash=${hash}`
).then(({ data }) => {
this.updateModsHashCache(data?.modVersions ?? []);
return data?.modVersions?.at(0);
@@ -162,18 +162,22 @@ export class BsModsManagerService {
let winePath: string = "";
if (process.platform === "linux") {
const { error: winePathError, result: winePathResult } =
await tryit(async () => this.linuxService.getWinePath());
tryit(() => this.linuxService.getWinePath());
if (winePathError) {
log.error(winePathError);
return false;
}
winePath = `"${winePathResult}"`;
winePath = await this.linuxService.isNixOS()
? `steam-run "${winePathResult}"`
: `"${winePathResult}"`;
const winePrefix = this.linuxService.getWinePrefixPath();
if (!winePrefix) {
throw new CustomError("Could not find BSManager WINEPREFIX path", "no-wineprefix");
}
env.WINEPREFIX = winePrefix;
Object.assign(env, process.env);
}
return new Promise<boolean>(resolve => {
+140 -70
View File
@@ -9,6 +9,7 @@ import { tryit } from 'shared/helpers/error.helpers';
import path from 'path';
import { pipeline } from 'stream/promises';
import sanitize from 'sanitize-filename';
import internal from 'stream';
import { app } from 'electron';
export class RequestService {
@@ -17,31 +18,55 @@ export class RequestService {
'User-Agent': `BSManager/${app.getVersion()} (Electron/${process.versions.electron} Chrome/${process.versions.chrome} Node/${process.versions.node})`,
}
private readonly PREFERRED_FAMILY_TESTS = [4, 6];
private preferredFamilyCache: Record<string, number> = {};
public static getInstance(): RequestService {
if (!RequestService.instance) {
RequestService.instance = new RequestService();
}
return RequestService.instance;
}
private constructor() {}
public async getJSON<T = unknown>(url: string, options?: {
silentError?: boolean
}): Promise<{ data: T; headers: IncomingHttpHeaders }> {
try {
// @ts-ignore (ESM is not well supported in this project, We need to move out electron-react-boilerplate, and use Vite)
const res = await got(url, { responseType: 'json', headers: this.baseHeaders });
return { data: res.body as T, headers: res.headers };
} catch (err) {
if (options?.silentError !== true) {
log.error(`Failed to get JSON from URL: ${url}`, err);
public async getJSON<T = unknown>(url: string): Promise<{ data: T; headers: IncomingHttpHeaders }> {
const domain = (new URL(url)).hostname;
const cachedFamily = this.preferredFamilyCache[domain];
if (cachedFamily) {
try {
return await this.requestData<T>(url, cachedFamily);
} catch (error: any) {
throw new Error(`Request failed: ${url}`, error);
}
throw err;
}
// Try on each IPv4/6 families on first request to a domain/website
for (const family of this.PREFERRED_FAMILY_TESTS) {
try {
const response = await this.requestData<T>(url, family);
log.info(`Caching "${domain}" with IPv${family}`);
this.preferredFamilyCache[domain] = family;
return response;
} catch (err) {
log.warn(`IPv${family} request failed, trying next one... URL: ${url}`, err);
}
}
throw new Error(`IPv4 and IPv6 requests failed for URL: ${url}`);
}
private async requestData<T>(url: string, family: number): Promise<{ data: T; headers: IncomingHttpHeaders }> {
const response = await got(url, {
// @ts-ignore (ESM is not well supported in this project, We need to move out electron-react-boilerplate, and use Vite)
dnsLookupIpVersion: family,
responseType: "json",
headers: this.baseHeaders
});
return {
data: response.body as T,
headers: response.headers
};
}
public downloadFile(
@@ -52,46 +77,69 @@ export class RequestService {
return new Observable<Progression<string>>((subscriber) => {
const progress: Progression<string> = { current: 0, total: 0 };
let file: WriteStream | undefined;
let attempt = 0;
let stream: got.GotEmitter & internal.Duplex;
// @ts-ignore (ESM is not well supported in this project, We need to move out electron-react-boilerplate, and use Vite)
const stream = got.stream(url, { headers: this.baseHeaders });
const domain = (new URL(url)).hostname;
const cachedFamily = this.preferredFamilyCache[domain];
const familiesToTry = cachedFamily
? [ cachedFamily ] : this.PREFERRED_FAMILY_TESTS;
stream.on('response', (response) => {
const filename = opt?.preferContentDisposition ? this.getFilenameFromContentDisposition(response.headers['content-disposition']) : null;
if (filename) {
dest = path.join(path.dirname(dest), sanitize(filename));
const tryNextFamily = () => {
if (attempt >= familiesToTry.length) {
subscriber.error(new Error(`Download failed over IPv4 and IPv6 for URL: ${url}`));
return;
}
progress.data = dest;
file = createWriteStream(dest);
const family = familiesToTry[attempt++];
let file: WriteStream | undefined;
pipeline(stream, file).catch(err => {
file?.destroy();
tryit(() => deleteFileSync(dest));
subscriber.error(err);
// @ts-ignore (ESM is not well supported in this project, We need to move out electron-react-boilerplate, and use Vite)
stream = got.stream(url, { dnsLookupIpVersion: family, headers: this.baseHeaders });
stream.on('response', (response) => {
if (!cachedFamily) {
log.info(`Caching "${domain}" with IPv${family}`);
this.preferredFamilyCache[domain] = family;
}
const filename = opt?.preferContentDisposition ? this.getFilenameFromContentDisposition(response.headers['content-disposition']) : null;
if (filename) {
dest = path.join(path.dirname(dest), sanitize(filename));
}
progress.data = dest;
file = createWriteStream(dest);
pipeline(stream, file).catch(err => {
file?.destroy();
tryit(() => deleteFileSync(dest));
subscriber.error(err);
});
});
});
stream.on('downloadProgress', ({ transferred, total }) => {
progress.current = transferred;
progress.total = total;
subscriber.next(progress);
});
stream.on('downloadProgress', ({ transferred, total }) => {
progress.current = transferred;
progress.total = total;
subscriber.next(progress);
});
stream.on('error', err => {
log.error(`Download failed for URL: ${url}`, err);
stream.destroy();
file?.destroy();
});
stream.on('error', err => {
log.warn(`Download failed over IPv${family} for URL: ${url}`, err);
stream.destroy();
file?.destroy();
tryNextFamily();
});
stream.on('end', () => {
file?.end();
subscriber.next(progress);
subscriber.complete();
});
stream.on('end', () => {
file?.end();
subscriber.next(progress);
subscriber.complete();
});
};
tryNextFamily();
return () => {
stream?.destroy();
@@ -106,7 +154,6 @@ export class RequestService {
url: string,
options?: got.GotOptions<null>
): Observable<Progression<Buffer, IncomingMessage>> {
return new Observable<Progression<Buffer, IncomingMessage>>((subscriber) => {
const progress: Progression<Buffer, IncomingMessage> = {
current: 0,
@@ -116,37 +163,60 @@ export class RequestService {
const headers = { ...this.baseHeaders, ...(options?.headers ?? {}) };
// @ts-ignore (ESM is not well supported in this project, We need to move out electron-react-boilerplate, and use Vite)
const stream = got.stream(url, { ...(options ?? {}), headers });
let attempt = 0;
let stream: got.GotEmitter & internal.Duplex;
let data = Buffer.alloc(0);
let response: IncomingMessage;
const domain = (new URL(url)).hostname;
const cachedFamily = this.preferredFamilyCache[domain];
const familiesToTry = cachedFamily
? [ cachedFamily ] : this.PREFERRED_FAMILY_TESTS;
stream.once('response', (res) => {
response = res;
});
const tryNextFamily = () => {
if (attempt >= familiesToTry.length) {
subscriber.error(new Error(`Download failed over IPv4 and IPv6 for URL: ${url}`));
return;
}
stream.on('data', (chunk: Buffer) => {
data = Buffer.concat([data, chunk]);
});
const family = familiesToTry[attempt++];
// @ts-ignore (ESM is not well supported in this project, We need to move out electron-react-boilerplate, and use Vite)
stream = got.stream(url, { dnsLookupIpVersion: family, ...(options ?? {}), headers });
stream.on('downloadProgress', ({ transferred, total }) => {
progress.current = transferred;
progress.total = total;
subscriber.next(progress);
});
let data = Buffer.alloc(0);
let response: IncomingMessage;
stream.on('error', err => {
log.error(`Download failed for URL: ${url}`, err);
stream.destroy();
});
stream.once('response', (res) => {
if (!cachedFamily) {
log.info(`Caching "${domain}" with IPv${family}`);
this.preferredFamilyCache[domain] = family;
}
response = res;
});
stream.on('end', () => {
progress.data = data;
progress.extra = response;
subscriber.next(progress);
subscriber.complete();
});
stream.on('data', (chunk: Buffer) => {
data = Buffer.concat([data, chunk]);
});
stream.on('downloadProgress', ({ transferred, total }) => {
progress.current = transferred;
progress.total = total;
subscriber.next(progress);
});
stream.on('error', err => {
log.warn(`Download failed over IPv${family} for URL: ${url}`, err);
stream.destroy();
tryNextFamily();
});
stream.on('end', () => {
progress.data = data;
progress.extra = response;
subscriber.next(progress);
subscriber.complete();
});
};
tryNextFamily();
return () => {
stream?.destroy();
@@ -5,6 +5,7 @@ import { PROTON_BINARY_PREFIX, WINE_BINARY_PREFIX } from "main/constants";
import { Observable, Subject } from "rxjs";
import { CustomError } from "shared/models/exceptions/custom-error.class";
import { BSVersion } from "shared/bs-version.interface";
import { AutoUpdate } from "shared/models/config";
export class StaticConfigurationService {
private static instance: StaticConfigurationService;
@@ -30,8 +31,8 @@ export class StaticConfigurationService {
return this.store.has(key);
}
public get<K extends StaticConfigKeys>(key: K): StaticConfigKeyValues[K] {
return this.store.get<K>(key) as StaticConfigKeyValues[K];
public get<K extends StaticConfigKeys>(key: K, defaultValue?: StaticConfigKeyValues[K]): StaticConfigKeyValues[K] {
return this.store.get<K>(key, defaultValue) as StaticConfigKeyValues[K];
}
public take<K extends StaticConfigKeys>(key: K, cb: (val: StaticConfigKeyValues[K]) => void): void {
@@ -90,6 +91,7 @@ export interface StaticConfigKeyValues {
"use-symlinks": boolean;
"use-system-proxy": boolean;
"last-version-launched": BSVersion;
"auto-update": AutoUpdate;
// Linux Specific static configs
"proton-folder": string;
@@ -249,7 +249,7 @@ export const LocalMapsListPanel = forwardRef<unknown, Props>(({
return (
<VirtualScroll
classNames={{ mainDiv: className, rows: "gap-x-2 px-2 py-2" }}
classNames={{ mainDiv: className, rows: "gap-x-2 px-2 pt-2 pb-[108px]" }}
itemHeight={108}
maxColumns={3}
minItemWidth={400}
@@ -6,13 +6,12 @@ import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { LaunchOption } from "shared/models/bs-launch";
import { useService } from "renderer/hooks/use-service.hook";
import { BSLauncherService } from "renderer/services/bs-launcher.service";
import { useMemo, useState } from "react";
import { useState } from "react";
import { BsNoteFill } from "renderer/components/svgs/icons/bs-note-fill.component";
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
import { ChevronTopIcon } from "renderer/components/svgs/icons/chevron-top-icon.component";
import Tippy from "@tippyjs/react";
import { LaunchMod } from "shared/models/bs-launch/launch-option.interface";
import { BsStore } from "shared/models/bs-store.enum";
export const CreateLaunchShortcutModal: ModalComponent<{ steamShortcut: boolean, launchOption: LaunchOption }, BSVersion> = ({resolver, options: {data}}) => {
@@ -26,10 +25,6 @@ export const CreateLaunchShortcutModal: ModalComponent<{ steamShortcut: boolean,
const [command, setCommand] = useState(launchOption.command || "");
const [steamShortcut, setSteamShortcut] = useState(false);
const isSteamVersion = useMemo(() => {
return data.steam || data.metadata?.store === BsStore.STEAM;
}, [data]);
const completeModal = () => {
launchOption.command = command.trim();
resolver({exitCode: ModalExitCode.COMPLETED, data: { launchOption, steamShortcut }});
@@ -96,17 +91,15 @@ export const CreateLaunchShortcutModal: ModalComponent<{ steamShortcut: boolean,
</div>
</div>
</div>
{isSteamVersion && (
<Tippy placement="right" theme="default" content={t("modals.create-launch-shortcut.steam-shortcut-tippy")}>
<div className="h-full flex items-center gap-1.5 mt-3 mb-4 w-fit pr-1">
<BsmCheckbox className="h-5 aspect-square relative z-[1]" checked={steamShortcut} onChange={e => setSteamShortcut(() => e)} />
<span>{t("modals.create-launch-shortcut.create-steam-shortcut")}</span>
</div>
</Tippy>
)}
<div className="grid grid-flow-col grid-cols-2 gap-4 mt-2">
<BsmButton typeColor="cancel" className="rounded-md text-center transition-all" onClick={() => resolver({ exitCode: ModalExitCode.CANCELED })} withBar={false} text="misc.cancel" />
<BsmButton typeColor="primary" className="rounded-md text-center transition-all" onClick={completeModal} withBar={false} text="modals.create-launch-shortcut.valid-btn" />
<Tippy placement="right" theme="default" content={t("modals.create-launch-shortcut.steam-shortcut-tippy")}>
<div className="h-full flex items-center gap-1.5 mt-3 mb-4 w-fit pr-1">
<BsmCheckbox className="h-5 aspect-square relative z-[1]" checked={steamShortcut} onChange={e => setSteamShortcut(() => e)} />
<span>{t("modals.create-launch-shortcut.create-steam-shortcut")}</span>
</div>
</Tippy>
<div className="grid grid-flow-col grid-cols-2 gap-4 h-8">
<BsmButton typeColor="cancel" className="h-full flex items-center justify-center rounded-md text-center transition-all" onClick={() => resolver({ exitCode: ModalExitCode.CANCELED })} withBar={false} text="misc.cancel" />
<BsmButton typeColor="primary" className="h-full flex items-center justify-center rounded-md text-center transition-all" onClick={completeModal} withBar={false} text="modals.create-launch-shortcut.valid-btn" />
</div>
</form>
)
@@ -8,10 +8,11 @@ type Props<T> = {
selectedItemId?: number;
selectedItemValue?: T;
direction?: CSSProperties["flexDirection"],
columnCount?: number;
onItemSelected?: (item: RadioItem<T>) => void
};
export function SettingRadioArray<T>({ id, items, selectedItemId, selectedItemValue, onItemSelected, direction = "column" }: Props<T>) {
export function SettingRadioArray<T>({ id, items, selectedItemId, selectedItemValue, onItemSelected, direction = "column",columnCount = 1, }: Props<T>) {
const t = useTranslation();
const isSelected = (item: RadioItem<T>) => {
@@ -19,17 +20,17 @@ export function SettingRadioArray<T>({ id, items, selectedItemId, selectedItemVa
}
return (
<div id={id} className="w-full flex gap-1.5" style={{flexDirection: direction}}>
<div id={id} className="w-full grid gap-1.5" style={{flexDirection: direction, gridTemplateColumns: `repeat(${columnCount}, minmax(0, 1fr))`}}>
{items.map(i => (
<div onClick={() => onItemSelected(i)} key={i.id} className={`py-3 w-full flex cursor-pointer justify-between items-center rounded-md px-2 transition-colors duration-300 ${isSelected(i) ? "bg-light-main-color-3 dark:bg-main-color-3" : "bg-light-main-color-1 dark:bg-main-color-1"} ${i.className}`}>
<div onClick={() => onItemSelected(i)} key={i.id} className={`h-12 py-3 w-full flex cursor-pointer justify-between rounded-md px-2 transition-colors duration-300 ${isSelected(i) ? "bg-light-main-color-3 dark:bg-main-color-3" : "bg-light-main-color-1 dark:bg-main-color-1"} ${i.className}`}>
<div className="flex items-center">
<div className="h-5 rounded-full aspect-square border-2 border-gray-800 dark:border-white p-[3px] mr-2">
<motion.span initial={{ scale: 0 }} animate={{ scale: isSelected(i) ? 1 : 0 }} className="h-full w-full block bg-gray-800 dark:bg-white rounded-full" />
</div>
<h2 className="font-extrabold">{t(i.text)}</h2>
<h2 className="font-extrabold text-nowrap">{t(i.text)}</h2>
</div>
{i.icon && (
<div className="flex items-center">
<div className="flex items-center text-right">
{i.textIcon && <span className="text-sm">{t(i.textIcon)}</span>}
{i.icon}
</div>
@@ -12,36 +12,47 @@ import { useService } from "renderer/hooks/use-service.hook";
import { lastValueFrom } from "rxjs";
import { useWindowControls } from "renderer/hooks/use-window-controls.hook";
import { useTranslationV2 } from "renderer/hooks/use-translation.hook";
import Tippy from "@tippyjs/react";
import { StaticConfigurationService } from "renderer/services/static-configuration.service";
import { AutoUpdate } from "shared/models/config";
import { UpdateInfo } from "electron-updater";
function TitleBarTags() {
function useVersion() {
const ipcService = useService(IpcService);
const t = useTranslationV2();
const [previewVersion, setPreviewVersion] = useState("");
const [outdated, setOutdated] = useState(false);
const [currentVersion, setCurrentVersion] = useState("");
const [latestVersion, setLatestVersion] = useState<UpdateInfo | null>(null);
useEffect(() => {
const requests: Promise<any>[] = [
lastValueFrom(ipcService.sendV2("current-version"))
];
if (window.electron.platform === "linux") {
requests.push(lastValueFrom(ipcService.sendV2("check-update")));
}
Promise.all(requests).then(([ currentVersion, outdated ]) => {
handlePrerelease(currentVersion);
setOutdated(outdated);
Promise.all([
lastValueFrom(ipcService.sendV2("current-version")),
lastValueFrom(ipcService.sendV2("get-available-update"))
]).then(([ currentVersion, latestVersion ]) => {
setCurrentVersion(currentVersion);
setLatestVersion(latestVersion);
});
}, []);
const handlePrerelease = (version: string) => {
return { currentVersion, latestVersion };
}
function TitleBarTags({ version, latestVersion }: Readonly<{
version: string;
latestVersion: UpdateInfo | null;
}>) {
const t = useTranslationV2();
const previewVersion = (() => {
if (version.toLowerCase().includes("alpha")) {
return setPreviewVersion("ALPHA");
return "ALPHA";
}
if (version.toLowerCase().includes("beta")) {
return setPreviewVersion("BETA");
return "BETA";
}
}
return "";
})();
return <>
{previewVersion &&
@@ -49,7 +60,7 @@ function TitleBarTags() {
{previewVersion}
</span>
}
{outdated &&
{latestVersion &&
<span className="bg-warning-500 text-black rounded-full ml-1 text-[10px] italic px-1 uppercase h-3.5 font-bold">
{t.text("title-bar.outdated")}
</span>
@@ -57,12 +68,65 @@ function TitleBarTags() {
</>;
}
function AutoUpdateButton({ latestVersion }: Readonly<{
latestVersion: UpdateInfo | null;
}>) {
const configService = useService(StaticConfigurationService);
const ipcService = useService(IpcService);
const { text: t } = useTranslationV2();
const isLinux = window.electron.platform === "linux";
const updateAndRestart = async () => {
await configService.set("auto-update", AutoUpdate.ONCE);
await lastValueFrom(ipcService.sendV2("restart-app"));
};
const renderTippyContent = () => {
return (
<div className="p-2">
<div>{t("title-bar.update-text", { version: latestVersion.version })}</div>
{latestVersion?.version ? (
<a href={`https://github.com/Zagrios/bs-manager/releases/tag/v${latestVersion.version}`} target="_blank" className="cursor-pointer underline text-sm hover:text-gray-300">{t("title-bar.see-changelog")}</a>
) : null}
{!isLinux &&
<BsmButton typeColor="primary"
className="text-center rounded-md px-2 py-1 mt-2"
text={t("title-bar.update-button")}
withBar={false}
onClick={() => updateAndRestart()}
/>
}
</div>
)
}
return latestVersion && (
<Tippy
zIndex={1000}
placement="bottom"
content={renderTippyContent()}
theme="default"
hideOnClick
interactive
>
<BsmButton
className="shrink-0 w-11 h-full aspect-square !bg-transparent flex items-start p-0.5"
icon="download"
withBar={false}
/>
</Tippy>
);
}
export default function TitleBar({ template = "index.html" }: { template: AppWindow }) {
const audio = useService(AudioPlayerService);
const windowControls = useWindowControls();
const volume = useObservable(() => audio.volume$, audio.volume);
const color = useThemeColor("first-color");
const { currentVersion, latestVersion } = useVersion();
const [maximized, setMaximized] = useState(false);
@@ -107,7 +171,7 @@ export default function TitleBar({ template = "index.html" }: { template: AppWin
<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>
<TitleBarTags />
<TitleBarTags version={currentVersion} latestVersion={latestVersion} />
</div>
</div>
<div id="window-controls" className="h-full flex shrink-0 items-center">
@@ -117,6 +181,7 @@ export default function TitleBar({ template = "index.html" }: { template: AppWin
</div>
<BsmButton className="shrink-0 h-[23px] w-[23px] aspect-square !bg-transparent flex items-start" iconClassName={volumeIcon === "volume-down" ? "-translate-x-[1.8px]" : null} icon={volumeIcon} withBar={false} onClick={() => audio.toggleMute()} />
</div>
<AutoUpdateButton latestVersion={latestVersion} />
<button onClick={minimizeWindow} className="text-gray-800 dark:text-gray-200 hover:bg-gray-300 dark:hover:bg-[#4F545C] cursor-pointer w-11 h-full shrink-0 flex justify-center items-center" id="min-button">
<svg aria-hidden="false" width="12" height="12" viewBox="0 0 12 12">
<rect fill="currentColor" width="10" height="1" x="1" y="6" />
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { useObservable } from "renderer/hooks/use-observable.hook";
import { useTranslationV2 } from "renderer/hooks/use-translation.hook";
@@ -30,29 +30,13 @@ import { CreateCustomLaunchOptionModal } from "renderer/components/modal/modal-t
type Props = { version: BSVersion };
export function LaunchSlide({ version }: Props) {
const { text: t, element: te } = useTranslationV2();
const configService = useService(ConfigurationService);
function useLaunchMods() {
const bsLauncherService = useService(BSLauncherService);
const bsDownloader = useService(BsDownloaderService);
const versions = useService(BSVersionManagerService);
const modal = useService(ModalService);
const configService = useService(ConfigurationService);
const [advancedLaunch, setAdvancedLaunch] = useState(false);
const [command, setCommand] = useState<string>(configService.get<string>("launch-command") || "");
const customLaunchOptions = useObservable<CustomLaunchOption[]>(() => configService.watch<CustomLaunchOption[]>("custom-launch-options"), []);
const [customLaunchModsArgs, setCustomLaunchModsArgs] = useState<string[]>([]);
const versionDownloading = useObservable(() => bsDownloader.downloadingVersion$);
const [activeLaunchMods, setActiveLaunchMods] = useState<string[]>(configService.get("launch-mods") ?? []);
const [pinnedLaunchMods, setPinnedLaunchMods] = useState<string[]>(configService.get("pinned-launch-mods" as DefaultConfigKey) ?? []);
const versionRunning = useObservable(() => bsLauncherService.versionRunning$);
useEffect(() => {
configService.set("launch-command", command);
}, [command]);
useEffect(() => {
configService.set("pinned-launch-mods", pinnedLaunchMods);
}, [pinnedLaunchMods])
@@ -65,24 +49,66 @@ export function LaunchSlide({ version }: Props) {
}
}, [activeLaunchMods]);
const toggleActiveLaunchMod = (checked: boolean, launchMod: string) => checked
const toggleActiveLaunchMod = useCallback((checked: boolean, launchMod: string) => checked
? setActiveLaunchMods(prev => [...prev, launchMod])
: setActiveLaunchMods(prev => prev.filter(mod => mod !== launchMod));
: setActiveLaunchMods(prev => prev.filter(mod => mod !== launchMod)),
[]);
const togglePinnedLaunchMod = (pinned: boolean, launchMod: string) => pinned
const togglePinnedLaunchMod = useCallback((pinned: boolean, launchMod: string) => pinned
? setPinnedLaunchMods(prev => [...prev, launchMod])
: setPinnedLaunchMods(prev => prev.filter(mod => mod !== launchMod));
: setPinnedLaunchMods(prev => prev.filter(mod => mod !== launchMod)),
[]);
const launchModItems = useMemo<LaunchModItemProps[]>(() => {
return {
activeLaunchMods,
pinnedLaunchMods,
toggleActiveLaunchMod,
togglePinnedLaunchMod,
}
}
let protonLogsPath: string[] = [];
if (window.electron.platform === "linux") {
protonLogsPath = version.steam
? [version.path, "Logs"]
: ["BSInstances", version.name, "Logs"];
}
function useCustomLaunchMods({
activeLaunchMods, pinnedLaunchMods,
toggleActiveLaunchMod,
togglePinnedLaunchMod,
}: {
activeLaunchMods: string[];
pinnedLaunchMods: string[];
const customOptions = customLaunchOptions?.map<LaunchModItemProps>(option => ({
toggleActiveLaunchMod: (checked: boolean, launchMod: string) => void;
togglePinnedLaunchMod: (checked: boolean, launchMod: string) => void;
}) {
const configService = useService(ConfigurationService);
const modalService = useService(ModalService);
const customLaunchOptions = useObservable<CustomLaunchOption[]>(() => (
configService.watch<CustomLaunchOption[]>("custom-launch-options")
), []);
const [customLaunchModsArgs, setCustomLaunchModsArgs] = useState<string[]>([]);
useEffect(() => {
const command = customLaunchOptions
?.map(option => option.data.command || "")
.filter(Boolean);
setCustomLaunchModsArgs(() => command ?? []);
}, [customLaunchOptions]);
const deleteCustomLaunchOption = useCallback((id: string) => {
const newCustomLaunchOptions = (customLaunchOptions ?? []).filter(mode => mode.id !== id);
configService.set("custom-launch-options", newCustomLaunchOptions);
}, [customLaunchOptions]);
const saveCustomLaunchOption = useCallback(async (option: Partial<CustomLaunchOption>) => {
const result = await modalService.openModal(CreateCustomLaunchOptionModal, { data: option });
if(result.exitCode !== ModalExitCode.COMPLETED) { return; }
const newCustomLaunchOptions = (customLaunchOptions ?? []).filter(mode => mode.id !== result.data.id);
newCustomLaunchOptions.push(result.data);
configService.set("custom-launch-options", newCustomLaunchOptions);
}, [customLaunchOptions]);
const getCustomLaunch = useCallback(() => {
return customLaunchOptions?.map<LaunchModItemProps>(option => ({
id: option.id,
label: option.label,
active: activeLaunchMods.includes(option.id),
@@ -104,6 +130,57 @@ export function LaunchSlide({ version }: Props) {
deleteCustomLaunchOption(option.id);
},
})) ?? [];
}, [customLaunchOptions, activeLaunchMods, pinnedLaunchMods])
return {
customLaunchOptions,
customLaunchModsArgs,
getCustomLaunch,
saveCustomLaunchOption
};
}
export function LaunchSlide({ version }: Props) {
const { text: t, element: te } = useTranslationV2();
const configService = useService(ConfigurationService);
const bsLauncherService = useService(BSLauncherService);
const bsDownloader = useService(BsDownloaderService);
const versions = useService(BSVersionManagerService);
const [advancedLaunch, setAdvancedLaunch] = useState(false);
const [command, setCommand] = useState<string>(configService.get<string>("launch-command") || "");
const versionDownloading = useObservable(() => bsDownloader.downloadingVersion$);
const versionRunning = useObservable(() => bsLauncherService.versionRunning$);
const {
activeLaunchMods, pinnedLaunchMods,
toggleActiveLaunchMod, togglePinnedLaunchMod,
} = useLaunchMods();
const {
customLaunchOptions, customLaunchModsArgs,
getCustomLaunch, saveCustomLaunchOption,
} = useCustomLaunchMods({
activeLaunchMods, pinnedLaunchMods,
toggleActiveLaunchMod,
togglePinnedLaunchMod,
});
useEffect(() => {
configService.set("launch-command", command);
}, [command]);
const launchModItems = useMemo<LaunchModItemProps[]>(() => {
let protonLogsPath: string[] = [];
if (window.electron.platform === "linux") {
protonLogsPath = version.steam
? [version.path, "Logs"]
: ["BSInstances", version.name, "Logs"];
}
const customOptions = getCustomLaunch();
return [
{
@@ -187,19 +264,6 @@ export function LaunchSlide({ version }: Props) {
return safeLt(version?.BSVersion, versions.getRecommendedVersion()?.BSVersion);
}, [version]);
const saveCustomLaunchOption = async (option: Partial<CustomLaunchOption>) => {
const result = await modal.openModal(CreateCustomLaunchOptionModal, { data: option });
if(result.exitCode !== ModalExitCode.COMPLETED) { return; }
const newCustomLaunchOptions = (customLaunchOptions ?? []).filter(mode => mode.id !== result.data.id);
newCustomLaunchOptions.push(result.data);
configService.set("custom-launch-options", newCustomLaunchOptions);
}
const deleteCustomLaunchOption = (id: string) => {
const newCustomLaunchOptions = (customLaunchOptions ?? []).filter(mode => mode.id !== id);
configService.set("custom-launch-options", newCustomLaunchOptions);
}
return (
<div className="w-full shrink-0 items-center relative flex flex-col justify-start overflow-hidden">
<div className="flex flex-col gap-3 justify-center items-center mb-4">
+51 -6
View File
@@ -48,6 +48,7 @@ import { InstallationLocationService } from "renderer/services/installation-loca
import { AutoUpdaterService } from "renderer/services/auto-updater.service";
import { OculusDownloaderService } from "renderer/services/bs-version-download/oculus-downloader.service";
import { DISCORD_URL } from "shared/constants";
import { AutoUpdate } from "shared/models/config";
export function SettingsPage() {
@@ -201,11 +202,18 @@ export function SettingsPage() {
const fileChooserRes = await lastValueFrom(ipcService.sendV2("choose-folder"));
if (!fileChooserRes.canceled && fileChooserRes.filePaths?.length) {
const newInstallationPath = fileChooserRes.filePaths[0];
if(newInstallationPath === installationFolder){
return;
}
progressBarService.showFake(0.008);
notificationService.notifySuccess({ title: "notifications.settings.move-folder.success.titles.transfer-started", desc: "notifications.settings.move-folder.success.descs.transfer-started" });
lastValueFrom(installationLocationService.setInstallationFolder(fileChooserRes.filePaths[0], true)).then(res => {
lastValueFrom(installationLocationService.setInstallationFolder(newInstallationPath, true)).then(res => {
progressBarService.complete();
progressBarService.hide();
@@ -497,7 +505,7 @@ export function SettingsPage() {
</SettingContainer>
<SettingContainer title="pages.settings.language.title" description="pages.settings.language.description">
<SettingRadioArray items={languagesItems} selectedItemValue={languageSelected} onItemSelected={handleChangeLanguage} />
<SettingRadioArray items={languagesItems} selectedItemValue={languageSelected} onItemSelected={handleChangeLanguage} columnCount={2} />
</SettingContainer>
<SettingContainer title="pages.settings.patreon.title" description="pages.settings.patreon.description">
@@ -546,11 +554,17 @@ function AdvancedSettings() {
const [hardwareAccelerationEnabled, setHardwareAccelerationEnabled] = useState(true);
const [useSymlink, setUseSymlink] = useState(false);
const [useSystemProxy, setUseSystemProxy] = useState(false);
const [autoUpdate, setAutoUpdate] = useState<AutoUpdate>(AutoUpdate.NEVER);
useEffect(() => {
staticConfig.get("disable-hadware-acceleration").then(disabled =>setHardwareAccelerationEnabled(() => disabled !== true));
staticConfig.get("use-symlinks").then(useSymlinks => setUseSymlink(() => useSymlinks));
staticConfig.get("use-system-proxy").then(useSystemProxy => setUseSystemProxy(() => useSystemProxy));
if (window.electron.platform === "win32") {
staticConfig.get("use-symlinks").then(useSymlinks => setUseSymlink(() => useSymlinks));
staticConfig.get("use-system-proxy").then(useSystemProxy => setUseSystemProxy(() => useSystemProxy));
staticConfig.get("auto-update").then(res => setAutoUpdate(() => res ?? AutoUpdate.ALWAYS));
}
}, []);
const onChangeHardwareAcceleration = async (newHardwareAccelerationEnabled: boolean) => {
@@ -631,12 +645,43 @@ function AdvancedSettings() {
setUseSystemProxy(() => newUseSystemProxy);
}
const advancedItems: Item[] = [{
const onChangeAutoUpdate = async (value: boolean) => {
if (window.electron.platform !== "win32") {
return;
}
const newAutoUpdate = value ? AutoUpdate.ALWAYS : AutoUpdate.NEVER;
const { error } = await tryit(() => staticConfig.set("auto-update", newAutoUpdate));
if (error) {
notification.notifyError({
title: "notifications.types.error",
desc: "pages.settings.advanced.auto-update.error-notification.message",
});
return;
}
setAutoUpdate(() => newAutoUpdate);
}
const advancedItems: Item[] = [];
if (window.electron.platform === "win32") {
advancedItems.push({
checked: autoUpdate === AutoUpdate.ALWAYS,
text: t.text("pages.settings.advanced.auto-update.title"),
desc: t.text("pages.settings.advanced.auto-update.description"),
onChange: onChangeAutoUpdate
});
}
advancedItems.push({
checked: hardwareAccelerationEnabled,
text: t.text("pages.settings.advanced.hardware-acceleration.title"),
desc: t.text("pages.settings.advanced.hardware-acceleration.description"),
onChange: onChangeHardwareAcceleration
}];
});
if (window.electron.platform === "win32") {
advancedItems.push({
checked: useSymlink,
+8
View File
@@ -0,0 +1,8 @@
export enum AutoUpdate {
ALWAYS = "always",
// When "Update and restart" is clicked
ONCE = "once",
NEVER = "never",
}
+2
View File
@@ -21,6 +21,7 @@ import { LocalBPList, LocalBPListsDetails } from "../playlists/local-playlist.mo
import { StaticConfigGetIpcRequestResponse, StaticConfigKeys, StaticConfigSetIpcRequest } from "main/services/static-configuration.service";
import { BbmFullMod, BbmModVersion, ExternalMod } from "../mods/mod.interface";
import { OculusDownloadInfo } from "main/services/bs-version-download/bs-oculus-downloader.service";
import { UpdateInfo } from "electron-updater";
export type IpcReplier<T> = (data: Observable<T>) => void;
@@ -122,6 +123,7 @@ export interface IpcChannelMapping {
/* ** launcher-ipcs ** */
"download-update": { request: void, response: Progression };
"check-update": { request: void, response: boolean };
"get-available-update": { request: void, response: UpdateInfo | null };
"install-update": { request: void, response: void };
/* ** model-saber.ipcs ** */