diff --git a/assets/jsons/bs-versions.json b/assets/jsons/bs-versions.json
index 5b8fe316..1e27b40a 100644
--- a/assets/jsons/bs-versions.json
+++ b/assets/jsons/bs-versions.json
@@ -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
}
-]
\ No newline at end of file
+]
diff --git a/assets/jsons/patreons.json b/assets/jsons/patreons.json
index fd4ab56b..e32f2e99 100644
--- a/assets/jsons/patreons.json
+++ b/assets/jsons/patreons.json
@@ -161,6 +161,14 @@
},
{
"username": "KingCrocman",
+ "type": "diamond",
+ "link": "https://www.youtube.com/KingCrocman"
+ },
+ {
+ "username": "bosspie",
"type": "diamond"
+ },
+ {
+ "username": "Camryn Jackson"
}
]
diff --git a/assets/jsons/translations/de.json b/assets/jsons/translations/de.json
index 202810d7..03d88495 100644
--- a/assets/jsons/translations/de.json
+++ b/assets/jsons/translations/de.json
@@ -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."
+ }
}
}
}
diff --git a/assets/jsons/translations/en.json b/assets/jsons/translations/en.json
index 4ae373b0..fb33467b 100644
--- a/assets/jsons/translations/en.json
+++ b/assets/jsons/translations/en.json
@@ -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."
+ }
}
}
}
diff --git a/assets/jsons/translations/es.json b/assets/jsons/translations/es.json
index 507b3be5..18d87f1f 100644
--- a/assets/jsons/translations/es.json
+++ b/assets/jsons/translations/es.json
@@ -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."
+ }
}
}
}
diff --git a/assets/jsons/translations/fr.json b/assets/jsons/translations/fr.json
index fbfcab63..640f6ded 100644
--- a/assets/jsons/translations/fr.json
+++ b/assets/jsons/translations/fr.json
@@ -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."
+ }
}
}
}
diff --git a/assets/jsons/translations/it.json b/assets/jsons/translations/it.json
index a0e51c9c..c7e542b4 100644
--- a/assets/jsons/translations/it.json
+++ b/assets/jsons/translations/it.json
@@ -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."
+ }
}
}
}
diff --git a/assets/jsons/translations/ja.json b/assets/jsons/translations/ja.json
index 47600eb5..d2d05150 100644
--- a/assets/jsons/translations/ja.json
+++ b/assets/jsons/translations/ja.json
@@ -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": "エラーが発生し、自動更新の設定を変更できませんでした。"
+ }
}
}
}
diff --git a/assets/jsons/translations/ko.json b/assets/jsons/translations/ko.json
index d051216d..24dc70e5 100644
--- a/assets/jsons/translations/ko.json
+++ b/assets/jsons/translations/ko.json
@@ -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": "오류가 발생하여 자동 업데이트 설정을 변경할 수 없습니다."
+ }
}
}
}
diff --git a/assets/jsons/translations/pt-br.json b/assets/jsons/translations/pt-br.json
index 36a86561..76921b70 100644
--- a/assets/jsons/translations/pt-br.json
+++ b/assets/jsons/translations/pt-br.json
@@ -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."
+ }
}
}
}
diff --git a/assets/jsons/translations/ru.json b/assets/jsons/translations/ru.json
index 5ba36e17..ce71eeda 100644
--- a/assets/jsons/translations/ru.json
+++ b/assets/jsons/translations/ru.json
@@ -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": "Произошла ошибка, не удалось изменить настройки автообновления."
+ }
}
}
}
diff --git a/assets/jsons/translations/tl.json b/assets/jsons/translations/tl.json
index 42ea75c4..748bcd43 100644
--- a/assets/jsons/translations/tl.json
+++ b/assets/jsons/translations/tl.json
@@ -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."
+ }
}
}
}
diff --git a/assets/jsons/translations/uk.json b/assets/jsons/translations/uk.json
index 260e831c..903d9824 100644
--- a/assets/jsons/translations/uk.json
+++ b/assets/jsons/translations/uk.json
@@ -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": "Сталася помилка, неможливо змінити налаштування автоматичного оновлення."
+ }
}
}
}
diff --git a/assets/jsons/translations/zh-tw.json b/assets/jsons/translations/zh-tw.json
index 6d3553f9..84985291 100644
--- a/assets/jsons/translations/zh-tw.json
+++ b/assets/jsons/translations/zh-tw.json
@@ -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": "發生錯誤,無法更改自動更新設定。"
+ }
}
}
}
diff --git a/assets/jsons/translations/zh.json b/assets/jsons/translations/zh.json
index f6ee41f0..5f171a95 100644
--- a/assets/jsons/translations/zh.json
+++ b/assets/jsons/translations/zh.json
@@ -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": "发生错误,无法更改自动更新设置。"
+ }
}
}
}
diff --git a/docs/wiki/Guides/Login/How-to-obtain-your-Oculus-Token/How-to-obtain-your-Oculus-Token.md b/docs/wiki/Guides/Login/How-to-obtain-your-Oculus-Token/How-to-obtain-your-Oculus-Token.md
index 294856ac..4d139b61 100644
--- a/docs/wiki/Guides/Login/How-to-obtain-your-Oculus-Token/How-to-obtain-your-Oculus-Token.md
+++ b/docs/wiki/Guides/Login/How-to-obtain-your-Oculus-Token/How-to-obtain-your-Oculus-Token.md
@@ -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.

diff --git a/package-lock.json b/package-lock.json
index f1c3bf53..12a462ac 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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"
diff --git a/package.json b/package.json
index 5d4735dd..b7347f4d 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/release/app/package-lock.json b/release/app/package-lock.json
index 6547d474..770e7ae1 100644
--- a/release/app/package-lock.json
+++ b/release/app/package-lock.json
@@ -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": {
diff --git a/release/app/package.json b/release/app/package.json
index 3d4f58ca..6edc5731 100644
--- a/release/app/package.json
+++ b/release/app/package.json
@@ -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": {
diff --git a/src/main/helpers/proxy.helpers.ts b/src/main/helpers/proxy.helpers.ts
index 0a102b13..98175230 100644
--- a/src/main/helpers/proxy.helpers.ts
+++ b/src/main/helpers/proxy.helpers.ts
@@ -51,7 +51,15 @@ async function enableWindowsProxy(enable: boolean): Promise
{
}
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))}`;
diff --git a/src/main/ipcs/launcher-ipcs.ts b/src/main/ipcs/launcher-ipcs.ts
index 1091cbf7..ee92cdd2 100644
--- a/src/main/ipcs/launcher-ipcs.ts
+++ b/src/main/ipcs/launcher-ipcs.ts
@@ -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()));
diff --git a/src/main/main.ts b/src/main/main.ts
index 60d4bd92..40c9e9d3 100644
--- a/src/main/main.ts
+++ b/src/main/main.ts
@@ -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();
diff --git a/src/main/services/additional-content/local-playlists-manager.service.ts b/src/main/services/additional-content/local-playlists-manager.service.ts
index 7f22684b..328cf509 100644
--- a/src/main/services/additional-content/local-playlists-manager.service.ts
+++ b/src/main/services/additional-content/local-playlists-manager.service.ts
@@ -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;
diff --git a/src/main/services/auto-updater.service.ts b/src/main/services/auto-updater.service.ts
index 9a054115..fd8a6a4f 100644
--- a/src/main/services/auto-updater.service.ts
+++ b/src/main/services/auto-updater.service.ts
@@ -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 {
+ 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 {
return new Observable(observer => {
diff --git a/src/main/services/bs-local-version.service.ts b/src/main/services/bs-local-version.service.ts
index 5e73f99d..1c8d29ee 100644
--- a/src/main/services/bs-local-version.service.ts
+++ b/src/main/services/bs-local-version.service.ts
@@ -244,8 +244,12 @@ export class BSLocalVersionService {
private async getSteamVersion(): Promise {
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) {
diff --git a/src/main/services/linux.service.ts b/src/main/services/linux.service.ts
index 7f92bed4..5f12896a 100644
--- a/src/main/services/linux.service.ts
+++ b/src/main/services/linux.service.ts
@@ -129,12 +129,7 @@ export class LinuxService {
return fs.pathExistsSync(protonPath) && fs.pathExistsSync(winePath);
}
- public async getWinePath(): Promise {
- 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");
}
diff --git a/src/main/services/mods/beat-mods-api.service.ts b/src/main/services/mods/beat-mods-api.service.ts
index c0136b96..8e63f141 100644
--- a/src/main/services/mods/beat-mods-api.service.ts
+++ b/src/main/services/mods/beat-mods-api.service.ts
@@ -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);
diff --git a/src/main/services/mods/bs-mods-manager.service.ts b/src/main/services/mods/bs-mods-manager.service.ts
index 5f726093..60cc1905 100644
--- a/src/main/services/mods/bs-mods-manager.service.ts
+++ b/src/main/services/mods/bs-mods-manager.service.ts
@@ -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(resolve => {
diff --git a/src/main/services/request.service.ts b/src/main/services/request.service.ts
index 8ade56ce..863c1bd9 100644
--- a/src/main/services/request.service.ts
+++ b/src/main/services/request.service.ts
@@ -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 = {};
public static getInstance(): RequestService {
if (!RequestService.instance) {
RequestService.instance = new RequestService();
}
return RequestService.instance;
-
}
private constructor() {}
- public async getJSON(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(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(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(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(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>((subscriber) => {
const progress: Progression = { 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
): Observable> {
-
return new Observable>((subscriber) => {
const progress: Progression = {
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();
diff --git a/src/main/services/static-configuration.service.ts b/src/main/services/static-configuration.service.ts
index 47e99245..4244f5a2 100644
--- a/src/main/services/static-configuration.service.ts
+++ b/src/main/services/static-configuration.service.ts
@@ -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(key: K): StaticConfigKeyValues[K] {
- return this.store.get(key) as StaticConfigKeyValues[K];
+ public get(key: K, defaultValue?: StaticConfigKeyValues[K]): StaticConfigKeyValues[K] {
+ return this.store.get(key, defaultValue) as StaticConfigKeyValues[K];
}
public take(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;
diff --git a/src/renderer/components/maps-playlists-panel/maps/local-maps-list-panel.component.tsx b/src/renderer/components/maps-playlists-panel/maps/local-maps-list-panel.component.tsx
index 39b670c4..d7e4f7a8 100644
--- a/src/renderer/components/maps-playlists-panel/maps/local-maps-list-panel.component.tsx
+++ b/src/renderer/components/maps-playlists-panel/maps/local-maps-list-panel.component.tsx
@@ -249,7 +249,7 @@ export const LocalMapsListPanel = forwardRef(({
return (
= ({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,
- {isSteamVersion && (
-
-
- setSteamShortcut(() => e)} />
- {t("modals.create-launch-shortcut.create-steam-shortcut")}
-
-
- )}
-
-
resolver({ exitCode: ModalExitCode.CANCELED })} withBar={false} text="misc.cancel" />
-
+
+
+ setSteamShortcut(() => e)} />
+ {t("modals.create-launch-shortcut.create-steam-shortcut")}
+
+
+
+ resolver({ exitCode: ModalExitCode.CANCELED })} withBar={false} text="misc.cancel" />
+
)
diff --git a/src/renderer/components/settings/setting-radio-array.component.tsx b/src/renderer/components/settings/setting-radio-array.component.tsx
index 1a7f2e60..05f1d083 100644
--- a/src/renderer/components/settings/setting-radio-array.component.tsx
+++ b/src/renderer/components/settings/setting-radio-array.component.tsx
@@ -8,10 +8,11 @@ type Props = {
selectedItemId?: number;
selectedItemValue?: T;
direction?: CSSProperties["flexDirection"],
+ columnCount?: number;
onItemSelected?: (item: RadioItem) => void
};
-export function SettingRadioArray({ id, items, selectedItemId, selectedItemValue, onItemSelected, direction = "column" }: Props) {
+export function SettingRadioArray({ id, items, selectedItemId, selectedItemValue, onItemSelected, direction = "column",columnCount = 1, }: Props) {
const t = useTranslation();
const isSelected = (item: RadioItem) => {
@@ -19,17 +20,17 @@ export function SettingRadioArray({ id, items, selectedItemId, selectedItemVa
}
return (
-
+
{items.map(i => (
-
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}`}>
+
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}`}>
-
{t(i.text)}
+
{t(i.text)}
{i.icon && (
-
+
{i.textIcon && {t(i.textIcon)}}
{i.icon}
diff --git a/src/renderer/components/title-bar/title-bar.component.tsx b/src/renderer/components/title-bar/title-bar.component.tsx
index b17647e6..73ef4401 100644
--- a/src/renderer/components/title-bar/title-bar.component.tsx
+++ b/src/renderer/components/title-bar/title-bar.component.tsx
@@ -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
(null);
useEffect(() => {
- const requests: Promise[] = [
- 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}
}
- {outdated &&
+ {latestVersion &&
{t.text("title-bar.outdated")}
@@ -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 (
+
+
{t("title-bar.update-text", { version: latestVersion.version })}
+ {latestVersion?.version ? (
+
{t("title-bar.see-changelog")}
+ ) : null}
+ {!isLinux &&
+
updateAndRestart()}
+ />
+ }
+
+ )
+ }
+
+ return latestVersion && (
+
+
+
+ );
+}
+
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
@@ -117,6 +181,7 @@ export default function TitleBar({ template = "index.html" }: { template: AppWin
audio.toggleMute()} />
+