Merge pull request #282 from Zagrios/feature/launch-bs-from-shortcut

[feature] Launch Beat Saber from shortcut
This commit is contained in:
MathieuG-P
2023-07-16 10:49:11 +02:00
committed by GitHub
48 changed files with 3498 additions and 1253 deletions
@@ -152,6 +152,7 @@ const configuration: webpack.Configuration = {
new HtmlWebpackPlugin(getHtmlPageOptions("oneclick-download-map.html")),
new HtmlWebpackPlugin(getHtmlPageOptions("oneclick-download-playlist.html")),
new HtmlWebpackPlugin(getHtmlPageOptions("oneclick-download-model.html")),
new HtmlWebpackPlugin(getHtmlPageOptions("shortcut-launch.html")),
],
node: {
@@ -146,6 +146,7 @@ const configuration: webpack.Configuration = {
new HtmlWebpackPlugin(getHtmlPageOptions("oneclick-download-map.html")),
new HtmlWebpackPlugin(getHtmlPageOptions("oneclick-download-playlist.html")),
new HtmlWebpackPlugin(getHtmlPageOptions("oneclick-download-model.html")),
new HtmlWebpackPlugin(getHtmlPageOptions("shortcut-launch.html")),
],
};
File diff suppressed because it is too large Load Diff
+39 -2
View File
@@ -97,6 +97,7 @@
"clone": "Klonen",
"edit": "Bearbeiten",
"uninstall": "Entfernen",
"create-shortcut": "Verknüpfung erstellen",
"shared-folders": "Geteilte Ordner"
}
},
@@ -335,12 +336,17 @@
"bs-launch": {
"success": {
"titles": {
"launching": "Starte...🚀"
"BS_LAUNCHING": "Starte...🚀",
"STEAM_LAUNCHING": "Steam startet!"
},
"msg": {
"BS_LAUNCHING": "Vergiss nicht, dich aufzuwärmen 😉",
"STEAM_LAUNCHING": "Beat Saber wird automatisch nach Steam gestartet."
}
},
"errors": {
"titles": {
"UNABLE_TO_LAUNCH": "Start nicht möglich",
"UNKNOWN_ERROR": "Start nicht möglich",
"STEAM_NOT_RUNNING": "Steam läuft nicht",
"OCULUS_NOT_RUNNING": "Oculus läuft nicht",
"BS_ALREADY_RUNNING": "BeatSaber läuft bereits",
@@ -348,6 +354,7 @@
"EXIT": "Abrupter Stop"
},
"msg": {
"UNKNOWN_ERROR": "Ein unbekannter Fehler ist aufgetreten.",
"STEAM_NOT_RUNNING": "Steam muss laufen, um BeatSaber zu starten.",
"OCULUS_NOT_RUNNING": "Oculus muss ausgeführt werden, um BeatSaber zu starten.",
"BS_ALREADY_RUNNING": "Schließe BeatSaber, bevor Du es erneut startest.",
@@ -445,6 +452,15 @@
"msg": "Das Freigeben des Ordners „UserData“ kann zu Fehlern führen. Bei Problemen trenne den Ordner, um die Sicherung wiederherzustellen"
}
}
},
"create-launch-shortcut": {
"success": {
"title": "Verknüpfung erstellt",
"msg": "Die Verknüpfung wurde auf dem Desktop erstellt."
},
"error": {
"msg": "Beim Erstellen der Verknüpfung ist ein Fehler aufgetreten."
}
}
},
"modals": {
@@ -605,6 +621,13 @@
"unlink-folder": "Ordner Verknüpfung Aufheben",
"link-all": "Alle verlinken"
}
},
"create-launch-shortcut": {
"title": "Verknüpfung erstellen",
"desc": "Das Erstellen einer Verknüpfung ermöglicht es Ihnen, Beat Saber mit den ausgewählten Optionen zu starten, ohne durch BSManager zu gehen.",
"launch-options": "Startoptionen",
"advanced-launch": "Erweiterte Start",
"valid-btn": "Verknüpfung erstellen"
}
},
"maps": {
@@ -855,6 +878,20 @@
"checking": "Prüfe auf Updates",
"downloading": "Lade Updates Herunter"
},
"bs-shortcut-launch": {
"beat-saber-launching": "Start von Beat Saber",
"launching": "Starten",
"open-bsmanager": "BSManager öffnen",
"status-text": {
"init": "Initialisierung...",
"success": {
"BS_LAUNCHING": "Start von Beat Saber...",
"STEAM_LAUNCHING": "Start von Steam...",
"STEAM_LAUNCHED": "Steam erfolgreich gestartet!",
"UNABLE_TO_LAUNCH_STEAM": "Steam kann nicht gestartet werden, erzwungener Start von Beat Saber..."
}
}
},
"dateformat": {
"dayNames": ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa", "Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"],
"monthNames": ["Jan", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez", "Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"],
+44 -7
View File
@@ -97,6 +97,7 @@
"clone": "Clone",
"edit": "Edit",
"uninstall": "Uninstall",
"create-shortcut": "Create a shortcut",
"shared-folders": "Shared Folders"
}
},
@@ -335,12 +336,17 @@
"bs-launch": {
"success": {
"titles": {
"launching": "Launching...🚀"
"BS_LAUNCHING": "Launching...🚀",
"STEAM_LAUNCHING": "Steam is launching!"
},
"msg": {
"BS_LAUNCHING": "Don't forget to warm up 😉",
"STEAM_LAUNCHING": "Beat Saber will automatically launch after Steam."
}
},
"errors": {
"titles": {
"UNABLE_TO_LAUNCH": "Unable to launch",
"UNKNOWN_ERROR": "Unable to launch",
"STEAM_NOT_RUNNING": "Steam is not running",
"OCULUS_NOT_RUNNING": "Oculus is not running",
"BS_ALREADY_RUNNING": "Beat Saber already running",
@@ -348,11 +354,12 @@
"EXIT": "Abrupt stop"
},
"msg": {
"STEAM_NOT_RUNNING": "Steam must be running to launch Beat Saber.",
"OCULUS_NOT_RUNNING": "Oculus must be running to launch Beat Saber.",
"BS_ALREADY_RUNNING": "Close Beat Saber before launching it again.",
"EXE_NOT_FINDED": "Some files seem to be missing, try to verify the game's files.",
"EXIT": "Beat Saber stopped abruptly, try to verify the game's files."
"UNKNOWN_ERROR": "An unknown error has occurred.",
"STEAM_NOT_RUNNING": "Steam must be running to launch BeatSaber.",
"OCULUS_NOT_RUNNING": "Oculus must be running to launch BeatSaber.",
"BS_ALREADY_RUNNING": "Close BeatSaber before launching it again.",
"EXE_NOT_FINDED": "Some files seem to be missing, try to verify the files.",
"EXIT": "BeatSaber stopped abruptly, tries to check the files."
},
"actions": {
"STEAM_NOT_RUNNING": "Launch Steam"
@@ -445,6 +452,15 @@
"msg": "Sharing the 'UserData' folder can generate errors, in case of problems unlink the folder to restore the backup"
}
}
},
"create-launch-shortcut": {
"success": {
"title": "Shortcut created",
"msg": "The shortcut has been created on the desktop."
},
"error": {
"msg": "An error occurred while creating the shortcut."
}
}
},
"modals": {
@@ -605,6 +621,13 @@
"unlink-folder": "Unlink Folder",
"link-all": "Link all"
}
},
"create-launch-shortcut": {
"title": "Create a shortcut",
"desc": "Creating a shortcut will allow you to start Beat Saber with the chosen options without going through BSManager.",
"launch-options": "Launch options",
"advanced-launch": "Advanced launch",
"valid-btn": "Create the shortcut"
}
},
"maps": {
@@ -855,6 +878,20 @@
"checking": "Checking for updates",
"downloading": "Downloading updates"
},
"bs-shortcut-launch": {
"beat-saber-launching": "Launching Beat Saber",
"launching": "Launching",
"open-bsmanager": "Open BSManager",
"status-text": {
"init": "Initializing...",
"success": {
"BS_LAUNCHING": "Launching Beat Saber...",
"STEAM_LAUNCHING": "Launching Steam...",
"STEAM_LAUNCHED": "Steam successfully launched!",
"UNABLE_TO_LAUNCH_STEAM": "Unable to launch Steam, forcing Beat Saber launch..."
}
}
},
"dateformat": {
"dayNames": ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
"monthNames": ["Jan", "Feb", "Mar", "Apr", "May", "June", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
+39 -2
View File
@@ -96,6 +96,7 @@
"clone": "Clonar",
"edit": "Editar",
"uninstall": "Desinstalar",
"create-shortcut": "Crear un atajo",
"shared-folders": "Carpetas Compartidas"
}
},
@@ -334,12 +335,17 @@
"bs-launch": {
"success": {
"titles": {
"launching": "Lanzamiento...🚀"
"BS_LAUNCHING": "Lanzamiento...🚀",
"STEAM_LAUNCHING": "¡Steam se está iniciando!"
},
"msg": {
"BS_LAUNCHING": "No olvides calentarte 😉",
"STEAM_LAUNCHING": "Beat Saber se lanzará automáticamente después de Steam."
}
},
"errors": {
"titles": {
"UNABLE_TO_LAUNCH": "No se puede lanzar",
"UNKNOWN_ERROR": "No se puede lanzar",
"STEAM_NOT_RUNNING": "Steam no funciona",
"OCULUS_NOT_RUNNING": "Oculus no funciona",
"BS_ALREADY_RUNNING": "BeatSaber ya está en marcha",
@@ -347,6 +353,7 @@
"EXIT": "Parada abrupta"
},
"msg": {
"UNKNOWN_ERROR": "Se ha producido un error desconocido.",
"STEAM_NOT_RUNNING": "Para iniciar BeatSaber es necesario que Steam esté en funcionamiento.",
"OCULUS_NOT_RUNNING": "Oculus debe estar funcionando para lanzar BeatSaber.",
"BS_ALREADY_RUNNING": "Cierra BeatSaber antes de volver a lanzarlo.",
@@ -444,6 +451,15 @@
"msg": "Compartir la carpeta 'UserData' puede generar errores, en caso de problemas desvincular la carpeta para restaurar la copia de seguridad"
}
}
},
"create-launch-shortcut": {
"success": {
"title": "Acceso directo creado",
"msg": "El acceso directo se ha creado en el escritorio."
},
"error": {
"msg": "Se produjo un error al crear el acceso directo."
}
}
},
"modals": {
@@ -604,6 +620,13 @@
"unlink-folder": "Desenlazar Carpeta",
"link-all": "Enlazar todo"
}
},
"create-launch-shortcut": {
"title": "Crear un atajo",
"desc": "Crear un atajo te permitirá iniciar Beat Saber con las opciones seleccionadas sin pasar por BSManager.",
"launch-options": "Opciones de lanzamiento",
"advanced-launch": "Lanzamiento avanzado",
"valid-btn": "Crear el atajo"
}
},
"maps": {
@@ -854,6 +877,20 @@
"checking": "Comprobar las actualizaciones",
"downloading": "Descargar actualizaciones"
},
"bs-shortcut-launch": {
"beat-saber-launching": "Iniciando Beat Saber",
"launching": "Iniciando",
"open-bsmanager": "Abrir BSManager",
"status-text": {
"init": "Inicializando...",
"success": {
"BS_LAUNCHING": "Iniciando Beat Saber...",
"STEAM_LAUNCHING": "Iniciando Steam...",
"STEAM_LAUNCHED": "¡Steam iniciado exitosamente!",
"UNABLE_TO_LAUNCH_STEAM": "Incapaz de iniciar Steam, forzando el inicio de Beat Saber..."
}
}
},
"dateformat": {
"dayNames": ["Dom", "Lun", "Mar", "Mié", "Jue", "Vie", "Sáb", "Domingo", "Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado"],
"monthNames": ["Ene", "Feb", "Mar", "Abr", "May", "Jun", "Jul", "Ago", "Sep", "Oct", "Nov", "Dic", "Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"],
+39 -2
View File
@@ -96,6 +96,7 @@
"clone": "Cloner",
"edit": "Editer",
"uninstall": "Désinstaller",
"create-shortcut": "Créer un raccourci",
"shared-folders": "Dossiers Partagés"
}
},
@@ -334,12 +335,17 @@
"bs-launch": {
"success": {
"titles": {
"launching": "Lancement...🚀"
"BS_LAUNCHING": "Lancement...🚀",
"STEAM_LAUNCHING": "Steam se lance !"
},
"msg": {
"BS_LAUNCHING": "N'oublie pas de t'échauffer 😉",
"STEAM_LAUNCHING": "Beat Saber se lancera automatiquement après Steam."
}
},
"errors": {
"titles": {
"UNABLE_TO_LAUNCH": "Lancement impossible",
"UNKNOWN_ERROR": "Lancement impossible",
"STEAM_NOT_RUNNING": "Steam n'est pas lancé",
"OCULUS_NOT_RUNNING": "Oculus n'est pas lancé",
"BS_ALREADY_RUNNING": "Beat Saber est déjà lancé",
@@ -347,6 +353,7 @@
"EXIT": "Arrêt brutal"
},
"msg": {
"UNKNOWN_ERROR": "Une erreur inconnue s'est produite.",
"STEAM_NOT_RUNNING": "Steam doit être en cours d'exécution pour lancer Beat Saber.",
"OCULUS_NOT_RUNNING": "Oculus doit être en cours d'exécution pour lancer Beat Saber.",
"BS_ALREADY_RUNNING": "Ferme Beat Saber avant de le lancer à nouveau.",
@@ -444,6 +451,15 @@
"msg": "Le partage du dossier 'UserData' peut générer des erreurs, en cas de soucis déliez le dossier pour restaurer la sauvegarde"
}
}
},
"create-launch-shortcut": {
"success": {
"title": "Raccourci créé",
"msg": "Le raccourci a été créé sur le bureau."
},
"error": {
"msg": "Une erreur s'est produite lors de la création du raccourci."
}
}
},
"modals": {
@@ -604,6 +620,13 @@
"unlink-folder": "Délier le dossier",
"link-all": "Tout lier"
}
},
"create-launch-shortcut": {
"title": "Créer un raccourci",
"desc": "Créer un raccourci te permettra de démarrer Beat Saber avec les options choisi sans passé par BSManager.",
"launch-options": "Options de lancement",
"advanced-launch": "Lancement avancé",
"valid-btn": "Créer le raccourci"
}
},
"maps": {
@@ -854,6 +877,20 @@
"checking": "Vérification des mises à jour",
"downloading": "Téléchargement des mises à jour"
},
"bs-shortcut-launch": {
"beat-saber-launching": "Lancement de Beat Saber",
"launching": "Lancement",
"open-bsmanager": "Ouvrir BSManager",
"status-text": {
"init": "Initialisation...",
"success": {
"BS_LAUNCHING": "Lancement de Beat Saber...",
"STEAM_LAUNCHING": "Lancement de Steam...",
"STEAM_LAUNCHED": "Steam lancé avec succès !",
"UNABLE_TO_LAUNCH_STEAM": "Impossible de lancer Steam, lancement forcé de Beat Saber..."
}
}
},
"dateformat": {
"dayNames": ["Dim", "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dimanche", "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi"],
"monthNames": ["Jan", "Fév", "Mar", "Avr", "Mai", "Jun", "Juil", "Aou", "Sept", "Oct", "Nov", "Déc", "Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"],
+1322 -190
View File
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -146,6 +146,7 @@
]
},
"devDependencies": {
"@electron/rebuild": "^3.2.13",
"@pmmmwh/react-refresh-webpack-plugin": "0.5.5",
"@teamsupercell/typings-for-css-modules-loader": "^2.5.1",
"@testing-library/jest-dom": "^5.16.5",
@@ -164,6 +165,7 @@
"@types/react-window": "^1.8.5",
"@types/recursive-readdir": "^2.2.1",
"@types/terser-webpack-plugin": "^5.0.4",
"@types/to-ico": "^1.1.1",
"@types/use-double-click": "^1.0.1",
"@types/webpack-bundle-analyzer": "^4.4.2",
"@types/webpack-env": "^1.18.0",
@@ -182,7 +184,6 @@
"electron-builder": "^24.4.0",
"electron-devtools-installer": "^3.2.0",
"electron-notarize": "^1.2.1",
"electron-rebuild": "^3.2.9",
"electronmon": "^2.0.2",
"eslint": "^8.22.0",
"eslint-config-airbnb-base": "^15.0.0",
@@ -266,6 +267,7 @@
"tailwind-scrollbar-hide": "^1.1.7",
"tailwindcss-scoped-groups": "^2.0.0",
"tippy.js": "^6.3.7",
"to-ico": "^1.1.5",
"use-double-click": "^1.0.5",
"use-fit-text": "^2.4.0"
},
+336 -25
View File
@@ -1,29 +1,340 @@
{
"name": "bs-manager",
"version": "1.2.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "bs-manager",
"version": "1.2.0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"ctrlc-windows": "^2.1.0"
}
},
"node_modules/ctrlc-windows": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ctrlc-windows/-/ctrlc-windows-2.1.0.tgz",
"integrity": "sha512-OrX5KI+K+2NMN91QIhYZdW7VDO2YsSdTZW494pA7Nvw/wBdU2hz+MGP006bR978zOTrG6Q8EIeJvLJmLqc6MsQ=="
}
"name": "bs-manager",
"version": "1.2.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "bs-manager",
"version": "1.2.0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@resvg/resvg-js": "^2.4.1",
"ctrlc-windows": "^2.1.0",
"ps-list": "^7.2.0"
}
},
"dependencies": {
"ctrlc-windows": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ctrlc-windows/-/ctrlc-windows-2.1.0.tgz",
"integrity": "sha512-OrX5KI+K+2NMN91QIhYZdW7VDO2YsSdTZW494pA7Nvw/wBdU2hz+MGP006bR978zOTrG6Q8EIeJvLJmLqc6MsQ=="
}
"node_modules/@resvg/resvg-js": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js/-/resvg-js-2.4.1.tgz",
"integrity": "sha512-wTOf1zerZX8qYcMmLZw3czR4paI4hXqPjShNwJRh5DeHxvgffUS5KM7XwxtbIheUW6LVYT5fhT2AJiP6mU7U4A==",
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
"@resvg/resvg-js-android-arm-eabi": "2.4.1",
"@resvg/resvg-js-android-arm64": "2.4.1",
"@resvg/resvg-js-darwin-arm64": "2.4.1",
"@resvg/resvg-js-darwin-x64": "2.4.1",
"@resvg/resvg-js-linux-arm-gnueabihf": "2.4.1",
"@resvg/resvg-js-linux-arm64-gnu": "2.4.1",
"@resvg/resvg-js-linux-arm64-musl": "2.4.1",
"@resvg/resvg-js-linux-x64-gnu": "2.4.1",
"@resvg/resvg-js-linux-x64-musl": "2.4.1",
"@resvg/resvg-js-win32-arm64-msvc": "2.4.1",
"@resvg/resvg-js-win32-ia32-msvc": "2.4.1",
"@resvg/resvg-js-win32-x64-msvc": "2.4.1"
}
},
"node_modules/@resvg/resvg-js-android-arm-eabi": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm-eabi/-/resvg-js-android-arm-eabi-2.4.1.tgz",
"integrity": "sha512-AA6f7hS0FAPpvQMhBCf6f1oD1LdlqNXKCxAAPpKh6tR11kqV0YIB9zOlIYgITM14mq2YooLFl6XIbbvmY+jwUw==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@resvg/resvg-js-android-arm64": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm64/-/resvg-js-android-arm64-2.4.1.tgz",
"integrity": "sha512-/QleoRdPfsEuH9jUjilYcDtKK/BkmWcK+1LXM8L2nsnf/CI8EnFyv7ZzCj4xAIvZGAy9dTYr/5NZBcTwxG2HQg==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@resvg/resvg-js-darwin-arm64": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-arm64/-/resvg-js-darwin-arm64-2.4.1.tgz",
"integrity": "sha512-U1oMNhea+kAXgiEXgzo7EbFGCD1Edq5aSlQoe6LMly6UjHzgx2W3N5kEXCwU/CgN5FiQhZr7PlSJSlcr7mdhfg==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@resvg/resvg-js-darwin-x64": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-x64/-/resvg-js-darwin-x64-2.4.1.tgz",
"integrity": "sha512-avyVh6DpebBfHHtTQTZYSr6NG1Ur6TEilk1+H0n7V+g4F7x7WPOo8zL00ZhQCeRQ5H4f8WXNWIEKL8fwqcOkYw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@resvg/resvg-js-linux-arm-gnueabihf": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm-gnueabihf/-/resvg-js-linux-arm-gnueabihf-2.4.1.tgz",
"integrity": "sha512-isY/mdKoBWH4VB5v621co+8l101jxxYjuTkwOLsbW+5RK9EbLciPlCB02M99ThAHzI2MYxIUjXNmNgOW8btXvw==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@resvg/resvg-js-linux-arm64-gnu": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-gnu/-/resvg-js-linux-arm64-gnu-2.4.1.tgz",
"integrity": "sha512-uY5voSCrFI8TH95vIYBm5blpkOtltLxLRODyhKJhGfskOI7XkRw5/t1u0sWAGYD8rRSNX+CA+np86otKjubrNg==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@resvg/resvg-js-linux-arm64-musl": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-musl/-/resvg-js-linux-arm64-musl-2.4.1.tgz",
"integrity": "sha512-6mT0+JBCsermKMdi/O2mMk3m7SqOjwi9TKAwSngRZ/nQoL3Z0Z5zV+572ztgbWr0GODB422uD8e9R9zzz38dRQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@resvg/resvg-js-linux-x64-gnu": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-gnu/-/resvg-js-linux-x64-gnu-2.4.1.tgz",
"integrity": "sha512-60KnrscLj6VGhkYOJEmmzPlqqfcw1keDh6U+vMcNDjPhV3B5vRSkpP/D/a8sfokyeh4VEacPSYkWGezvzS2/mg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@resvg/resvg-js-linux-x64-musl": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-musl/-/resvg-js-linux-x64-musl-2.4.1.tgz",
"integrity": "sha512-0AMyZSICC1D7ge115cOZQW8Pcad6PjWuZkBFF3FJuSxC6Dgok0MQnLTs2MfMdKBlAcwO9dXsf3bv9tJZj8pATA==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@resvg/resvg-js-win32-arm64-msvc": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-arm64-msvc/-/resvg-js-win32-arm64-msvc-2.4.1.tgz",
"integrity": "sha512-76XDFOFSa3d0QotmcNyChh2xHwk+JTFiEQBVxMlHpHMeq7hNrQJ1IpE1zcHSQvrckvkdfLboKRrlGB86B10Qjw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@resvg/resvg-js-win32-ia32-msvc": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-ia32-msvc/-/resvg-js-win32-ia32-msvc-2.4.1.tgz",
"integrity": "sha512-odyVFGrEWZIzzJ89KdaFtiYWaIJh9hJRW/frcEcG3agJ464VXkN/2oEVF5ulD+5mpGlug9qJg7htzHcKxDN8sg==",
"cpu": [
"ia32"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@resvg/resvg-js-win32-x64-msvc": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-x64-msvc/-/resvg-js-win32-x64-msvc-2.4.1.tgz",
"integrity": "sha512-vY4kTLH2S3bP+puU5x7hlAxHv+ulFgcK6Zn3efKSr0M0KnZ9A3qeAjZteIpkowEFfUeMPNg2dvvoFRJA9zqxSw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/ctrlc-windows": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ctrlc-windows/-/ctrlc-windows-2.1.0.tgz",
"integrity": "sha512-OrX5KI+K+2NMN91QIhYZdW7VDO2YsSdTZW494pA7Nvw/wBdU2hz+MGP006bR978zOTrG6Q8EIeJvLJmLqc6MsQ=="
},
"node_modules/ps-list": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/ps-list/-/ps-list-7.2.0.tgz",
"integrity": "sha512-v4Bl6I3f2kJfr5o80ShABNHAokIgY+wFDTQfE+X3zWYgSGQOCBeYptLZUpoOALBqO5EawmDN/tjTldJesd0ujQ==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
}
},
"dependencies": {
"@resvg/resvg-js": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js/-/resvg-js-2.4.1.tgz",
"integrity": "sha512-wTOf1zerZX8qYcMmLZw3czR4paI4hXqPjShNwJRh5DeHxvgffUS5KM7XwxtbIheUW6LVYT5fhT2AJiP6mU7U4A==",
"requires": {
"@resvg/resvg-js-android-arm-eabi": "2.4.1",
"@resvg/resvg-js-android-arm64": "2.4.1",
"@resvg/resvg-js-darwin-arm64": "2.4.1",
"@resvg/resvg-js-darwin-x64": "2.4.1",
"@resvg/resvg-js-linux-arm-gnueabihf": "2.4.1",
"@resvg/resvg-js-linux-arm64-gnu": "2.4.1",
"@resvg/resvg-js-linux-arm64-musl": "2.4.1",
"@resvg/resvg-js-linux-x64-gnu": "2.4.1",
"@resvg/resvg-js-linux-x64-musl": "2.4.1",
"@resvg/resvg-js-win32-arm64-msvc": "2.4.1",
"@resvg/resvg-js-win32-ia32-msvc": "2.4.1",
"@resvg/resvg-js-win32-x64-msvc": "2.4.1"
}
},
"@resvg/resvg-js-android-arm-eabi": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm-eabi/-/resvg-js-android-arm-eabi-2.4.1.tgz",
"integrity": "sha512-AA6f7hS0FAPpvQMhBCf6f1oD1LdlqNXKCxAAPpKh6tR11kqV0YIB9zOlIYgITM14mq2YooLFl6XIbbvmY+jwUw==",
"optional": true
},
"@resvg/resvg-js-android-arm64": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm64/-/resvg-js-android-arm64-2.4.1.tgz",
"integrity": "sha512-/QleoRdPfsEuH9jUjilYcDtKK/BkmWcK+1LXM8L2nsnf/CI8EnFyv7ZzCj4xAIvZGAy9dTYr/5NZBcTwxG2HQg==",
"optional": true
},
"@resvg/resvg-js-darwin-arm64": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-arm64/-/resvg-js-darwin-arm64-2.4.1.tgz",
"integrity": "sha512-U1oMNhea+kAXgiEXgzo7EbFGCD1Edq5aSlQoe6LMly6UjHzgx2W3N5kEXCwU/CgN5FiQhZr7PlSJSlcr7mdhfg==",
"optional": true
},
"@resvg/resvg-js-darwin-x64": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-x64/-/resvg-js-darwin-x64-2.4.1.tgz",
"integrity": "sha512-avyVh6DpebBfHHtTQTZYSr6NG1Ur6TEilk1+H0n7V+g4F7x7WPOo8zL00ZhQCeRQ5H4f8WXNWIEKL8fwqcOkYw==",
"optional": true
},
"@resvg/resvg-js-linux-arm-gnueabihf": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm-gnueabihf/-/resvg-js-linux-arm-gnueabihf-2.4.1.tgz",
"integrity": "sha512-isY/mdKoBWH4VB5v621co+8l101jxxYjuTkwOLsbW+5RK9EbLciPlCB02M99ThAHzI2MYxIUjXNmNgOW8btXvw==",
"optional": true
},
"@resvg/resvg-js-linux-arm64-gnu": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-gnu/-/resvg-js-linux-arm64-gnu-2.4.1.tgz",
"integrity": "sha512-uY5voSCrFI8TH95vIYBm5blpkOtltLxLRODyhKJhGfskOI7XkRw5/t1u0sWAGYD8rRSNX+CA+np86otKjubrNg==",
"optional": true
},
"@resvg/resvg-js-linux-arm64-musl": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-musl/-/resvg-js-linux-arm64-musl-2.4.1.tgz",
"integrity": "sha512-6mT0+JBCsermKMdi/O2mMk3m7SqOjwi9TKAwSngRZ/nQoL3Z0Z5zV+572ztgbWr0GODB422uD8e9R9zzz38dRQ==",
"optional": true
},
"@resvg/resvg-js-linux-x64-gnu": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-gnu/-/resvg-js-linux-x64-gnu-2.4.1.tgz",
"integrity": "sha512-60KnrscLj6VGhkYOJEmmzPlqqfcw1keDh6U+vMcNDjPhV3B5vRSkpP/D/a8sfokyeh4VEacPSYkWGezvzS2/mg==",
"optional": true
},
"@resvg/resvg-js-linux-x64-musl": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-musl/-/resvg-js-linux-x64-musl-2.4.1.tgz",
"integrity": "sha512-0AMyZSICC1D7ge115cOZQW8Pcad6PjWuZkBFF3FJuSxC6Dgok0MQnLTs2MfMdKBlAcwO9dXsf3bv9tJZj8pATA==",
"optional": true
},
"@resvg/resvg-js-win32-arm64-msvc": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-arm64-msvc/-/resvg-js-win32-arm64-msvc-2.4.1.tgz",
"integrity": "sha512-76XDFOFSa3d0QotmcNyChh2xHwk+JTFiEQBVxMlHpHMeq7hNrQJ1IpE1zcHSQvrckvkdfLboKRrlGB86B10Qjw==",
"optional": true
},
"@resvg/resvg-js-win32-ia32-msvc": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-ia32-msvc/-/resvg-js-win32-ia32-msvc-2.4.1.tgz",
"integrity": "sha512-odyVFGrEWZIzzJ89KdaFtiYWaIJh9hJRW/frcEcG3agJ464VXkN/2oEVF5ulD+5mpGlug9qJg7htzHcKxDN8sg==",
"optional": true
},
"@resvg/resvg-js-win32-x64-msvc": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-x64-msvc/-/resvg-js-win32-x64-msvc-2.4.1.tgz",
"integrity": "sha512-vY4kTLH2S3bP+puU5x7hlAxHv+ulFgcK6Zn3efKSr0M0KnZ9A3qeAjZteIpkowEFfUeMPNg2dvvoFRJA9zqxSw==",
"optional": true
},
"ctrlc-windows": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ctrlc-windows/-/ctrlc-windows-2.1.0.tgz",
"integrity": "sha512-OrX5KI+K+2NMN91QIhYZdW7VDO2YsSdTZW494pA7Nvw/wBdU2hz+MGP006bR978zOTrG6Q8EIeJvLJmLqc6MsQ=="
},
"ps-list": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/ps-list/-/ps-list-7.2.0.tgz",
"integrity": "sha512-v4Bl6I3f2kJfr5o80ShABNHAokIgY+wFDTQfE+X3zWYgSGQOCBeYptLZUpoOALBqO5EawmDN/tjTldJesd0ujQ=="
}
}
}
+20 -18
View File
@@ -1,20 +1,22 @@
{
"name": "bs-manager",
"version": "1.2.0",
"description": "BSManager",
"main": "./dist/main/main.js",
"author": {
"name": "Zagrios",
"email": "admin@bsmanager.io",
"url": "https://github.com/Zagrios/bs-manager"
},
"scripts": {
"electron-rebuild": "node -r ts-node/register ../../.erb/scripts/electron-rebuild.js",
"link-modules": "node -r ts-node/register ../../.erb/scripts/link-modules.ts",
"postinstall": "npm run electron-rebuild && npm run link-modules"
},
"dependencies": {
"ctrlc-windows": "^2.1.0"
},
"license": "MIT"
"name": "bs-manager",
"version": "1.2.0",
"description": "BSManager",
"main": "./dist/main/main.js",
"author": {
"name": "Zagrios",
"email": "admin@bsmanager.io",
"url": "https://github.com/Zagrios/bs-manager"
},
"scripts": {
"electron-rebuild": "node -r ts-node/register ../../.erb/scripts/electron-rebuild.js",
"link-modules": "node -r ts-node/register ../../.erb/scripts/link-modules.ts",
"postinstall": "npm run electron-rebuild && npm run link-modules"
},
"dependencies": {
"@resvg/resvg-js": "^2.4.1",
"ctrlc-windows": "^2.1.0",
"ps-list": "^7.2.0"
},
"license": "MIT"
}
+6
View File
@@ -1,3 +1,6 @@
import { app } from "electron";
import path from "path";
export const BS_EXECUTABLE = "Beat Saber.exe";
export const OCULUS_BS_DIR = "hyperbolic-magnetism-beat-saber";
export const BS_APP_ID = "620980";
@@ -5,3 +8,6 @@ export const BS_DEPOT = "620981";
export const APP_NAME = "BSManager";
export const STEAMVR_APP_ID = "250820";
export const IMAGE_CACHE_FOLDER = "imagescache";
export const IMAGE_CACHE_PATH = path.join(app.getPath("userData"), IMAGE_CACHE_FOLDER);
+13
View File
@@ -0,0 +1,13 @@
import { ProviderPlatform } from "shared/models/provider-platform.enum";
export function execOnOs<T>(executions: { [key in ProviderPlatform]?: () => T }, noError = false): T {
if(executions[process.platform as ProviderPlatform]) {
return executions[process.platform as ProviderPlatform]();
}
if(!noError) {
throw new Error(`No execution found for platform ${process.platform}`);
}
return undefined;
}
+14 -17
View File
@@ -1,20 +1,17 @@
import { ipcMain } from "electron";
import { UtilsService } from "../services/utils.service";
import { LauchOption } from "shared/models/bs-launch";
import { BSLauncherService } from "../services/bs-launcher.service";
import { IpcRequest } from "shared/models/ipc";
import { BsmException } from "shared/models/bsm-exception.model";
ipcMain.on("bs-launch.launch", (event, request: IpcRequest<LauchOption>) => {
const launcherService = BSLauncherService.getInstance();
const utilsService = UtilsService.getInstance();
import { LaunchOption } from "shared/models/bs-launch";
import { BSLauncherService } from "../services/bs-launcher.service"
import { IpcService } from '../services/ipc.service';
import { from } from "rxjs";
launcherService
.launch(request.args)
.then(res => {
utilsService.ipcSend(request.responceChannel, { success: true, data: res });
})
.catch((err: BsmException) => {
utilsService.ipcSend(request.responceChannel, { success: false, error: err });
});
const ipc = IpcService.getInstance();
ipc.on<LaunchOption>('bs-launch.launch', (req, reply) => {
const bsLauncher = BSLauncherService.getInstance();
reply(bsLauncher.launch(req.args));
});
ipc.on<LaunchOption>("create-launch-shortcut", (req, reply) => {
const bsLauncher = BSLauncherService.getInstance();
reply(from(bsLauncher.createLaunchShortcut(req.args)));
});
+8
View File
@@ -3,8 +3,11 @@ import { WindowManagerService } from "../services/window-manager.service";
import { IpcRequest } from "shared/models/ipc";
import { AppWindow } from "shared/models/window-manager/app-window.model";
import { BSLauncherService } from "../services/bs-launcher.service";
import { IpcService } from "../services/ipc.service";
import { from } from "rxjs";
const launcher = BSLauncherService.getInstance();
const ipc = IpcService.getInstance();
ipcMain.on("open-window-then-close-all", async (event, request: IpcRequest<AppWindow>) => {
const windowManager = WindowManagerService.getInstance();
@@ -25,3 +28,8 @@ ipcMain.on("close-windows", async (event, request: IpcRequest<AppWindow[]>) => {
const windowManager = WindowManagerService.getInstance();
windowManager.close(...request.args);
});
ipc.on<AppWindow>("open-window-or-focus", (req, reply) => {
const windowManager = WindowManagerService.getInstance();
reply(from(windowManager.openWindowOrFocus(req.args)));
});
+28 -29
View File
@@ -1,5 +1,3 @@
/* eslint global-require: off, no-console: off, promise/always-return: off */
/**
* This module executes inside of electron's main process. You can start
* electron renderer process from here and communicate with the other processes
@@ -65,8 +63,9 @@ const initServicesMustBeInitialized = () => {
LocalMapsManagerService.getInstance();
LocalPlaylistsManagerService.getInstance();
LocalModelsManagerService.getInstance();
// Model
};
BSLauncherService.getInstance();
}
const gotTheLock = app.requestSingleInstanceLock();
@@ -83,31 +82,31 @@ if (!gotTheLock) {
DeepLinkService.getInstance().dispatchLinkOpened(deepLink);
});
app.whenReady()
.then(() => {
app.setAppUserModelId(APP_NAME);
app.whenReady().then(() => {
app.setAppUserModelId(APP_NAME);
initServicesMustBeInitialized();
initServicesMustBeInitialized();
const deepLink = process.argv.find(arg => DeepLinkService.getInstance().isDeepLink(arg));
const deepLink = process.argv.find(arg => DeepLinkService.getInstance().isDeepLink(arg));
if (!deepLink) {
createWindow();
} else {
DeepLinkService.getInstance().dispatchLinkOpened(deepLink);
}
protocol.registerFileProtocol("file", (request, callback) => {
const pathname = decodeURI(request.url.replace("file:///", ""));
callback(pathname);
});
BSLauncherService.getInstance().restoreSteamVR();
// Log renderer errors
ipcMain.on("log-error", (event, args: IpcRequest<any>) => {
log.error(args?.args);
});
})
.catch(log.error);
if (!deepLink) {
createWindow();
} else {
DeepLinkService.getInstance().dispatchLinkOpened(deepLink);
}
protocol.registerFileProtocol("file", (request, callback) => {
const pathname = decodeURI(request.url.replace("file:///", ""));
callback(pathname);
});
BSLauncherService.getInstance().restoreSteamVR();
// Log renderer errors
ipcMain.on("log-error", (event, args: IpcRequest<any>) => {
log.error(args?.args);
});
}).catch(log.error);
}
+6 -6
View File
@@ -1,6 +1,6 @@
import { BS_APP_ID, BS_DEPOT } from "../constants";
import path from "path";
import { BSVersion, PartialBSVersion } from "shared/bs-version.interface";
import { BSVersion } from "shared/bs-version.interface";
import { UtilsService } from "./utils.service";
import { ChildProcessWithoutNullStreams, spawn, spawnSync } from "child_process";
import log from "electron-log";
@@ -194,18 +194,18 @@ export class BSInstallerService {
return destPath;
}
public async importVersion(path: string): Promise<PartialBSVersion> {
const rawBsVersion = await this.localVersionService.getVersionOfBSFolder(path);
public async importVersion(path: string): Promise<BSVersion> {
const version = await this.localVersionService.getVersionOfBSFolder(path);
if (!rawBsVersion) {
if (!version) {
throw new Error("NOT_BS_FOLDER");
}
const destPath = await this.getPathNotAleardyExist(await this.localVersionService.getVersionPath(rawBsVersion));
const destPath = await this.getPathNotAleardyExist(await this.localVersionService.getVersionPath(version));
await copy(path, destPath, { dereference: true });
return rawBsVersion;
return version;
}
}
+274 -69
View File
@@ -1,28 +1,36 @@
import path from "path";
import { LaunchResult, LauchOption } from "shared/models/bs-launch";
import { LaunchOption, BSLaunchEvent, BSLaunchEventData, BSLaunchWarning, BSLaunchErrorData, BSLaunchError } from "../../shared/models/bs-launch";
import { UtilsService } from "./utils.service";
import { BS_EXECUTABLE, BS_APP_ID, STEAMVR_APP_ID } from "../constants";
import { ChildProcessWithoutNullStreams, spawn } from "child_process";
import { BS_EXECUTABLE, BS_APP_ID, STEAMVR_APP_ID, IMAGE_CACHE_PATH } from "../constants";
import { ChildProcessWithoutNullStreams, SpawnOptionsWithoutStdio, spawn } from "child_process";
import { SteamService } from "./steam.service";
import { BSLocalVersionService } from "./bs-local-version.service";
import { OculusService } from "./oculus.service";
import { pathExist } from "../helpers/fs.helpers";
import { rename } from "fs/promises";
import log from "electron-log";
import { timer } from "rxjs";
import { NotificationService } from "./notification.service";
import { NotificationType } from "../../shared/models/notification/notification.model";
import { Observable, lastValueFrom, of, timer } from "rxjs";
import { BsmProtocolService } from "./bsm-protocol.service";
import { app, shell} from "electron";
import { Resvg } from "@resvg/resvg-js";
import Color from "color";
import { ensureDir, writeFile } from "fs-extra";
import toIco from "to-ico";
import { objectFromEntries } from "../../shared/helpers/object.helpers";
import { WindowManagerService } from "./window-manager.service";
import { IpcService } from "./ipc.service";
import { BSVersionLibService } from "./bs-version-lib.service";
import { execOnOs } from "../helpers/env.helpers";
export class BSLauncherService {
private static instance: BSLauncherService;
private readonly utilsService: UtilsService;
private readonly steamService: SteamService;
private readonly oculusService: OculusService;
private readonly localVersionService: BSLocalVersionService;
private readonly notification: NotificationService;
private bsProcess: ChildProcessWithoutNullStreams;
private readonly bsmProtocolService: BsmProtocolService;
private readonly windows: WindowManagerService;
private readonly ipc: IpcService;
private readonly remoteVersion: BSVersionLibService;
public static getInstance(): BSLauncherService {
if (!BSLauncherService.instance) {
@@ -34,9 +42,17 @@ export class BSLauncherService {
private constructor() {
this.utilsService = UtilsService.getInstance();
this.steamService = SteamService.getInstance();
this.oculusService = OculusService.getInstance();
this.localVersionService = BSLocalVersionService.getInstance();
this.notification = NotificationService.getInstance();
this.bsmProtocolService = BsmProtocolService.getInstance();
this.windows = WindowManagerService.getInstance();
this.ipc = IpcService.getInstance();
this.remoteVersion = BSVersionLibService.getInstance();
this.bsmProtocolService.on("launch", link => {
log.info("Launch from bsm protocol", link.toString());
const shortcutParams = objectFromEntries(link.searchParams.entries()) as ShortcutParams;
this.openShortcutLaunchWindow(shortcutParams);
});
}
private getSteamVRPath(): Promise<string> {
@@ -61,43 +77,15 @@ export class BSLauncherService {
}
public async isBsRunning(): Promise<boolean> {
return this.bsProcess?.connected || (await this.utilsService.taskRunning(BS_EXECUTABLE)) === true;
return this.utilsService.taskRunning(BS_EXECUTABLE);
}
// TODO : Rework with shortcuts implementation
public async launch(launchOptions: LauchOption): Promise<LaunchResult> {
if ((await this.isBsRunning()) === true) {
return "BS_ALREADY_RUNNING";
}
if (launchOptions.version.oculus && (await this.oculusService.oculusRunning()) === false) {
return "OCULUS_NOT_RUNNING";
}
private buildBsLaunchArgs(launchOptions: LaunchOption): string[]{
const launchArgs = [];
const steamRunning = await this.steamService.steamRunning().catch(() => true); // True if error (error not not means that steam is not running)
if (!launchOptions.version.oculus && !steamRunning) {
this.notification.notifyRenderer({
title: "notifications.steam.steam-launching.title",
desc: "notifications.steam.steam-launching.description",
type: NotificationType.SUCCESS,
});
await this.steamService.openSteam().catch(log.error);
}
const cwd = await this.localVersionService.getVersionPath(launchOptions.version);
const exePath = path.join(cwd, BS_EXECUTABLE);
if (!(await pathExist(exePath))) {
return "EXE_NOT_FINDED";
}
let launchArgs = [];
if (!launchOptions.version.steam && !launchOptions.version.oculus) {
launchArgs.push("--no-yeet");
}
if (launchOptions.oculus) {
launchArgs.push("-vrmode oculus");
launchArgs.push("-vrmode");
launchArgs.push("oculus");
}
if (launchOptions.desktop) {
launchArgs.push("fpfc");
@@ -109,35 +97,252 @@ export class BSLauncherService {
launchArgs.push(...launchOptions.additionalArgs);
}
launchArgs = Array.from(new Set(launchArgs).values());
return Array.from(new Set(launchArgs).values());
}
let restoreTimout: NodeJS.Timeout;
private launchBSProcess(bsExePath: string, args: string[], debug = false): ChildProcessWithoutNullStreams{
if (launchArgs.includes("fpfc")) {
await this.backupSteamVR()
.catch(() => {
this.restoreSteamVR();
})
.finally(() => {
restoreTimout = setTimeout(() => this.restoreSteamVR(), 35_000);
const spawnOptions: SpawnOptionsWithoutStdio = { detached: true, cwd: path.dirname(bsExePath), env: {...process.env, "SteamAppId": BS_APP_ID} };
if(debug){
spawnOptions.windowsVerbatimArguments = true;
}
return spawn(bsExePath, args, spawnOptions);
}
public launch(launchOptions: LaunchOption): Observable<BSLaunchEventData>{
return new Observable<BSLaunchEventData>(obs => {(async () => {
if(launchOptions.skipAlreadyRunning !== true && await this.isBsRunning()){
return obs.error({type: BSLaunchError.BS_ALREADY_RUNNING} as BSLaunchErrorData);
}
const bsFolderPath = await this.localVersionService.getInstalledVersionPath(launchOptions.version);
const exePath = path.join(bsFolderPath, BS_EXECUTABLE);
if(!(await pathExist(exePath))){
return obs.error({type: BSLaunchError.BS_NOT_FOUND} as BSLaunchErrorData);
}
// Open Steam if not running
if(!launchOptions.version.oculus && !(await this.steamService.steamRunning())){
obs.next({type: BSLaunchEvent.STEAM_LAUNCHING});
await this.steamService.openSteam().then(() => {
obs.next({type: BSLaunchEvent.STEAM_LAUNCHED});
}).catch(e => {
log.error(e);
obs.next({type: BSLaunchWarning.UNABLE_TO_LAUNCH_STEAM});
});
await timer(2_000).toPromise();
} else {
await this.restoreSteamVR();
}
}
if (launchOptions.debug) {
this.bsProcess = spawn(`"${exePath}"`, launchArgs, { shell: true, cwd, env: { ...process.env, SteamAppId: BS_APP_ID }, detached: true, windowsVerbatimArguments: true });
} else {
this.bsProcess = spawn(`"${exePath}"`, launchArgs, { shell: true, cwd, env: { ...process.env, SteamAppId: BS_APP_ID } });
}
// Backup SteamVR when desktop mode is enabled
if(!launchOptions.version.oculus && launchOptions.desktop){
await this.backupSteamVR().catch(() => {
return this.restoreSteamVR();
});
await lastValueFrom(timer(2_000));
} else if(!launchOptions.version.oculus){
await this.restoreSteamVR();
}
this.bsProcess.on("error", err => log.error(err));
this.bsProcess.on("exit", code => {
this.restoreSteamVR().finally(() => clearTimeout(restoreTimout));
this.utilsService.ipcSend("bs-launch.exit", { data: code, success: code === 0 });
const launchArgs = this.buildBsLaunchArgs(launchOptions);
obs.next({type: BSLaunchEvent.BS_LAUNCHING});
await new Promise<number>((resolve, reject) => {
const bsProcess = this.launchBSProcess(exePath, launchArgs, launchOptions.debug);
bsProcess.on("error", reject);
bsProcess.on("exit", resolve);
setTimeout(() => {
bsProcess.removeAllListeners("error");
bsProcess.removeAllListeners("exit");
resolve(-1);
}, 4000);
}).then(exitCode => {
log.info("BS process exit code", exitCode);
}).catch(err => {
log.error(err);
obs.error({type: BSLaunchError.BS_EXIT_ERROR, data: err} as BSLaunchErrorData);
});
})().then(() => {
obs.complete();
}).catch(err => {
obs.error({type: BSLaunchError.UNKNOWN_ERROR, data: err} as BSLaunchErrorData);
})});
}
private shortcutParamsToLaunchOption(params: ShortcutParams): LaunchOption{
const res: LaunchOption = {
version: {
BSVersion: params.version,
name: params.versionName,
steam: params.versionSteam === "true",
oculus: params.versionOculus === "true",
ino: +params.versionIno
},
oculus: params.oculusMode === "true",
desktop: params.desktopMode === "true",
debug: params.debug === "true",
additionalArgs: params.additionalArgs
};
return res;
}
private launchOptionToShortcutParams(launchOptions: LaunchOption): ShortcutParams{
const res: ShortcutParams = { version: launchOptions.version.BSVersion };
if(launchOptions.version.name){ res.versionName = launchOptions.version.name; }
if(launchOptions.version.steam){ res.versionSteam = `${launchOptions.version.steam}`; }
if(launchOptions.version.oculus){ res.versionOculus = `${launchOptions.version.oculus}`; }
if(launchOptions.version.ino){ res.versionIno = `${launchOptions.version.ino}`; }
if(launchOptions.oculus){ res.oculusMode = "true"; }
if(launchOptions.desktop){ res.desktopMode = "true"; }
if(launchOptions.debug){ res.debug = "true"; }
if(launchOptions.additionalArgs){ res.additionalArgs = launchOptions.additionalArgs; }
return res;
}
private createShortcutPngBuffer(color: Color): Buffer{
const svgIcon = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 406.4 406.4" height="406.4" width="406.4">
<rect rx="69.453" height="406.4" width="406.4" fill="${color.hex()}"/>
<path d="M65.467 60.6H336.4v33.867L200.933 162.2 65.467 94.467z" fill="#fff"/>
</svg>
`;
return new Resvg(svgIcon, {
fitTo: { mode: "width", value: 256 }
}).render().asPng();
}
/**
* Create .png file for the shortcut with the given color
* @param {Color} color
* @returns {Promise<string>} Path of the icon
*/
private async createShortcutPng(color: Color): Promise<string>{
const pngBuffer = this.createShortcutPngBuffer(color);
await ensureDir(IMAGE_CACHE_PATH);
const iconPath = path.join(IMAGE_CACHE_PATH, `launch_shortcut_${color.hex()}.png`);
await writeFile(iconPath, pngBuffer);
return iconPath;
}
/**
* Create .ico file for the shortcut with the given color
* @param {Color} color
* @returns {Promise<string>} Path of the icon
*/
private async createShortcutIco(color: Color): Promise<string>{
const pngBuffer = this.createShortcutPngBuffer(color);
const icoBuffer = await toIco([pngBuffer]);
await ensureDir(IMAGE_CACHE_PATH);
const iconPath = path.join(IMAGE_CACHE_PATH, `launch_shortcut_${color.hex()}.ico`);
await writeFile(iconPath, icoBuffer);
return iconPath;
}
public async createLaunchShortcut(launchOptions: LaunchOption): Promise<boolean>{
const shortcutParams = this.launchOptionToShortcutParams(launchOptions);
const shortcutUrl = this.bsmProtocolService.buildLink("launch", shortcutParams).toString();
const shortcutName = ["Beat Saber", launchOptions.version.BSVersion, launchOptions.version.name].join(" ");
const shortcutIconColor = new Color(launchOptions.version.color, "hex");
return execOnOs({
win32: async () => (
shell.writeShortcutLink(path.join(app.getPath("desktop"), `${shortcutName}.lnk`), {
target: shortcutUrl,
icon: await this.createShortcutIco(shortcutIconColor),
iconIndex: 0,
description: [shortcutName, launchOptions.version.color].join(" "), // <= Need color in description to help windows know that the shortcut is different
})
),
linux: async () => (
createDesktopUrlShortcut(path.join(app.getPath("desktop"), `${shortcutName}.desktop`), {
name: shortcutName,
url: shortcutUrl,
icon: await this.createShortcutPng(shortcutIconColor),
})
)
})
}
private async openShortcutLaunchWindow(launchOptions: ShortcutParams): Promise<void>{
const launchOption = this.shortcutParamsToLaunchOption(launchOptions);
const bsPath = await this.localVersionService.getInstalledVersionPath(launchOption.version);
launchOption.version = await this.localVersionService.getVersionOfBSFolder(bsPath, {
steam: launchOption.version.steam,
oculus: launchOption.version.oculus,
});
return "LAUNCHED";
launchOption.version = {...(await this.remoteVersion.getVersionDetails(launchOption.version.BSVersion)), ...launchOption.version};
this.ipc.once("shortcut-launch-options", (_data, reply) => {
reply(of(launchOption));
});
this.windows.openWindow("shortcut-launch.html");
}
}
type ShortcutParams = {
oculusMode?: string;
desktopMode?: string;
debug?: string;
additionalArgs?: string[];
version: string;
versionName?: string;
versionIno?: string;
versionSteam?: string;
versionOculus?: string;
}
/**
* Create .desktop file for url shortcut (only for linux)
* @param {string} shortcutPath
* @param options
* @returns
*/
function createDesktopUrlShortcut(shortcutPath: string, options?: {
url: string
name: string,
icon: string
}): Promise<boolean> {
const { url, name, icon } = options || {};
const data = [
"[Desktop Entry]",
"Type=Link",
`Name=${name}`,
`Icon=${icon}`,
`URL=${url}`
].join("\n");
return writeFile(shortcutPath, data).then(() => true);
}
+172 -156
View File
@@ -1,12 +1,11 @@
import { BSVersionLibService } from "./bs-version-lib.service";
import { BSVersion, PartialBSVersion } from "shared/bs-version.interface";
import { BSVersion } from "shared/bs-version.interface";
import { InstallationLocationService } from "./installation-location.service";
import { SteamService } from "./steam.service";
import { BS_APP_ID, OCULUS_BS_DIR } from "../constants";
import path from "path";
import { createReadStream } from "fs";
import { ConfigurationService } from "./configuration.service";
import { rename } from "fs/promises";
import { lstat, rename } from "fs/promises";
import { BsmException } from "shared/models/bsm-exception.model";
import log from "electron-log";
import { OculusService } from "./oculus.service";
@@ -14,6 +13,8 @@ import { DownloadLinkType } from "shared/models/mods";
import sanitize from "sanitize-filename";
import { copyDirectoryWithJunctions, deleteFolder, getFoldersInFolder, pathExist } from "../helpers/fs.helpers";
import { FolderLinkerService } from "./folder-linker.service";
import { ReadStream, createReadStream } from "fs-extra";
import readline from "readline";
export class BSLocalVersionService {
private static instance: BSLocalVersionService;
@@ -43,76 +44,128 @@ export class BSLocalVersionService {
this.linker = FolderLinkerService.getInstance();
}
public async getVersionOfBSFolder(bsPath: string): Promise<PartialBSVersion> {
const versionFilePath = path.join(bsPath, "Beat Saber_Data", "globalgamemanagers");
if (!(await pathExist(versionFilePath))) {
return null;
}
const versionsAvailable = await this.remoteVersionService.getAvailableVersions();
return new Promise<PartialBSVersion>(resolve => {
const stream = createReadStream(versionFilePath);
let findVersion: PartialBSVersion = null;
stream.on("data", line => {
line = line.toString();
for (const version of versionsAvailable) {
if (line.includes(version.BSVersion)) {
findVersion = { BSVersion: version.BSVersion };
if (findVersion.BSVersion !== path.basename(bsPath)) {
findVersion.name = path.basename(bsPath);
}
private async getVersionFromGlobalGameManagerFile(versionFilePath: string): Promise<BSVersion> {
if(!(await pathExist(versionFilePath))){ return null; }
const versionsDict = await this.remoteVersionService.getAvailableVersions();
let stream: ReadStream;
try{
stream = createReadStream(versionFilePath);
const rl = readline.createInterface({
input: stream,
crlfDelay: Infinity
});
for await (const line of rl) {
for (const bsVersion of versionsDict) {
if (line.includes(bsVersion.BSVersion)) {
stream.close();
stream.destroy();
break;
return {...bsVersion};
}
}
});
stream.on("end", () => {
if (findVersion) {
resolve(findVersion);
}
resolve(findVersion);
});
stream.on("close", () => {
if (findVersion) {
resolve(findVersion);
}
resolve(findVersion);
});
}
} catch(e) {
log.error(e);
} finally {
stream?.close();
}
return null;
}
public async getVersionOfBSFolder(
bsPath: string,
options?: {
steam?: boolean;
oculus?: boolean;
}
): Promise<BSVersion>{
const versionFilePath = path.join(bsPath, 'Beat Saber_Data', 'globalgamemanagers');
const folderVersion = await this.getVersionFromGlobalGameManagerFile(versionFilePath);
if(!folderVersion){ return null; }
if(options?.steam || options?.oculus){
return {...folderVersion, ...options};
}
if(folderVersion.BSVersion !== path.basename(bsPath)){
folderVersion.name = path.basename(bsPath);
}
const folderStats = await lstat(bsPath);
if(folderStats.ino){
folderVersion.ino = folderStats.ino;
}
const customVersion = this.getCustomVersions().find(customVersion => {
return customVersion.BSVersion === folderVersion.BSVersion && customVersion.name === folderVersion.name;
});
folderVersion.color = customVersion?.color;
return folderVersion;
}
private setCustomVersions(versions: BSVersion[]): void{
this.configService.set(this.CUSTOM_VERSIONS_KEY, versions);
}
private addCustomVersion(version: BSVersion): void{
this.setCustomVersions([...this.getCustomVersions() ?? [], version]);
}
private getCustomVersions(): BSVersion[]{
return this.configService.get<BSVersion[]>(this.CUSTOM_VERSIONS_KEY) || [];
}
private deleteCustomVersion(version: BSVersion): void{
const customVersions = this.getCustomVersions() || [];
this.setCustomVersions(customVersions.filter(v => (v.name !== version.name || v.BSVersion !== version.BSVersion || v.color !== version.color)));
}
/**
* Return path of a version even if it's not installed.
* @param {BSVersion} version
* @returns {Promise<string>}
*/
public async getVersionPath(version: BSVersion): Promise<string>{
if(version.steam){ return this.steamService.getGameFolder(BS_APP_ID, "Beat Saber") }
if(version.oculus){ return this.oculusService.getGameFolder(OCULUS_BS_DIR); }
return path.join(
this.installLocationService.versionsDirectory,
this.getVersionFolder(version)
);
}
private setCustomVersions(versions: BSVersion[]): void {
this.configService.set(this.CUSTOM_VERSIONS_KEY, versions);
}
/**
* Return path of an installed version. Returns null if not found.
* @param {BSVersion} version
* @returns {Promise<string>}
*/
public async getInstalledVersionPath(version: BSVersion): Promise<string>{
const versionPath = await this.getVersionPath(version);
if(await pathExist(versionPath)){ return versionPath; }
private addCustomVersion(version: BSVersion): void {
this.setCustomVersions([...(this.getCustomVersions() ?? []), version]);
}
const versionFolders = await getFoldersInFolder(this.installLocationService.versionsDirectory);
private getCustomVersions(): BSVersion[] {
return this.configService.get<BSVersion[]>(this.CUSTOM_VERSIONS_KEY) || [];
}
private deleteCustomVersion(version: BSVersion): void {
const customVersions = this.getCustomVersions() || [];
this.setCustomVersions(customVersions.filter(v => v.name !== version.name || v.BSVersion !== version.BSVersion || v.color !== version.color));
}
public async getVersionPath(version: BSVersion): Promise<string> {
if (version.steam) {
return this.steamService.getGameFolder(BS_APP_ID, "Beat Saber");
for(const folder of versionFolders){
const stats = await lstat(folder);
if(stats.ino === version.ino){
return folder;
}
}
if (version.oculus) {
return this.oculusService.getGameFolder(OCULUS_BS_DIR);
}
return path.join(this.installLocationService.versionsDirectory, this.getVersionFolder(version));
return null;
}
private removeSpecialChar(seq: string): string {
return sanitize(seq);
}
public getVersionFolder(version: BSVersion): string {
public getVersionFolder(version: BSVersion): string{
return version.name ?? version.BSVersion;
}
@@ -128,35 +181,22 @@ export class BSLocalVersionService {
private async getSteamVersion(): Promise<BSVersion> {
const steamBsFolder = await this.steamService.getGameFolder(BS_APP_ID, "Beat Saber");
if (!steamBsFolder || !(await pathExist(steamBsFolder))) {
return null;
}
const steamBsVersion = await this.getVersionOfBSFolder(steamBsFolder);
if (!steamBsVersion) {
return null;
}
const version = await this.remoteVersionService.getVersionDetails(steamBsVersion.BSVersion);
if (!version) {
return null;
}
return { ...version, steam: true };
return this.getVersionOfBSFolder(steamBsFolder, { steam: true });
}
private async getOculusVersion(): Promise<BSVersion> {
const oculusBsFolder = await this.oculusService.getGameFolder(OCULUS_BS_DIR);
if (!oculusBsFolder) {
return null;
}
const oculusBsVersion = await this.getVersionOfBSFolder(oculusBsFolder);
if (!oculusBsVersion) {
return null;
}
const version = await this.remoteVersionService.getVersionDetails(oculusBsVersion.BSVersion);
if (!version) {
return null;
}
return { ...version, oculus: true };
return this.getVersionOfBSFolder(oculusBsFolder, { oculus: true });
}
public async getInstalledVersions(): Promise<BSVersion[]> {
@@ -180,103 +220,79 @@ export class BSLocalVersionService {
for (const f of folderInInstallation) {
log.info("try get version from folder", f);
const rawVersion = await this.getVersionOfBSFolder(f);
if (!rawVersion) {
continue;
}
const bsVersion = { ...(await this.remoteVersionService.getVersionDetails(rawVersion.BSVersion)) };
if (!bsVersion) {
continue;
}
bsVersion.name = path.basename(f) !== bsVersion.BSVersion ? path.basename(f) : undefined;
const customVersion = this.getCustomVersions().find(custom => custom.BSVersion === bsVersion.BSVersion && custom.name === bsVersion.name);
const version = await this.getVersionOfBSFolder(f);
if (customVersion) {
bsVersion.color = customVersion.color;
}
if(!version){ continue; }
versions.push(version);
};
versions.push(bsVersion);
}
this.setCustomVersions(versions.filter(v => !!v.color));
return versions;
}
public async deleteVersion(version: BSVersion): Promise<boolean> {
if (version.steam || version.oculus) {
return false;
}
public async deleteVersion(version: BSVersion): Promise<boolean>{
if(version.steam || version.oculus){ return false; }
const versionFolder = await this.getVersionPath(version);
if (!(await pathExist(versionFolder))) {
return true;
}
if(!(await pathExist(versionFolder))){ return true; }
return deleteFolder(versionFolder)
.then(() => {
return true;
})
.catch(() => {
return false;
});
.then(() => { return true; })
.catch(() => { return false; })
}
public async editVersion(version: BSVersion, name: string, color: string): Promise<BSVersion> {
if (version.steam || version.oculus) {
throw { title: "CantEditSteam", message: "CantEditSteam" } as BsmException;
}
const oldPath = await this.getVersionPath(version);
const editedVersion: BSVersion = version.BSVersion === name ? { ...version, name: undefined, color } : { ...version, name: this.removeSpecialChar(name), color };
const newPath = await this.getVersionPath(editedVersion);
public async editVersion(version: BSVersion, name: string, color: string): Promise<BSVersion>{
if(version.steam || version.oculus){ throw {title: "CantEditSteam", message: "CantEditSteam"} as BsmException; }
const oldPath = await this.getVersionPath(version);
const editedVersion: BSVersion = version.BSVersion === name
? {...version, name: undefined, color}
: {...version, name: sanitize(name), color};
const newPath = await this.getVersionPath(editedVersion);
if (oldPath === newPath) {
this.deleteCustomVersion(version);
this.addCustomVersion(editedVersion);
return editedVersion;
}
if(oldPath === newPath){
this.deleteCustomVersion(version);
this.addCustomVersion(editedVersion);
return editedVersion;
}
if ((await pathExist(newPath)) && newPath === oldPath) {
throw { title: "VersionAlreadExist" } as BsmException;
}
if((await pathExist(newPath)) && newPath === oldPath){ throw {title: "VersionAlreadExist"} as BsmException; }
return rename(oldPath, newPath)
.then(() => {
this.deleteCustomVersion(version);
this.addCustomVersion(editedVersion);
return editedVersion;
})
.catch((err: Error) => {
log.error("edit version error", err, version, name, color);
throw { title: "CantRename", ...err } as BsmException;
});
}
return rename(oldPath, newPath).then(() => {
this.deleteCustomVersion(version);
this.addCustomVersion(editedVersion);
return editedVersion;
}).catch((err: Error) => {
log.error("edit version error", err, version, name, color);
throw {title: "CantRename", ...err} as BsmException;
});
}
public async cloneVersion(version: BSVersion, name: string, color: string): Promise<BSVersion> {
const originPath = await this.getVersionPath(version);
const cloneVersion: BSVersion = version.BSVersion === name ? { ...version, name: undefined, color, steam: false, oculus: false } : { ...version, name: this.removeSpecialChar(name), color, steam: false, oculus: false };
const newPath = await this.getVersionPath(cloneVersion);
public async cloneVersion(version: BSVersion, name: string, color: string): Promise<BSVersion>{
const originPath = await this.getVersionPath(version);
const cloneVersion: BSVersion = version.BSVersion === name
? {...version, name: undefined, color, steam: false, oculus: false}
: {...version, name: sanitize(name), color, steam: false, oculus: false};
const newPath = await this.getVersionPath(cloneVersion);
if (originPath === newPath) {
this.deleteCustomVersion(version);
this.addCustomVersion(cloneVersion);
}
if(originPath === newPath){
this.deleteCustomVersion(version);
this.addCustomVersion(cloneVersion);
}
if (await pathExist(newPath)) {
throw { title: "VersionAlreadExist" } as BsmException;
}
if(await pathExist(newPath)){ throw {title: "VersionAlreadExist"} as BsmException; }
return copyDirectoryWithJunctions(originPath, newPath)
.then(() => {
this.addCustomVersion(cloneVersion);
return cloneVersion;
})
.catch((err: Error) => {
log.error("clone version error", err, version, name, color);
throw { title: "CantClone", ...err } as BsmException;
});
}
return copyDirectoryWithJunctions(originPath, newPath).then(() => {
this.addCustomVersion(cloneVersion);
return cloneVersion;
}).catch((err: Error) => {
log.error("clone version error", err, version, name, color);
throw {title: "CantClone", ...err} as BsmException
})
}
public async getLinkedFolders(version: BSVersion): Promise<string[]> {
public async getLinkedFolders(version: BSVersion): Promise<string[]>{
const versionPath = await this.getVersionPath(version);
const [rootFolders, beatSaberDataFolders] = await Promise.all([getFoldersInFolder(versionPath), getFoldersInFolder(path.join(versionPath, "Beat Saber_Data"))]);
+49
View File
@@ -0,0 +1,49 @@
import { Subject, Subscription, filter } from "rxjs";
import { DeepLinkService } from "./deep-link.service";
import { URL } from "url";
import { buildUrl, isValidUrl } from "../../shared/helpers/url.helpers";
export class BsmProtocolService {
private static instance: BsmProtocolService;
public static getInstance(): BsmProtocolService{
if(!BsmProtocolService.instance){ BsmProtocolService.instance = new BsmProtocolService(); }
return BsmProtocolService.instance;
}
private readonly BSM_PROTOCOL = "bsmanager";
private readonly deepLink: DeepLinkService;
private linkeReceived$ = new Subject<URL>();
private constructor(){
this.deepLink = DeepLinkService.getInstance();
this.registerBsmProtocol();
this.deepLink.addLinkOpenedListener(this.BSM_PROTOCOL, link => {
if(!isValidUrl(link)){ return; }
this.linkeReceived$.next(new URL(link));
});
}
private registerBsmProtocol(): boolean{
if(this.deepLink.isDeepLinkRegistred(this.BSM_PROTOCOL)){ return true; }
return this.deepLink.registerDeepLink(this.BSM_PROTOCOL);
}
public on(host: string, listener: (link: URL) => void): Subscription {
return this.linkeReceived$.pipe(filter(link => link.host === host)).subscribe(listener);
}
public buildLink(host: string, params?: Record<string, string|string[]>): URL {
return buildUrl({
protocol: this.BSM_PROTOCOL,
host,
search: params
});
}
}
+3 -2
View File
@@ -66,8 +66,9 @@ export class DeepLinkService {
listeners.splice(fnIndex, 1);
}
public dispatchLinkOpened(link: string) {
log.info("DEISPATCH", link);
public dispatchLinkOpened(link: string){
log.info("DISPATCH", link);
const url = new URL(link);
-13
View File
@@ -1,8 +1,6 @@
import { SystemNotificationOptions } from "shared/models/notification/system-notification.model";
import { UtilsService } from "./utils.service";
import { Notification } from "electron";
import { Notification as NotificationRenderer } from "../../shared/models/notification/notification.model";
import { IpcService } from "./ipc.service";
export class NotificationService {
private static instance: NotificationService;
@@ -17,24 +15,13 @@ export class NotificationService {
private readonly APP_ICON: string;
private readonly utils: UtilsService;
private readonly ipc: IpcService;
private constructor() {
this.utils = UtilsService.getInstance();
this.ipc = IpcService.getInstance();
this.APP_ICON = this.utils.getAssetsPath("favicon.ico");
}
public notify(options: SystemNotificationOptions) {
new Notification({ ...options, icon: this.APP_ICON }).show();
}
// TODO : Make actions work
public notifyRenderer(notification: Omit<NotificationRenderer, "actions">) {
try {
this.ipc.send("show-notification", "index.html", notification);
} catch (e) {
console.error(e);
}
}
}
+48 -56
View File
@@ -5,75 +5,69 @@ import { parse } from "@node-steam/vdf";
import { readFile } from "fs/promises";
import { pathExist } from "../helpers/fs.helpers";
import log from "electron-log";
import psList from "ps-list";
import { app, shell } from "electron";
export class SteamService {
private static instance: SteamService;
private readonly utils: UtilsService = UtilsService.getInstance();
private steamPath: string = "";
private steamPath: string = '';
private constructor() {
private constructor(){
const vbsDirectory = path.join(this.utils.getAssetsScriptsPath(), "node-regedit", "vbs");
regedit.setExternalVBSLocation(vbsDirectory);
}
public static getInstance() {
if (!SteamService.instance) {
SteamService.instance = new SteamService();
}
public static getInstance(){
if(!SteamService.instance){ SteamService.instance = new SteamService(); }
return SteamService.instance;
}
public async getActiveUser(): Promise<number> {
public async getActiveUser(): Promise<number>{
const res = await regedit.promisified.list(["HKCU\\Software\\Valve\\Steam\\ActiveProcess"]);
const keys = res?.["HKCU\\Software\\Valve\\Steam\\ActiveProcess"];
if (!keys?.exists) {
throw 'Key "HKCU\\Software\\Valve\\Steam\\ActiveProcess" not exist';
}
if(!keys?.exists){ throw "Key \"HKCU\\Software\\Valve\\Steam\\ActiveProcess\" not exist"; }
return (keys.values?.ActiveUser.value || undefined) as number;
}
public async steamRunning(): Promise<boolean> {
if (process.platform === "win32") {
return !!(await this.getActiveUser());
}
return psList()
.then(processes => !!processes.find(process => process.cmd.includes("steam")))
.catch(e => {
log.error(e);
throw e;
});
public async steamRunning(): Promise<boolean>{
const steamProcessRunning = await this.utils.taskRunning("steam");
if(process.platform === "linux") { return steamProcessRunning; }
return steamProcessRunning && !!(await this.getActiveUser());
}
public async getSteamPath(): Promise<string> {
if (this.steamPath) {
return this.steamPath;
public async getSteamPath(): Promise<string>{
if(this.steamPath){ return this.steamPath; }
switch (process.platform) {
case "linux":
this.steamPath = path.join(app.getPath('home'), '.steam', "steam");
return this.steamPath;
case "win32":
// eslint-disable-next-line no-case-declarations
const [win32Res, win64Res] = await Promise.all([
regedit.promisified.list(['HKLM\\SOFTWARE\\Valve\\Steam']),
regedit.promisified.list(['HKLM\\SOFTWARE\\WOW6432Node\\Valve\\Steam'])
]);
// eslint-disable-next-line no-case-declarations
const [win32, win64] = [win32Res["HKLM\\SOFTWARE\\Valve\\Steam"], win64Res["HKLM\\SOFTWARE\\WOW6432Node\\Valve\\Steam"]];
if(win64.exists && win64?.values?.InstallPath?.value){
this.steamPath = win64.values.InstallPath.value as string;
}
else if (win32.exists && win32?.values?.InstallPath?.value){
this.steamPath = win32.values.InstallPath.value as string;
}
return this.steamPath;
default:
return null;
}
if (process.platform === "win32") {
const [win32Res, win64Res] = await Promise.all([regedit.promisified.list(["HKLM\\SOFTWARE\\Valve\\Steam"]), regedit.promisified.list(["HKLM\\SOFTWARE\\WOW6432Node\\Valve\\Steam"])]);
const [win32, win64] = [win32Res["HKLM\\SOFTWARE\\Valve\\Steam"], win64Res["HKLM\\SOFTWARE\\WOW6432Node\\Valve\\Steam"]];
let res = "";
if (win64.exists && win64?.values?.InstallPath?.value) {
res = win64.values.InstallPath.value as string;
} else if (win32.exists && win32?.values?.InstallPath?.value) {
res = win32.values.InstallPath.value as string;
}
this.steamPath = res;
return res;
}
if (process.platform === "linux") {
this.steamPath = path.join(app.getPath("home"), ".steam", "steam");
return this.steamPath;
}
return null;
}
public async getGameFolder(gameId: string, gameFolder?: string): Promise<string> {
@@ -82,25 +76,23 @@ export class SteamService {
let libraryFolders: any = path.join(steamPath, "steamapps", "libraryfolders.vdf");
if (!(await pathExist(libraryFolders))) {
return null;
}
if (!(await pathExist(libraryFolders))) { return null; }
libraryFolders = parse(await readFile(libraryFolders, { encoding: "utf-8" }));
if (!libraryFolders.libraryfolders) {
return null;
}
if (!libraryFolders.libraryfolders) { return null; }
libraryFolders = libraryFolders.libraryfolders;
for (const libKey in Object.keys(libraryFolders)) {
if (!libraryFolders[libKey] || !libraryFolders[libKey].apps) {
continue;
}
if (!libraryFolders[libKey] || !libraryFolders[libKey].apps) { continue; }
if (libraryFolders[libKey].apps[gameId] != null) {
return path.join(libraryFolders[libKey].path, "steamapps", "common", gameFolder);
}
}
return null;
} catch (e) {
log.error(e);
return null;
@@ -119,7 +111,7 @@ export class SteamService {
}
clearInterval(interval);
resolve();
}, 3000);
}, 4000);
// If steam is not running after 60 seconds, reject
setTimeout(() => {
+3 -2
View File
@@ -50,8 +50,9 @@ export class UtilsService {
public async taskRunning(task: string): Promise<boolean> {
try {
const processes = await psList();
return !!processes.find(process => process.cmd.includes(task));
} catch (error) {
return processes.some(process => process.name?.includes(task) || process.cmd?.includes(task));
}
catch(error){
log.error(error);
return null;
}
@@ -18,6 +18,7 @@ export class WindowManagerService {
"oneclick-download-map.html": { width: 350, height: 400, minWidth: 350, minHeight: 400, resizable: false },
"oneclick-download-playlist.html": { width: 350, height: 400, minWidth: 350, minHeight: 400, resizable: false },
"oneclick-download-model.html": { width: 350, height: 400, minWidth: 350, minHeight: 400, resizable: false },
"shortcut-launch.html": { width: 600, height: 300, minWidth: 600, minHeight: 300, resizable: false },
};
private readonly baseWindowOption: BrowserWindowConstructorOptions = {
@@ -89,4 +90,15 @@ export class WindowManagerService {
public getAppWindowFromWebContents(sender: Electron.WebContents): AppWindow {
return Array.from(this.windows.entries()).find(([, value]) => value.webContents.id === sender.id)[0];
}
public openWindowOrFocus(window: AppWindow): Promise<void> {
const win = this.getWindow(window);
if (win) {
win.focus();
return Promise.resolve();
}
return this.openWindow(window).then(() => {});
}
}
@@ -0,0 +1,96 @@
import { ModalComponent, ModalExitCode } from "renderer/services/modale.service"
import { BSVersion } from "shared/bs-version.interface"
import { BsmCheckbox } from "renderer/components/shared/bsm-checkbox.component";
import { useTranslation } from "renderer/hooks/use-translation.hook";
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 { 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";
export const CreateLaunchShortcutModal: ModalComponent<LaunchOption, BSVersion> = ({resolver, data}) => {
const bsLauncher = useService(BSLauncherService);
const t = useTranslation();
const color = useThemeColor("second-color");
const [launchOptions, setLaunchOptions] = useState(bsLauncher.getLaunchOptions(data));
const [advanced, setAdvanced] = useState(!!launchOptions.additionalArgs?.length);
const [additionalArgsString, setAdditionalArgsString] = useState(launchOptions.additionalArgs?.join("; ") ?? "");
const completeModal = () => {
if(advanced) {
launchOptions.additionalArgs = additionalArgsString.split(";").map(arg => arg.trim()).filter(arg => arg.length);
} else {
launchOptions.additionalArgs = undefined;
}
resolver({exitCode: ModalExitCode.COMPLETED, data: launchOptions});
}
return (
<form className="text-gray-800 dark:text-gray-200 max-w-lg" onSubmit={e => e.preventDefault()}>
<h1 className="text-3xl uppercase tracking-wide w-full text-center">{t("modals.create-launch-shortcut.title")}</h1>
<p className="my-5">{t("modals.create-launch-shortcut.desc")}</p>
<div className="flex justify-center my-5 gap-3 items-center">
<BsNoteFill className="aspect-square h-16" style={{color: data.color ?? color}}/>
<div className="flex flex-col">
<span className="dark:text-neutral-300 tracking-wide italic">Beat Saber</span>
<span className="font-bold text-3xl">{[data.BSVersion, data.name].join(" ")}</span>
</div>
</div>
<h2 className="font-bold">{t("modals.create-launch-shortcut.launch-options")}</h2>
<div className="mb-1 grid grid-flow-col gap-3 w-full rounded-md py-2 bg-light-main-color-1 dark:bg-main-color-1">
{data.oculus !== true && (
<div className="h-full flex justify-center items-center gap-2">
<BsmCheckbox className="h-5 aspect-square relative z-[1]" checked={launchOptions.oculus} onChange={e => setLaunchOptions({...launchOptions, oculus: e})} />
<Tippy className="!bg-main-color-1" content={t("pages.version-viewer.launch-mods.oculus-description")} delay={[300, 0]} arrow={false}>
<span className="font-bold cursor-help">{t("pages.version-viewer.launch-mods.oculus")}</span>
</Tippy>
</div>
)}
<div className="h-full flex justify-center items-center gap-2">
<BsmCheckbox className="h-5 aspect-square relative z-[1]" checked={launchOptions.desktop} onChange={e => setLaunchOptions({...launchOptions, desktop: e})} />
<Tippy className="!bg-main-color-1" content={t("pages.version-viewer.launch-mods.desktop-description")} delay={[300, 0]} arrow={false}>
<span className="font-bold cursor-help">{t("pages.version-viewer.launch-mods.desktop")}</span>
</Tippy>
</div>
<div className="h-full flex justify-center items-center gap-2">
<BsmCheckbox className="h-5 aspect-square relative z-[1]" checked={launchOptions.debug} onChange={e => setLaunchOptions({...launchOptions, debug: e})} />
<Tippy className="!bg-main-color-1" content={t("pages.version-viewer.launch-mods.debug-description")} delay={[300, 0]} arrow={false}>
<span className="font-bold cursor-help">{t("pages.version-viewer.launch-mods.debug")}</span>
</Tippy>
</div>
</div>
<div className="w-full rounded-md bg-light-main-color-1 dark:bg-main-color-1">
<div className="flex items-center justify-between cursor-pointer pl-3 pr-1 py-1" onClick={() => setAdvanced(prev => !prev)}>
<span className="font-bold">{t("modals.create-launch-shortcut.advanced-launch")}</span>
<ChevronTopIcon className={`h-8 transition-transform ${advanced ? "rotate-180" : ""}`}/>
</div>
<div className={`grid grid-rows-[0fr] transition-[grid-template-rows] ${advanced ? "!grid-rows-[1fr]" : ""}`}>
<div className="overflow-hidden">
<div className="p-2">
<input
type="text"
className="w-full rounded-md text-center outline-none bg-light-main-color-3 dark:bg-main-color-3"
placeholder={t("pages.version-viewer.launch-mods.advanced-launch.placeholder")}
value={additionalArgsString}
onChange={e => setAdditionalArgsString(e.target.value)}
/>
</div>
</div>
</div>
</div>
<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" />
</div>
</form>
)
}
@@ -32,7 +32,12 @@ export function BsVersionItem(props: { version: BSVersion }) {
};
const handleDoubleClick = () => {
launcherService.launch(state, !!configService.get<boolean>(LaunchMods.OCULUS_MOD), !!configService.get<boolean>(LaunchMods.DESKTOP_MOD), !!configService.get<boolean>(LaunchMods.DEBUG_MOD));
launcherService.launch({
version: state,
oculus: !!configService.get<boolean>(LaunchMods.OCULUS_MOD),
desktop: !!configService.get<boolean>(LaunchMods.DESKTOP_MOD),
debug: !!configService.get<boolean>(LaunchMods.DEBUG_MOD),
});
};
const cancel = () => {
@@ -59,8 +59,9 @@ import { ModelTypePlatformIcon } from "./icons/model-type-platform-icon.componen
import { ModelTypeBloqIcon } from "./icons/model-type-bloq-icon.component";
import { ChevronTopIcon } from "./icons/chevron-top-icon.component";
import { EyeCrossIcon } from "./icons/eye-cross-icon.component";
import { ShortcutIcon } from "./icons/shortcut-icon.component";
export type BsmIconType = BsvMapCharacteristic | MSModelType | ("settings" | "trash" | "favorite" | "folder" | "bsNote" | "check" | "three-dots" | "twitch" | "eye" | "play" | "checkCircleIcon" | "discord" | "info" | "eye-cross" | "terminal" | "desktop" | "oculus" | "add" | "cross" | "task" | "github" | "close" | "thumbUpFill" | "timerFill" | "pause" | "twitter" | "sync" | "chevron-top" | "copy" | "steam" | "edit" | "export" | "patreon" | "search" | "bsMapDifficulty" | "link" | "unlink" | "download" | "filter" | "mee6" | "volume-up" | "volume-off" | "volume-down" | "fr-FR-flag" | "es-ES-flag" | "en-US-flag" | "en-EN-flag" | "de-DE-flag");
export type BsmIconType = BsvMapCharacteristic | MSModelType | ("settings" | "trash" | "favorite" | "folder" | "bsNote" | "check" | "three-dots" | "twitch" | "eye" | "play" | "checkCircleIcon" | "discord" | "info" | "eye-cross" | "terminal" | "desktop" | "oculus" | "add" | "cross" | "task" | "github" | "close" | "thumbUpFill" | "timerFill" | "pause" | "twitter" | "sync" | "chevron-top" | "copy" | "steam" | "edit" | "export" | "patreon" | "search" | "bsMapDifficulty" | "link" | "unlink" | "download" | "filter" | "mee6" | "volume-up" | "volume-off" | "volume-down" | "shortcut" | "fr-FR-flag" | "es-ES-flag" | "en-US-flag" | "en-EN-flag" | "de-DE-flag");
export const BsmIcon = memo(({ className, icon, style }: { className?: string; icon: BsmIconType; style?: CSSProperties }) => {
// TODO : Very ugly very messy, need to find a better way to do this
@@ -229,6 +230,10 @@ export const BsmIcon = memo(({ className, icon, style }: { className?: string; i
return <EyeCrossIcon className={className} style={style} />;
}
if (icon === "shortcut") {
return <ShortcutIcon className={className} style={style} />;
}
if (icon === MSModelType.Avatar) {
return <ModelTypeAvatarIcon className={className} style={style} />;
}
@@ -2,7 +2,7 @@ import { CSSProperties } from "react";
export function BsNoteFill(props: { className?: string; style?: CSSProperties }) {
return (
<svg className={props.className} style={props.style} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 406.4 406.4" height="406.4" width="406.4">
<svg className={props.className} style={props.style} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 406.4 406.4">
<g>
<rect rx="69.453" height="406.4" width="406.4" fill="currentColor" fillRule="evenodd" paintOrder="fill markers stroke" />
<path transform="translate(-70 170)" d="M135.467-109.4H406.4v33.867L270.933-7.8 135.467-75.533z" fill="white" />
@@ -0,0 +1,9 @@
import { CSSProperties } from "react";
export function ShortcutIcon(props: { className?: string; style?: CSSProperties }) {
return (
<svg className={props.className} style={props.style} xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor">
<path d="M189.058-113.304q-30.994 0-53.374-22.38-22.38-22.38-22.38-53.374v-581.884q0-31.06 22.38-53.486 22.38-22.427 53.374-22.427h239.065q16 0 26.938 11.066Q466-824.724 466-808.688t-10.939 26.891q-10.938 10.855-26.938 10.855H189.058v581.884h581.884v-239.065q0-16 10.972-26.938Q792.886-466 808.922-466t26.984 10.939q10.949 10.938 10.949 26.938v239.065q0 30.994-22.427 53.374-22.426 22.38-53.486 22.38H189.058Zm581.884-604.168L416.58-362.949q-11.146 11.021-26.425 10.568-15.278-.452-26.3-11.474t-11.022-26.402q0-15.381 11.022-26.402l354.283-354.283H570.543q-16 0-26.939-10.972-10.938-10.972-10.938-27.008t10.938-26.984q10.939-10.949 26.939-10.949h276.312v276.312q0 16-11.066 26.939-11.065 10.938-27.101 10.938t-26.891-10.938q-10.855-10.939-10.855-26.939v-146.929Z"/>
</svg>
)
}
@@ -116,7 +116,7 @@ export default function TitleBar({ template = "index.html" }: { template: AppWin
</header>
);
}
if (template === "oneclick-download-map.html" || template === "oneclick-download-playlist.html" || template === "oneclick-download-model.html") {
if (template === "oneclick-download-map.html" || template === "oneclick-download-playlist.html" || template === "oneclick-download-model.html" || "shortcut-launch.html") {
return (
<header id="titlebar" className="min-h-[22px] bg-transparent w-screen h-[22px] flex content-center items-center justify-start z-10">
<div id="drag-region" className="grow h-full">
@@ -127,9 +127,7 @@ export default function TitleBar({ template = "index.html" }: { template: AppWin
<div id="window-controls" className="h-full flex shrink-0">
<div onClick={closeWindow} className="text-gray-200 cursor-pointer w-7 h-full shrink-0 flex justify-center items-center rounded-bl-md" id="close-button" draggable="false">
<svg aria-hidden="false" width="12" height="12" viewBox="0 0 12 12">
<polygon fill="currentColor" fillRule="evenodd" points="11 1.576 6.583 6 11 10.424 10.424 11 6 6.583 1.576 11 1 10.424 5.417 6 1 1.576 1.576 1 6 5.417 10.424 1">
{" "}
</polygon>
<polygon fill="currentColor" fillRule="evenodd" points="11 1.576 6.583 6 11 10.424 10.424 11 6 6.583 1.576 11 1 10.424 5.417 6 1 1.576 1.576 1 6 5.417 10.424 1"/>
</svg>
</div>
</div>
@@ -24,7 +24,7 @@ export function LaunchSlide({ version }: Props) {
const [advancedLaunch, setAdvancedLaunch] = useState(false);
const [additionalArgsString, setAdditionalArgsString] = useState<string>(configService.get<string>("additionnal-args") || "");
const launchState = useObservable(bsLauncherService.launchState$);
const versionRunning = useObservable(bsLauncherService.versionRunning$);
useEffect(() => {
configService.set("additionnal-args", additionalArgsString);
@@ -50,7 +50,14 @@ export function LaunchSlide({ version }: Props) {
.map(arg => arg.trim())
.filter(arg => arg.length > 0)
: undefined;
bsLauncherService.launch(version, version.oculus ? false : oculusMode, desktopMode, debugMode, additionalArgs);
bsLauncherService.launch({
version,
oculus: version.oculus ? false : oculusMode,
desktop: desktopMode,
debug: debugMode,
additionalArgs
})
};
return (
@@ -78,8 +85,8 @@ export function LaunchSlide({ version }: Props) {
<input className="w-[calc(100%-12px)] h-[calc(100%-12px)] bg-light-main-color-1 dark:bg-main-color-1 text-black dark:text-white rounded-full outline-none text-center" type="text" placeholder={t("pages.version-viewer.launch-mods.advanced-launch.placeholder")} value={additionalArgsString} onChange={handleAdditionalArgsChange} />
</motion.div>
</div>
<div className="grow flex justify-center items-center">
<BsmButton onClick={launch} active={JSON.stringify(version) === JSON.stringify(launchState)} className="relative -translate-y-1/2 text-5xl text-gray-800 dark:text-gray-200 font-bold tracking-wide pt-1 pb-3 px-7 rounded-lg shadow-md italic shadow-black active:scale-90 transition-transform" text="misc.launch" />
<div className='grow flex justify-center items-center'>
<BsmButton onClick={launch} active={JSON.stringify(version) === JSON.stringify(versionRunning)} className='relative -translate-y-1/2 text-5xl text-gray-800 dark:text-gray-200 font-bold tracking-wide pt-1 pb-3 px-7 rounded-lg shadow-md italic shadow-black active:scale-90 transition-transform' text="misc.launch"/>
</div>
</div>
);
+5
View File
@@ -8,6 +8,7 @@ const launcherContainer = document.getElementById("launcher");
const oneclickDownloadMapContainer = document.getElementById("oneclick-download-map");
const oneclickDownloadPlaylistContainer = document.getElementById("oneclick-download-playlist");
const oneclickDownloadModelContainer = document.getElementById("oneclick-download-model");
const shortcutLaunchContainer = document.getElementById("shortcut-launch");
const ipc = IpcService.getInstance();
@@ -31,6 +32,10 @@ if (launcherContainer) {
import("./windows/OneClick/OneClickDownloadModel").then(reactWindow => {
createRoot(oneclickDownloadModelContainer).render(<reactWindow.default />);
});
} else if (shortcutLaunchContainer) {
import("./windows/ShortcutLaunch").then(reactWindow => {
createRoot(shortcutLaunchContainer).render(<reactWindow.default />);
});
} else {
const root = document.getElementById("root");
import("./windows/App").then(reactWindow => {
@@ -16,13 +16,21 @@ import { UninstallModal } from "renderer/components/modal/modal-types/uninstall-
import { MapsPlaylistsPanel } from "renderer/components/maps-mangement-components/maps-playlists-panel.component";
import { ShareFoldersModal } from "renderer/components/modal/modal-types/share-folders-modal.component";
import { ModelsPanel } from "renderer/components/models-management/models-panel.component";
import { useService } from "renderer/hooks/use-service.hook";
import { BSLauncherService } from "renderer/services/bs-launcher.service";
import { CreateLaunchShortcutModal } from "renderer/components/modal/modal-types/create-launch-shortcut-modal.component";
import { lastValueFrom } from "rxjs";
import { NotificationService } from "renderer/services/notification.service";
export function VersionViewer() {
const bsUninstallerService = BSUninstallerService.getInstance();
const bsVersionManagerService = BSVersionManagerService.getInstance();
const modalService = ModalService.getInsance();
const bsDownloaderService = BsDownloaderService.getInstance();
const ipcService = IpcService.getInstance();
const bsLauncher = useService(BSLauncherService);
const notification = useService(NotificationService);
const { state } = useLocation() as { state: BSVersion };
const navigate = useNavigate();
@@ -74,6 +82,23 @@ export function VersionViewer() {
modalService.openModal(ShareFoldersModal, state);
};
const createLaunchShortcut = async () => {
const { exitCode, data } = await modalService.openModal(CreateLaunchShortcutModal, state);
if(exitCode !== ModalExitCode.COMPLETED){ return; }
lastValueFrom(bsLauncher.createLaunchShortcut(data)).then(() => {
notification.notifySuccess({
title: "notifications.create-launch-shortcut.success.title",
desc: "notifications.create-launch-shortcut.success.msg"
});
}).catch(() => {
notification.notifyError({
title: "notifications.types.error",
desc: "notifications.create-launch-shortcut.error.msg"
});
});
}
return (
<>
<BsmImage className="absolute w-full h-full top-0 left-0 object-cover" image={state.ReleaseImg || DefautVersionImage} errorImage={DefautVersionImage} />
@@ -90,7 +115,14 @@ export function VersionViewer() {
<ModsSlide version={state} onDisclamerDecline={handleModsDisclaimerDecline} />
</div>
</div>
<BsmDropdownButton className="absolute top-3 right-4 h-9 w-9 bg-light-main-color-2 dark:bg-main-color-2 rounded-md" items={[{ text: "pages.version-viewer.dropdown.open-folder", icon: "folder", onClick: openFolder }, !state.steam && !state.oculus && { text: "pages.version-viewer.dropdown.verify-files", icon: "task", onClick: verifyFiles }, !state.steam && !state.oculus && { text: "pages.version-viewer.dropdown.edit", icon: "edit", onClick: edit }, !state.oculus && { text: "pages.version-viewer.dropdown.clone", icon: "copy", onClick: clone }, { text: "pages.version-viewer.dropdown.shared-folders", icon: "link", onClick: openShareFolderModal }, !state.steam && !state.oculus && { text: "pages.version-viewer.dropdown.uninstall", icon: "trash", onClick: uninstall }]} />
<BsmDropdownButton className="absolute top-3 right-4 h-9 w-9 bg-light-main-color-2 dark:bg-main-color-2 rounded-md" items={[
{ text: "pages.version-viewer.dropdown.open-folder", icon: "folder", onClick: openFolder },
!state.steam && !state.oculus && { text: "pages.version-viewer.dropdown.verify-files", icon: "task", onClick: verifyFiles },
!state.steam && !state.oculus && { text: "pages.version-viewer.dropdown.edit", icon: "edit", onClick: edit },
!state.oculus && { text: "pages.version-viewer.dropdown.clone", icon: "copy", onClick: clone },
{ text: "pages.version-viewer.dropdown.shared-folders", icon: "link", onClick: openShareFolderModal },
{ text: "pages.version-viewer.dropdown.create-shortcut", icon: "shortcut", onClick: createLaunchShortcut },
!state.steam && !state.oculus && { text: "pages.version-viewer.dropdown.uninstall", icon: "trash", onClick: uninstall }]} />
</>
);
}
@@ -136,7 +136,7 @@ export class BsDownloaderService {
}
public async download(bsVersion: BSVersion, isVerification?: boolean, isFirstCall = true): Promise<IpcResponse<DownloadEvent>> {
// TODO : to remake cause we don't need recursion anymore
// TODO : to remake cause we don't need recursion anymore (will be rework with qr code)
if (isFirstCall && !this.progressBarService.require()) {
return { success: false };
+56 -57
View File
@@ -1,79 +1,78 @@
import { LauchOption, LaunchResult } from "shared/models/bs-launch";
import { BSVersion } from "shared/bs-version.interface";
import { LaunchOption, BSLaunchEvent, BSLaunchWarning, BSLaunchEventData, BSLaunchErrorData, BSLaunchError } from "shared/models/bs-launch";
import { BSVersion } from 'shared/bs-version.interface';
import { IpcService } from "./ipc.service";
import { NotificationService } from "./notification.service";
import { BsDownloaderService } from "./bs-downloader.service";
import { BehaviorSubject } from "rxjs";
import { NotificationResult } from "shared/models/notification/notification.model";
import { BehaviorSubject, Observable, filter } from "rxjs";
import { ConfigurationService } from "./configuration.service";
import { ThemeService } from "./theme.service";
export class BSLauncherService {
private static instance: BSLauncherService;
private readonly ipcService: IpcService;
private readonly notificationService: NotificationService;
private readonly bsDownloaderService: BsDownloaderService;
private readonly config: ConfigurationService;
private readonly theme: ThemeService;
public readonly launchState$: BehaviorSubject<BSVersion> = new BehaviorSubject(null);
public static getInstance() {
if (!BSLauncherService.instance) {
BSLauncherService.instance = new BSLauncherService();
}
public readonly versionRunning$: BehaviorSubject<BSVersion> = new BehaviorSubject(null);
public static getInstance(){
if(!BSLauncherService.instance){ BSLauncherService.instance = new BSLauncherService(); }
return BSLauncherService.instance;
}
private constructor() {
this.ipcService = IpcService.getInstance();
this.notificationService = NotificationService.getInstance();
this.bsDownloaderService = BsDownloaderService.getInstance();
this.listenBsExit();
this.config = ConfigurationService.getInstance();
this.theme = ThemeService.getInstance();
}
private listenBsExit(): void {
this.ipcService.watch("bs-launch.exit").subscribe(res => {
const version = this.launchState$.value;
this.launchState$.next(null);
if (res.success) {
return;
}
this.notificationService.notifyError({ title: "notifications.bs-launch.errors.titles.EXIT", desc: "notifications.bs-launch.errors.msg.EXIT", actions: [{ id: "0", title: "misc.verify" }] }).then(res => {
if (res === "0") {
this.bsDownloaderService.download(version, true);
}
});
});
}
// TODO : Rework with shortcuts implementation
public launch(version: BSVersion, oculus: boolean, desktop: boolean, debug: boolean, additionalArgs?: string[]): Promise<NotificationResult | string> {
const lauchOption: LauchOption = { debug, oculus, desktop, version, additionalArgs };
if (this.launchState$.value) {
return this.notificationService.notifyError({ title: "notifications.bs-launch.errors.titles.BS_ALREADY_RUNNING" });
public getLaunchOptions(version: BSVersion): LaunchOption{
return {
version,
oculus: this.config.get(LaunchMods.OCULUS_MOD),
desktop: this.config.get(LaunchMods.DESKTOP_MOD),
debug: this.config.get(LaunchMods.DEBUG_MOD),
additionalArgs: (this.config.get<string>("additionnal-args") || "").split(";").map(arg => arg.trim()).filter(arg => arg.length > 0)
}
this.launchState$.next(version);
return this.ipcService.send<LaunchResult>("bs-launch.launch", { args: lauchOption }).then(res => {
if (res.data === "LAUNCHED") {
return this.notificationService.notifySuccess({ title: "notifications.bs-launch.success.titles.launching" });
}
this.launchState$.next(null);
if (!res.success) {
return this.notificationService.notifyError({ title: "notifications.bs-launch.errors.titles.UNABLE_TO_LAUNCH", desc: res.error.title });
}
if (res.data === "EXE_NOT_FINDED") {
return this.notificationService.notifyError({ title: "notifications.bs-launch.errors.titles.EXE_NOT_FINDED", desc: "notifications.bs-launch.errors.msg.EXE_NOT_FINDED", actions: [{ id: "0", title: "misc.verify" }] }).then(res => {
if (res === "0") {
this.bsDownloaderService.download(version, true);
}
return res;
});
}
if (res.data) {
return this.notificationService.notifyError({ title: `notifications.bs-launch.errors.titles.${res.data}` });
}
return this.notificationService.notifyError({ title: res.data || res.error.title });
});
}
public doLaunch(launchOptions: LaunchOption): Observable<BSLaunchEventData>{
return this.ipcService.sendV2<BSLaunchEventData, LaunchOption>("bs-launch.launch", {args: launchOptions});
}
public launch(launchOptions: LaunchOption): Observable<BSLaunchEventData> {
const launchState$ = this.doLaunch(launchOptions);
this.versionRunning$.next(launchOptions.version);
launchState$.pipe(filter(event => {
const eventToFilter = [...Object.values(BSLaunchWarning), BSLaunchEvent.STEAM_LAUNCHED]
return !eventToFilter.includes(event.type);
})).subscribe({
next: event => {
this.notificationService.notifySuccess({title: `notifications.bs-launch.success.titles.${event.type}`, desc: `notifications.bs-launch.success.msg.${event.type}`});
},
error: (err: BSLaunchErrorData) => {
if(!Object.values(BSLaunchError).includes(err.type)){
this.notificationService.notifyError({title: "notifications.bs-launch.errors.titles.UNKNOWN_ERROR", desc: "notifications.bs-launch.errors.msg.UNKNOWN_ERROR"});
} else {
this.notificationService.notifyError({title: `notifications.bs-launch.errors.titles.${err.type}`, desc: `notifications.bs-launch.errors.msg.${err.type}`})
}
}
}).add(() => {
this.versionRunning$.next(null);
});
return launchState$;
}
public createLaunchShortcut(launchOptions: LaunchOption): Observable<void>{
const options: LaunchOption = {...launchOptions, version: {...launchOptions.version, color: launchOptions.version.color || this.theme.getBsmColors()[1]}};
return this.ipcService.sendV2<void, LaunchOption>("create-launch-shortcut", {args: options});
}
}
export enum LaunchMods {
@@ -20,11 +20,6 @@ export class NotificationService {
private constructor() {
this.ipc = IpcService.getInstance();
this.notifications$ = new BehaviorSubject<ResolvableNotification[]>([]);
// TODO : Make actions work and adapt with "watch" remork
this.ipc.watch<Notification>("show-notification").subscribe(notification => {
this.notify(notification as unknown as Notification);
});
}
public notify(notification: Notification): Promise<NotificationResult | string> {
+6 -2
View File
@@ -1,5 +1,5 @@
import { DefaultConfigKey, ThemeConfig } from "renderer/config/default-configuration.config";
import { BehaviorSubject } from "rxjs";
import { Observable } from "rxjs";
import { ConfigurationService } from "./configuration.service";
export class ThemeService {
@@ -7,7 +7,7 @@ export class ThemeService {
private readonly configService: ConfigurationService;
public readonly theme$: BehaviorSubject<ThemeConfig>;
public readonly theme$: Observable<ThemeConfig>;
public static getInstance(): ThemeService {
if (!ThemeService.instance) {
@@ -29,6 +29,10 @@ export class ThemeService {
return this.configService.get("theme" as DefaultConfigKey);
}
public getBsmColors(): [string, string]{
return [this.configService.get("first-color" as DefaultConfigKey), this.configService.get("second-color" as DefaultConfigKey)];
}
public get isLight() {
return this.configService.get("theme" as DefaultConfigKey) === ("light" as ThemeConfig);
}
@@ -1,5 +1,6 @@
import { AppWindow } from "shared/models/window-manager/app-window.model";
import { IpcService } from "./ipc.service";
import { lastValueFrom } from "rxjs";
export class WindowManagerService {
private static instance: WindowManagerService;
@@ -28,4 +29,9 @@ export class WindowManagerService {
public close(...win: AppWindow[]) {
this.ipcService.sendLazy<AppWindow[]>("close-windows", { args: win });
}
public openWindowOrFocus(window: AppWindow): Promise<void> {
return lastValueFrom(this.ipcService.sendV2<void, AppWindow>("open-window-or-focus", { args: window }));
}
}
+14
View File
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://use.typekit.net/okr8ven.css">
<meta charset="utf-8" />
<meta
http-equiv="Content-Security-Policy"
content="script-src 'self' 'unsafe-inline'"
/>
</head>
<body>
<div id="shortcut-launch"></div>
</body>
</html>
+115
View File
@@ -0,0 +1,115 @@
import TitleBar from "renderer/components/title-bar/title-bar.component";
import { useService } from "renderer/hooks/use-service.hook";
import { ThemeService } from "renderer/services/theme.service";
import { useEffect, useState } from "react"
import { WindowManagerService } from "renderer/services/window-manager.service";
import { IpcService } from "renderer/services/ipc.service";
import { take } from "rxjs";
import { BSLauncherService } from "renderer/services/bs-launcher.service";
import { BsmImage } from "renderer/components/shared/bsm-image.component";
import defaultImage from "../../../assets/images/default-version-img.jpg";
import { useObservable } from "renderer/hooks/use-observable.hook";
import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
import { BsNoteFill } from "renderer/components/svgs/icons/bs-note-fill.component";
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { motion } from "framer-motion"
import { BSLaunchError, BSLaunchEventType, LaunchOption } from "shared/models/bs-launch";
import { NotificationService } from "renderer/services/notification.service";
import { useTranslation } from "renderer/hooks/use-translation.hook";
export default function ShortcutLaunch() {
const themeService = useService(ThemeService);
const windows = useService(WindowManagerService);
const ipc = useService(IpcService);
const bsLauncher = useService(BSLauncherService);
const notification = useService(NotificationService);
const t = useTranslation();
const color = useThemeColor("second-color");
const launchOptions = useObservable(ipc.sendV2<LaunchOption>("shortcut-launch-options").pipe(take(1)), null)
const [rotation, setRotation] = useState(0);
const [status, setStatus] = useState<BSLaunchEventType>();
useEffect(() => {
const sub = themeService.theme$.subscribe(() => {
if (themeService.isDark || (themeService.isOS && window.matchMedia("(prefers-color-scheme: dark)").matches)) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
});
const interval = setInterval(() => {
setRotation(rotation => rotation + 45 * 3);
}, 1500);
return () => {
sub.unsubscribe();
clearInterval(interval);
}
}, []);
useOnUpdate(() => {
if(!launchOptions) { return; }
launchOptions.skipAlreadyRunning = true;
const sub = bsLauncher.doLaunch(launchOptions).subscribe({
next: event => {
setStatus(event.type);
},
error: err => {
if(!Object.values(BSLaunchError).includes(err.type)){
notification.notifySystem({title: t("notifications.bs-launch.errors.titles.UNKNOWN_ERROR"), body: t("notifications.bs-launch.errors.msg.UNKNOWN_ERROR")});
} else {
notification.notifySystem({title: t(`notifications.bs-launch.errors.titles.${err.type}`), body: t(`notifications.bs-launch.errors.msg.${err.type}`)})
}
}
});
sub.add(() => {
windows.close("shortcut-launch.html");
});
return () => sub.unsubscribe();
}, [launchOptions]);
return (
<div className="relative w-screen h-screen overflow-hidden ">
<BsmImage className="absolute top-0 left-0 w-full h-full object-cover" placeholder={defaultImage} image={launchOptions?.version?.ReleaseImg ?? defaultImage}/>
<div className="w-full h-full backdrop-blur-lg flex flex-col">
<TitleBar template="shortcut-launch.html"/>
<div className="grow px-6 pb-6 pt-3 flex gap-6">
<div className="h-full w-40 relative flex items-center justify-center">
<BsmImage className="absolute top-0 left-0 w-full h-full object-cover shadow-black shadow-center" placeholder={defaultImage} image={launchOptions?.version?.ReleaseImg ?? defaultImage}/>
<motion.div className="shadow-black shadow-[0px_0px_11px_5px] rounded-2xl aspect-square w-20 z-[1]" animate={{
rotate: rotation,
transition: {
type: "spring",
}
}}>
<BsNoteFill className="w-full h-full" style={{color: launchOptions?.version?.color ?? color}}/>
</motion.div>
</div>
<div className="grow flex flex-col py-3">
<div className="w-full h-full bg-main-color-2 rounded-md shadow-black shadow-md p-3 flex flex-col gap-3">
<h1 className="text-neutral-300 tracking-wide italic">{t("bs-shortcut-launch.beat-saber-launching")}</h1>
<h2 className="text-light-main-color-2 font-bold text-3xl">{[launchOptions?.version?.BSVersion, launchOptions?.version?.name].join(" ")}</h2>
<div className="flex flex-col grow justify-center text-neutral-400">
<span className="uppercase text-neutral-300 tracking-wide">{t("bs-shortcut-launch.launching")}</span>
<span className="italic text-neutral-400 leading-4 text-sm font-bold">{(
status ? t(`bs-shortcut-launch.status-text.success.${status}`) : t("bs-shortcut-launch.status-text.init")
)}
</span>
</div>
<BsmButton className="shrink-0 h-10 rounded-md flex items-center justify-center" text="bs-shortcut-launch.open-bsmanager" typeColor="cancel" withBar={false} onClick={() => windows.openWindowOrFocus("index.html")}/>
</div>
</div>
</div>
</div>
</div>
)
}
+3 -2
View File
@@ -1,6 +1,7 @@
export interface PartialBSVersion {
BSVersion: string;
name?: string;
BSVersion: string,
name?: string
ino?: number,
}
export interface BSVersion extends PartialBSVersion {
+13
View File
@@ -0,0 +1,13 @@
export function objectFromEntries<T>(entries: Iterable<readonly [PropertyKey, T]>): Record<PropertyKey, T | T[]> {
const temp: Record<PropertyKey, T | T[]> = {};
for (const [key, value] of entries) {
if (temp[key]) {
temp[key] = Array.isArray(temp[key]) ? [...(temp[key] as T[]), value] : [(temp[key] as T), value];
} else {
temp[key] = value;
}
}
return temp;
}
+27
View File
@@ -6,3 +6,30 @@ export function isValidUrl(url: string): boolean {
return false;
}
}
export function buildUrl({
protocol = "https",
host = "about:blank",
path = "",
search = {},
hash = ""
}: {
protocol?: string,
host?: string,
path?: string,
search?: Record<string, string | string[]>,
hash?: string
}): URL {
const url = new URL(`${protocol}://${host}${path}`);
url.hash = hash;
for (const [key, value] of Object.entries(search)) {
if (Array.isArray(value)) {
value.forEach(v => url.searchParams.append(key, v));
} else {
url.searchParams.append(key, value);
}
}
return url;
}
+3 -2
View File
@@ -1,2 +1,3 @@
export { LauchOption } from "./launch-option.interface";
export { LaunchResult } from "./launch-result.interface";
export { LaunchOption } from "./launch-option.interface"
export { LaunchResult } from "./launch-result.interface"
export { BSLaunchError, BSLaunchErrorData, BSLaunchEvent, BSLaunchEventData, BSLaunchEventType, BSLaunchWarning } from "./launch-event.model"
@@ -0,0 +1,29 @@
export interface BSLaunchEventData{
type: BSLaunchEventType;
data?: unknown;
}
export interface BSLaunchErrorData{
type: BSLaunchError;
data?: unknown;
}
export enum BSLaunchError{
BS_NOT_FOUND = "EXE_NOT_FINDED",
BS_ALREADY_RUNNING = "BS_ALREADY_RUNNING",
OCULUS_NOT_RUNNING = "OCULUS_NOT_RUNNING",
BS_EXIT_ERROR = "EXIT",
UNKNOWN_ERROR = "UNKNOWN_ERROR",
}
export enum BSLaunchEvent{
STEAM_LAUNCHING = "STEAM_LAUNCHING",
STEAM_LAUNCHED = "STEAM_LAUNCHED",
BS_LAUNCHING = "BS_LAUNCHING",
}
export enum BSLaunchWarning{
UNABLE_TO_LAUNCH_STEAM = "UNABLE_TO_LAUNCH_STEAM",
}
export type BSLaunchEventType = BSLaunchEvent | BSLaunchWarning;
@@ -1,9 +1,10 @@
import { BSVersion } from "shared/bs-version.interface";
export interface LauchOption {
version: BSVersion;
oculus: boolean;
desktop: boolean;
debug: boolean;
additionalArgs?: string[];
export interface LaunchOption {
version: BSVersion,
oculus?: boolean,
desktop?: boolean,
debug?: boolean,
additionalArgs?: string[],
skipAlreadyRunning?: boolean
}
@@ -1 +1 @@
export type AppWindow = "index.html" | "launcher.html" | "oneclick-download-map.html" | "oneclick-download-playlist.html" | "oneclick-download-model.html";
export type AppWindow = "index.html" | "launcher.html" | "oneclick-download-map.html" | "oneclick-download-playlist.html" | "oneclick-download-model.html" | "shortcut-launch.html";