[feature] can now create launch shortcut

This commit is contained in:
MathieuG-P
2023-07-16 04:00:16 +02:00
parent 2f3b5bf610
commit 0acb70cf7e
20 changed files with 384 additions and 189 deletions
+17
View File
@@ -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": {
+17
View File
@@ -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": {
+17
View File
@@ -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": {
+17
View File
@@ -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": {
+1 -1
View File
@@ -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);
+7 -2
View File
@@ -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<LaunchOption>, reply) => {
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)));
});
-25
View File
@@ -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<any>) => {
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);
}
+87 -44
View File
@@ -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<string>} Path of the icon
*/
private async createShortcutIco(color: Color): Promise<string>{
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()}"/>
@@ -236,10 +223,36 @@ export class BSLauncherService {
</svg>
`;
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<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);
@@ -251,36 +264,42 @@ export class BSLauncherService {
return iconPath;
}
private createInternetShortcut(options: { url: string, output: string, iconFile?: string, iconIndex?: number }): Promise<void>{
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<void>{
public async createLaunchShortcut(launchOptions: LaunchOption): Promise<boolean>{
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<void>{
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<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);
}
+45 -50
View File
@@ -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<BSVersion> {
public async getVersionOfBSFolder(bsPath: string): Promise<BSVersion>{
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<BSVersion>(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<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);
}
@@ -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<BSVersion> {
@@ -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<BSVersion[]> {
@@ -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){
-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);
}
}
}
@@ -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>
)
}
@@ -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} />;
}
@@ -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>
)
}
@@ -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 };
+20 -39
View File
@@ -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<BSVersion> = 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<string>("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<NotificationResult|string>{
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<LaunchResult>("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<BSLaunchEventData>{
return this.ipcService.sendV2<BSLaunchEventData, LaunchOption>("bs-launch.launch", {args: launchOptions});
}
@@ -96,6 +72,11 @@ export class BSLauncherService {
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);
}
+2 -2
View File
@@ -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}`)})
}
}
});
@@ -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
}