diff --git a/assets/jsons/translations/de.json b/assets/jsons/translations/de.json index be559f32..a0a87635 100644 --- a/assets/jsons/translations/de.json +++ b/assets/jsons/translations/de.json @@ -97,6 +97,7 @@ "clone": "Klonen", "edit": "Bearbeiten", "uninstall": "Entfernen", + "create-shortcut": "Verknüpfung erstellen", "shared-folders": "Geteilte Ordner" } }, @@ -451,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": { @@ -611,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": { diff --git a/assets/jsons/translations/en.json b/assets/jsons/translations/en.json index db781be8..49481eb4 100644 --- a/assets/jsons/translations/en.json +++ b/assets/jsons/translations/en.json @@ -97,6 +97,7 @@ "clone": "Clone", "edit": "Edit", "uninstall": "Uninstall", + "create-shortcut": "Create a shortcut", "shared-folders": "Shared Folders" } }, @@ -451,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": { @@ -611,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": { diff --git a/assets/jsons/translations/es.json b/assets/jsons/translations/es.json index 4c59ad74..2aea8d24 100644 --- a/assets/jsons/translations/es.json +++ b/assets/jsons/translations/es.json @@ -96,6 +96,7 @@ "clone": "Clonar", "edit": "Editar", "uninstall": "Desinstalar", + "create-shortcut": "Crear un atajo", "shared-folders": "Carpetas Compartidas" } }, @@ -450,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": { @@ -610,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": { diff --git a/assets/jsons/translations/fr.json b/assets/jsons/translations/fr.json index c2ed1461..5e8100fd 100644 --- a/assets/jsons/translations/fr.json +++ b/assets/jsons/translations/fr.json @@ -96,6 +96,7 @@ "clone": "Cloner", "edit": "Editer", "uninstall": "Désinstaller", + "create-shortcut": "Créer un raccourci", "shared-folders": "Dossiers Partagés" } }, @@ -450,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": { @@ -610,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": { diff --git a/src/main/constants.ts b/src/main/constants.ts index 83b22a7c..2392c70b 100644 --- a/src/main/constants.ts +++ b/src/main/constants.ts @@ -10,4 +10,4 @@ export const APP_NAME = "BSManager"; export const STEAMVR_APP_ID = "250820"; export const IMAGE_CACHE_FOLDER = "imagescache"; -export const IMAGE_CACHE_PATH = path.join(path.dirname(app.getPath("exe")), IMAGE_CACHE_FOLDER); +export const IMAGE_CACHE_PATH = path.join(app.getPath("userData"), IMAGE_CACHE_FOLDER); diff --git a/src/main/ipcs/bs-launcher-ipcs.ts b/src/main/ipcs/bs-launcher-ipcs.ts index c9225649..1a272778 100644 --- a/src/main/ipcs/bs-launcher-ipcs.ts +++ b/src/main/ipcs/bs-launcher-ipcs.ts @@ -1,12 +1,17 @@ import { LaunchOption } from "shared/models/bs-launch"; import { BSLauncherService } from "../services/bs-launcher.service" -import { IpcRequest } from 'shared/models/ipc'; import { IpcService } from '../services/ipc.service'; +import { from } from "rxjs"; const ipc = IpcService.getInstance(); -ipc.on('bs-launch.launch', async (req: IpcRequest, reply) => { +ipc.on('bs-launch.launch', (req, reply) => { const bsLauncher = BSLauncherService.getInstance(); reply(bsLauncher.launch(req.args)); }); + +ipc.on("create-launch-shortcut", (req, reply) => { + const bsLauncher = BSLauncherService.getInstance(); + reply(from(bsLauncher.createLaunchShortcut(req.args))); +}); diff --git a/src/main/main.ts b/src/main/main.ts index 761f8a0c..ea405e39 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -87,12 +87,6 @@ if (!gotTheLock) { app.setAppUserModelId(APP_NAME); initServicesMustBeInitialized(); - - // TODO : to remove - setTimeout(() => { - // DeepLinkService.getInstance().dispatchLinkOpened("bsmanager://launch?version=1.29.1&versionName=Hi+Twitter&versionIno=2533274792493431&oculusMode=true&desktopMode=true&additionalArgs=--nowait&additionalArgs=-omgargs"); - }, 3000); - const deepLink = process.argv.find(arg => DeepLinkService.getInstance().isDeepLink(arg)); @@ -113,25 +107,6 @@ if (!gotTheLock) { ipcMain.on("log-error", (event, args: IpcRequest) => { log.error(args?.args); }); - - // TODO : remove this - BSLauncherService.getInstance().createLaunchShortcut({ - debug: false, - oculus: true, - desktop: true, - version: { - BSVersion: '1.29.1', - BSManifest: '886973241045584398', - ReleaseURL: 'https://steamcommunity.com/games/620980/announcements/detail/6169409105101272202', - ReleaseImg: 'https://cdn.akamai.steamstatic.com/steamcommunity/public/images/clans/32055887/c328e407367e9914abaf92f609501877ee5abb63.png', - ReleaseDate: '1680623885', - year: '2023', - name: 'Hi Twitter', - ino: 2533274792493431, - color: '#6545ff' - }, - additionalArgs: [ '--nowait', '-omgargs' ] - }); }).catch(log.error); } diff --git a/src/main/services/bs-launcher.service.ts b/src/main/services/bs-launcher.service.ts index cea0f096..7724418f 100644 --- a/src/main/services/bs-launcher.service.ts +++ b/src/main/services/bs-launcher.service.ts @@ -11,7 +11,7 @@ import { rename } from "fs/promises"; import log from "electron-log"; import { Observable, lastValueFrom, of, timer } from "rxjs"; import { BsmProtocolService } from "./bsm-protocol.service"; -import { app} from "electron"; +import { app, shell} from "electron"; import { Resvg } from "@resvg/resvg-js"; import Color from "color"; import { ensureDir, writeFile } from "fs-extra"; @@ -20,13 +20,13 @@ 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 bsmProtocolService: BsmProtocolService; private readonly windows: WindowManagerService; @@ -43,7 +43,6 @@ export class BSLauncherService { private constructor() { this.utilsService = UtilsService.getInstance(); this.steamService = SteamService.getInstance(); - this.oculusService = OculusService.getInstance(); this.localVersionService = BSLocalVersionService.getInstance(); this.bsmProtocolService = BsmProtocolService.getInstance(); this.windows = WindowManagerService.getInstance(); @@ -82,14 +81,12 @@ export class BSLauncherService { return this.utilsService.taskRunning(BS_EXECUTABLE); } - private buildBsLaunchArgs(launchOptions: LaunchOption){ + private buildBsLaunchArgs(launchOptions: LaunchOption): string[]{ const 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"); @@ -106,14 +103,13 @@ export class BSLauncherService { private launchBSProcess(bsExePath: string, args: string[], debug = false): ChildProcessWithoutNullStreams{ - const spawnOptions: SpawnOptionsWithoutStdio = { shell: true, cwd: path.dirname(bsExePath), env: {...process.env, "SteamAppId": BS_APP_ID} }; + const spawnOptions: SpawnOptionsWithoutStdio = { detached: true, cwd: path.dirname(bsExePath), env: {...process.env, "SteamAppId": BS_APP_ID} }; if(debug){ - spawnOptions.detached = true; spawnOptions.windowsVerbatimArguments = true; } - return spawn(`\"${bsExePath}\"`, args, spawnOptions); + return spawn(bsExePath, args, spawnOptions); } @@ -125,10 +121,6 @@ export class BSLauncherService { return obs.error({type: BSLaunchError.BS_ALREADY_RUNNING} as BSLaunchErrorData); } - if(launchOptions.version.oculus && (await this.oculusService.oculusRunning())){ - return obs.error({type: BSLaunchError.OCULUS_NOT_RUNNING} as BSLaunchErrorData); - } - const bsFolderPath = await this.localVersionService.getInstalledVersionPath(launchOptions.version); const exePath = path.join(bsFolderPath, BS_EXECUTABLE); @@ -222,13 +214,8 @@ export class BSLauncherService { return res; } - /** - * Create .ico file for the shortcut with the given color - * @param {Color} color - * @returns {Promise} Path of the icon - */ - private async createShortcutIco(color: Color): Promise{ - + private createShortcutPngBuffer(color: Color): Buffer{ + const svgIcon = ` @@ -236,10 +223,36 @@ export class BSLauncherService { `; - const pngBuffer = new Resvg(svgIcon, { + 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} Path of the icon + */ + private async createShortcutPng(color: Color): Promise{ + 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} Path of the icon + */ + private async createShortcutIco(color: Color): Promise{ + const pngBuffer = this.createShortcutPngBuffer(color); const icoBuffer = await toIco([pngBuffer]); await ensureDir(IMAGE_CACHE_PATH); @@ -251,36 +264,42 @@ export class BSLauncherService { return iconPath; } - private createInternetShortcut(options: { url: string, output: string, iconFile?: string, iconIndex?: number }): Promise{ - - const data = [ - "[InternetShortcut]", - `URL=${options.url}`, - options.iconFile ? `IconFile=${options.iconFile}` : null, - `IconIndex=${options.iconIndex ?? 0}` - ].join("\r\n"); - - return writeFile(options.output, data); - } - - public async createLaunchShortcut(launchOptions: LaunchOption): Promise{ + public async createLaunchShortcut(launchOptions: LaunchOption): Promise{ const shortcutParams = this.launchOptionToShortcutParams(launchOptions); - const shortcutUrl = this.bsmProtocolService.buildLink("launch", shortcutParams); + const shortcutUrl = this.bsmProtocolService.buildLink("launch", shortcutParams).toString(); - const shortcutName = ["Beat Saber", launchOptions.version.BSVersion, launchOptions.version.name].join(" ") + const shortcutName = ["Beat Saber", launchOptions.version.BSVersion, launchOptions.version.name].join(" "); + const shortcutIconColor = new Color(launchOptions.version.color, "hex"); - return this.createInternetShortcut({ - output: path.join(app.getPath("desktop"), `${shortcutName}.url`), - url: shortcutUrl.toString(), - iconFile: await this.createShortcutIco(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{ const launchOption = this.shortcutParamsToLaunchOption(launchOptions); - launchOption.version = await this.localVersionService.getVersionOfBSFolder(await this.localVersionService.getInstalledVersionPath(launchOption.version)); + const bsPath = await this.localVersionService.getInstalledVersionPath(launchOption.version); + + launchOption.version = await this.localVersionService.getVersionOfBSFolder(bsPath, { + steam: launchOption.version.steam, + oculus: launchOption.version.oculus, + }); launchOption.version = {...(await this.remoteVersion.getVersionDetails(launchOption.version.BSVersion)), ...launchOption.version}; @@ -304,3 +323,27 @@ type ShortcutParams = { 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 { + 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); +} diff --git a/src/main/services/bs-local-version.service.ts b/src/main/services/bs-local-version.service.ts index 2b96da09..6d13ab08 100644 --- a/src/main/services/bs-local-version.service.ts +++ b/src/main/services/bs-local-version.service.ts @@ -4,7 +4,6 @@ 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 { lstat, rename } from "fs/promises"; import { BsmException } from "shared/models/bsm-exception.model"; @@ -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,41 +44,55 @@ export class BSLocalVersionService { this.linker = FolderLinkerService.getInstance(); } - + private async getVersionFromGlobalGameManagerFile(versionFilePath: string): Promise { - public async getVersionOfBSFolder(bsPath: string): Promise{ - const versionFilePath = path.join(bsPath, 'Beat Saber_Data', 'globalgamemanagers'); - if(!(await pathExist(versionFilePath))){ return null; } - + const versionsDict = await this.remoteVersionService.getAvailableVersions(); - - const folderVersion = await new Promise(async (resolve, reject) => { - const stream = createReadStream(versionFilePath); - stream.on('data', line => { - - line = line.toString(); + let stream: ReadStream; - for(const bsVersion of versionsDict){ - - if(!line.includes(bsVersion.BSVersion)){ continue; } - - resolve({...bsVersion}); - - stream.close(); - stream.destroy(); - - return; - } + try{ + stream = createReadStream(versionFilePath); + const rl = readline.createInterface({ + input: stream, + crlfDelay: Infinity }); - stream.on("close", () => { resolve(null); }); - stream.on("error", e => { log.error(e); reject(e); }); - }); + for await (const line of rl) { + for (const bsVersion of versionsDict) { + if (line.includes(bsVersion.BSVersion)) { + stream.close(); + return {...bsVersion}; + } + } + } + + } catch(e) { + log.error(e); + } finally { + stream?.close(); + } + + return null; + } + + public async getVersionOfBSFolder( + bsPath: string, + options?: { + steam?: boolean; + oculus?: boolean; + } + ): Promise{ + 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); } @@ -150,10 +165,6 @@ export class BSLocalVersionService { return null; } - private removeSpecialChar(seq: string): string{ - return sanitize(seq); - } - public getVersionFolder(version: BSVersion): string{ return version.name ?? version.BSVersion; } @@ -175,16 +186,7 @@ export class BSLocalVersionService { return null; } - const steamBsVersion = await this.getVersionOfBSFolder(steamBsFolder); - - if(!steamBsVersion){ - return null; - } - - steamBsVersion.name = undefined; - steamBsVersion.steam = true; - - return steamBsVersion; + return this.getVersionOfBSFolder(steamBsFolder, { steam: true }); } private async getOculusVersion(): Promise { @@ -194,14 +196,7 @@ export class BSLocalVersionService { return null; } - const oculusBsVersion = await this.getVersionOfBSFolder(oculusBsFolder); - - if(!oculusBsVersion){ return null; } - - oculusBsVersion.name = undefined; - oculusBsVersion.oculus = true; - - return oculusBsVersion; + return this.getVersionOfBSFolder(oculusBsFolder, { oculus: true }); } public async getInstalledVersions(): Promise { @@ -253,7 +248,7 @@ export class BSLocalVersionService { const oldPath = await this.getVersionPath(version); const editedVersion: BSVersion = version.BSVersion === name ? {...version, name: undefined, color} - : {...version, name: this.removeSpecialChar(name), color}; + : {...version, name: sanitize(name), color}; const newPath = await this.getVersionPath(editedVersion); if(oldPath === newPath){ @@ -278,7 +273,7 @@ export class BSLocalVersionService { 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}; + : {...version, name: sanitize(name), color, steam: false, oculus: false}; const newPath = await this.getVersionPath(cloneVersion); if(originPath === newPath){ diff --git a/src/main/services/notification.service.ts b/src/main/services/notification.service.ts index 5ea92cbc..e64f4e51 100644 --- a/src/main/services/notification.service.ts +++ b/src/main/services/notification.service.ts @@ -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) { - try { - this.ipc.send("show-notification", "index.html", notification); - } catch (e) { - console.error(e); - } - } } diff --git a/src/renderer/components/modal/modal-types/create-launch-shortcut-modal.component.tsx b/src/renderer/components/modal/modal-types/create-launch-shortcut-modal.component.tsx new file mode 100644 index 00000000..18f5fc4c --- /dev/null +++ b/src/renderer/components/modal/modal-types/create-launch-shortcut-modal.component.tsx @@ -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 = ({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 ( +
e.preventDefault()}> +

{t("modals.create-launch-shortcut.title")}

+

{t("modals.create-launch-shortcut.desc")}

+
+ +
+ Beat Saber + {[data.BSVersion, data.name].join(" ")} +
+
+

{t("modals.create-launch-shortcut.launch-options")}

+
+ {data.oculus !== true && ( +
+ setLaunchOptions({...launchOptions, oculus: e})} /> + + {t("pages.version-viewer.launch-mods.oculus")} + +
+ )} +
+ setLaunchOptions({...launchOptions, desktop: e})} /> + + {t("pages.version-viewer.launch-mods.desktop")} + +
+
+ setLaunchOptions({...launchOptions, debug: e})} /> + + {t("pages.version-viewer.launch-mods.debug")} + +
+
+
+
setAdvanced(prev => !prev)}> + {t("modals.create-launch-shortcut.advanced-launch")} + +
+
+
+
+ setAdditionalArgsString(e.target.value)} + /> +
+
+
+
+
+ resolver({ exitCode: ModalExitCode.CANCELED })} withBar={false} text="misc.cancel" /> + +
+
+ ) +} diff --git a/src/renderer/components/svgs/bsm-icon.component.tsx b/src/renderer/components/svgs/bsm-icon.component.tsx index a954abab..7ba0ae47 100644 --- a/src/renderer/components/svgs/bsm-icon.component.tsx +++ b/src/renderer/components/svgs/bsm-icon.component.tsx @@ -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 ; } + if (icon === "shortcut") { + return ; + } + if (icon === MSModelType.Avatar) { return ; } diff --git a/src/renderer/components/svgs/icons/shortcut-icon.component.tsx b/src/renderer/components/svgs/icons/shortcut-icon.component.tsx new file mode 100644 index 00000000..13e4598e --- /dev/null +++ b/src/renderer/components/svgs/icons/shortcut-icon.component.tsx @@ -0,0 +1,9 @@ +import { CSSProperties } from "react"; + +export function ShortcutIcon(props: { className?: string; style?: CSSProperties }) { + return ( + + + + ) +} diff --git a/src/renderer/pages/version-viewer.component.tsx b/src/renderer/pages/version-viewer.component.tsx index cab3dbe0..6d4d92dd 100644 --- a/src/renderer/pages/version-viewer.component.tsx +++ b/src/renderer/pages/version-viewer.component.tsx @@ -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 ( <> @@ -90,7 +115,14 @@ export function VersionViewer() { - + ); } diff --git a/src/renderer/services/bs-downloader.service.ts b/src/renderer/services/bs-downloader.service.ts index 667dfa30..bd4bd379 100644 --- a/src/renderer/services/bs-downloader.service.ts +++ b/src/renderer/services/bs-downloader.service.ts @@ -136,7 +136,7 @@ export class BsDownloaderService { } public async download(bsVersion: BSVersion, isVerification?: boolean, isFirstCall = true): Promise> { - // 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 }; diff --git a/src/renderer/services/bs-launcher.service.ts b/src/renderer/services/bs-launcher.service.ts index 40f00861..492e5d5d 100644 --- a/src/renderer/services/bs-launcher.service.ts +++ b/src/renderer/services/bs-launcher.service.ts @@ -3,8 +3,10 @@ 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, Observable, filter } from "rxjs"; +import { BehaviorSubject, Observable, filter, of } from "rxjs"; import { NotificationResult } from "shared/models/notification/notification.model"; +import { ConfigurationService } from "./configuration.service"; +import { ThemeService } from "./theme.service"; export class BSLauncherService { private static instance: BSLauncherService; @@ -12,6 +14,8 @@ export class BSLauncherService { private readonly ipcService: IpcService; private readonly notificationService: NotificationService; private readonly bsDownloaderService: BsDownloaderService; + private readonly config: ConfigurationService; + private readonly theme: ThemeService; public readonly versionRunning$: BehaviorSubject = new BehaviorSubject(null); @@ -24,48 +28,20 @@ export class BSLauncherService { this.ipcService = IpcService.getInstance(); this.notificationService = NotificationService.getInstance(); this.bsDownloaderService = BsDownloaderService.getInstance(); + this.config = ConfigurationService.getInstance(); + this.theme = ThemeService.getInstance(); } - // TODO REMOVE - private listenBsExit(): void{ - this.ipcService.watch("bs-launch.exit").subscribe(res => { - const version = this.versionRunning$.value; - this.versionRunning$.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); } - }); - }); + 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("additionnal-args") || "").split(";").map(arg => arg.trim()).filter(arg => arg.length > 0) + } } - // TODO : Rework with shortcuts implementation - public launch_old(version: BSVersion, oculus: boolean, desktop: boolean, debug: boolean, additionalArgs?: string[]): Promise{ - const lauchOption: LaunchOption = {debug, oculus, desktop, version, additionalArgs}; - if(this.versionRunning$.value){ return this.notificationService.notifyError({title: "notifications.bs-launch.errors.titles.BS_ALREADY_RUNNING"}); } - this.versionRunning$.next(version); - return this.ipcService.send("bs-launch.launch", {args: lauchOption}).then(res => { - - if(res.data === "LAUNCHED"){ return this.notificationService.notifySuccess({title: "notifications.bs-launch.success.titles.launching"}); } - - this.versionRunning$.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{ return this.ipcService.sendV2("bs-launch.launch", {args: launchOptions}); } @@ -96,6 +72,11 @@ export class BSLauncherService { return launchState$; } + public createLaunchShortcut(launchOptions: LaunchOption): Observable{ + const options: LaunchOption = {...launchOptions, version: {...launchOptions.version, color: launchOptions.version.color || this.theme.getBsmColors()[1]}}; + return this.ipcService.sendV2("create-launch-shortcut", {args: options}); + } + } export enum LaunchMods { diff --git a/src/renderer/services/notification.service.ts b/src/renderer/services/notification.service.ts index dfd4a150..c1183172 100644 --- a/src/renderer/services/notification.service.ts +++ b/src/renderer/services/notification.service.ts @@ -20,11 +20,6 @@ export class NotificationService { private constructor() { this.ipc = IpcService.getInstance(); this.notifications$ = new BehaviorSubject([]); - - // TODO : Make actions work and adapt with "watch" remork - this.ipc.watch("show-notification").subscribe(notification => { - this.notify(notification as unknown as Notification); - }); } public notify(notification: Notification): Promise { diff --git a/src/renderer/services/theme.service.ts b/src/renderer/services/theme.service.ts index d605dd68..2b2ceb5a 100644 --- a/src/renderer/services/theme.service.ts +++ b/src/renderer/services/theme.service.ts @@ -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; + public readonly theme$: Observable; 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); } diff --git a/src/renderer/windows/ShortcutLaunch.tsx b/src/renderer/windows/ShortcutLaunch.tsx index 13d4d8fd..c1058f34 100644 --- a/src/renderer/windows/ShortcutLaunch.tsx +++ b/src/renderer/windows/ShortcutLaunch.tsx @@ -63,9 +63,9 @@ export default function ShortcutLaunch() { }, error: err => { if(!Object.values(BSLaunchError).includes(err.type)){ - notification.notifySystem({title: t("bs-launch.errors.titles.UNKNOWN_ERROR"), body: t("bs-launch.errors.msg.UNKNOWN_ERROR")}); + 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(`bs-launch.errors.titles.${err.type}`), body: t(`bs-launch.errors.msg.${err.type}`)}) + notification.notifySystem({title: t(`notifications.bs-launch.errors.titles.${err.type}`), body: t(`notifications.bs-launch.errors.msg.${err.type}`)}) } } }); diff --git a/src/shared/models/bs-launch/launch-option.interface.ts b/src/shared/models/bs-launch/launch-option.interface.ts index 66feaa8a..ae3739a3 100644 --- a/src/shared/models/bs-launch/launch-option.interface.ts +++ b/src/shared/models/bs-launch/launch-option.interface.ts @@ -2,9 +2,9 @@ import { BSVersion } from "shared/bs-version.interface"; export interface LaunchOption { version: BSVersion, - oculus: boolean, - desktop: boolean, - debug: boolean, + oculus?: boolean, + desktop?: boolean, + debug?: boolean, additionalArgs?: string[], skipAlreadyRunning?: boolean }