mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
Merge branch 'Zagrios:master' into patch-1
This commit is contained in:
@@ -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
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -161,6 +161,14 @@
|
||||
},
|
||||
{
|
||||
"username": "KingCrocman",
|
||||
"type": "diamond",
|
||||
"link": "https://www.youtube.com/KingCrocman"
|
||||
},
|
||||
{
|
||||
"username": "bosspie",
|
||||
"type": "diamond"
|
||||
},
|
||||
{
|
||||
"username": "Camryn Jackson"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "エラーが発生し、自動更新の設定を変更できませんでした。"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "오류가 발생하여 자동 업데이트 설정을 변경할 수 없습니다."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "Произошла ошибка, не удалось изменить настройки автообновления."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "Сталася помилка, неможливо змінити налаштування автоматичного оновлення."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "發生錯誤,無法更改自動更新設定。"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "发生错误,无法更改自动更新设置。"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-5
@@ -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" />
|
||||
|
||||
Generated
+27
-27
@@ -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
@@ -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",
|
||||
|
||||
Generated
+2
-2
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "bs-manager",
|
||||
"version": "1.5.3-alpha.1",
|
||||
"version": "1.5.3",
|
||||
"description": "BSManager",
|
||||
"main": "./dist/main/main.js",
|
||||
"author": {
|
||||
|
||||
@@ -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))}`;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
+1
-1
@@ -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}
|
||||
|
||||
+10
-17
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
export enum AutoUpdate {
|
||||
ALWAYS = "always",
|
||||
// When "Update and restart" is clicked
|
||||
ONCE = "once",
|
||||
NEVER = "never",
|
||||
}
|
||||
|
||||
@@ -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 ** */
|
||||
|
||||
Reference in New Issue
Block a user