mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
Merge pull request #731 from Zagrios/feature/create-steam-shortcut
[feature] create steam shortcut
This commit is contained in:
@@ -636,7 +636,8 @@
|
||||
"create-launch-shortcut": {
|
||||
"success": {
|
||||
"title": "Verknüpfung erstellt",
|
||||
"msg": "Die Verknüpfung wurde auf dem Desktop erstellt."
|
||||
"msg": "Die Verknüpfung wurde auf dem Desktop erstellt.",
|
||||
"msg-steam": "Die Verknüpfung wurde in der Steam-Bibliothek erstellt."
|
||||
},
|
||||
"error": {
|
||||
"msg": "Beim Erstellen der Verknüpfung ist ein Fehler aufgetreten."
|
||||
@@ -843,7 +844,9 @@
|
||||
"desc": "Das Erstellen einer Verknüpfung ermöglicht es dir, Beat Saber mit den ausgewählten Optionen zu starten, ohne durch BSManager zu gehen.",
|
||||
"launch-options": "Startoptionen",
|
||||
"advanced-launch": "Erweiterter Start",
|
||||
"valid-btn": "Verknüpfung erstellen"
|
||||
"valid-btn": "Verknüpfung erstellen",
|
||||
"create-steam-shortcut": "Steam-Verknüpfung erstellen",
|
||||
"steam-shortcut-tippy": "Wenn aktiviert, wird anstelle einer Verknüpfung auf dem Desktop eine Verknüpfung in Steam erstellt."
|
||||
},
|
||||
"connect-to-meta": {
|
||||
"title": "Mit Meta verbinden",
|
||||
|
||||
@@ -636,7 +636,8 @@
|
||||
"create-launch-shortcut": {
|
||||
"success": {
|
||||
"title": "Shortcut created",
|
||||
"msg": "The shortcut has been created on the desktop."
|
||||
"msg": "The shortcut has been created on the desktop.",
|
||||
"msg-steam": "The shortcut has been created in the Steam library."
|
||||
},
|
||||
"error": {
|
||||
"msg": "An error occurred while creating the shortcut."
|
||||
@@ -838,7 +839,9 @@
|
||||
"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"
|
||||
"valid-btn": "Create the shortcut",
|
||||
"create-steam-shortcut": "Create a Steam shortcut",
|
||||
"steam-shortcut-tippy": "If enabled, instead of creating a shortcut on the desktop, it will be created in Steam."
|
||||
},
|
||||
"connect-to-meta": {
|
||||
"title": "Connect to Meta",
|
||||
|
||||
@@ -636,7 +636,8 @@
|
||||
"create-launch-shortcut": {
|
||||
"success": {
|
||||
"title": "Acceso directo creado",
|
||||
"msg": "El acceso directo se ha creado en el escritorio."
|
||||
"msg": "El acceso directo se ha creado en el escritorio.",
|
||||
"msg-steam": "El acceso directo se ha creado en la biblioteca de Steam."
|
||||
},
|
||||
"error": {
|
||||
"msg": "Se produjo un error al crear el acceso directo."
|
||||
@@ -843,7 +844,9 @@
|
||||
"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"
|
||||
"valid-btn": "Crear el atajo",
|
||||
"create-steam-shortcut": "Crear un atajo directo de Steam",
|
||||
"steam-shortcut-tippy": "Si está activado, en lugar de crear un atajo directo en el escritorio, se creará en Steam."
|
||||
},
|
||||
"connect-to-meta": {
|
||||
"title": "Conexión a Meta",
|
||||
|
||||
@@ -637,7 +637,8 @@
|
||||
"create-launch-shortcut": {
|
||||
"success": {
|
||||
"title": "Raccourci créé",
|
||||
"msg": "Le raccourci a été créé sur le bureau."
|
||||
"msg": "Le raccourci a été créé sur le bureau.",
|
||||
"msg-steam": "Le raccourci a été créé dans la bibliothèque Steam."
|
||||
},
|
||||
"error": {
|
||||
"msg": "Une erreur s'est produite lors de la création du raccourci."
|
||||
@@ -844,7 +845,9 @@
|
||||
"desc": "Créer un raccourci te permettra de démarrer Beat Saber avec les options choisies sans passer par BSManager.",
|
||||
"launch-options": "Options de lancement",
|
||||
"advanced-launch": "Lancement avancé",
|
||||
"valid-btn": "Créer le raccourci"
|
||||
"valid-btn": "Créer le raccourci",
|
||||
"create-steam-shortcut": "Créer un raccourci Steam",
|
||||
"steam-shortcut-tippy": "Si activé, au lieu de créer un raccourci sur le bureau, celui-ci sera créé dans Steam."
|
||||
},
|
||||
"connect-to-meta": {
|
||||
"title": "Connexion à Meta",
|
||||
|
||||
@@ -636,7 +636,8 @@
|
||||
"create-launch-shortcut": {
|
||||
"success": {
|
||||
"title": "Scorciatoia creata",
|
||||
"msg": "La scorciatoia è già stat creata sul desktop."
|
||||
"msg": "La scorciatoia è già stat creata sul desktop.",
|
||||
"msg-steam": "Il collegamento è stato creato nella libreria di Steam."
|
||||
},
|
||||
"error": {
|
||||
"msg": "C'è stato un errore durante la creazione della scorciatoia."
|
||||
@@ -838,7 +839,9 @@
|
||||
"desc": "Creare una scorciatoia ti permetterà di avviare Beat Saber con le opzioni scelte senza passare per BSManager.",
|
||||
"launch-options": "Opzioni di lancio",
|
||||
"advanced-launch": "Lancio avanzato",
|
||||
"valid-btn": "Crea la scorciatoia"
|
||||
"valid-btn": "Crea la scorciatoia",
|
||||
"create-steam-shortcut": "Crea un scorciatoia Steam",
|
||||
"steam-shortcut-tippy": "Se abilitato, invece di creare un scorciatoia sul desktop, verrà creato in Steam."
|
||||
},
|
||||
"connect-to-meta": {
|
||||
"title": "Connettiti a Meta",
|
||||
|
||||
@@ -636,7 +636,8 @@
|
||||
"create-launch-shortcut": {
|
||||
"success": {
|
||||
"title": "ショートカットを作成しました",
|
||||
"msg": "デスクトップにショートカットを作成されました。"
|
||||
"msg": "デスクトップにショートカットを作成されました。",
|
||||
"msg-steam": "ショートカットがSteamライブラリに作成されました。"
|
||||
},
|
||||
"error": {
|
||||
"msg": "ショートカットの作成中にエラーが発生しました。"
|
||||
@@ -843,7 +844,9 @@
|
||||
"desc": "ショートカットを作成すると、BSManagerを起動せずに、直接Beat Saberを起動できます。",
|
||||
"launch-options": "起動オプション",
|
||||
"advanced-launch": "高度な起動",
|
||||
"valid-btn": "ショートカットを作成"
|
||||
"valid-btn": "ショートカットを作成",
|
||||
"create-steam-shortcut": "Steamショートカットを作成",
|
||||
"steam-shortcut-tippy": "有効にすると、デスクトップにショートカットを作成する代わりに、Steam内に作成されます。"
|
||||
},
|
||||
"connect-to-meta": {
|
||||
"title": "Metaに接続する",
|
||||
|
||||
@@ -636,7 +636,8 @@
|
||||
"create-launch-shortcut": {
|
||||
"success": {
|
||||
"title": "바로 가기 생성 완료",
|
||||
"msg": "바탕 화면에 바로 가기가 생성되었습니다."
|
||||
"msg": "바탕 화면에 바로 가기가 생성되었습니다.",
|
||||
"msg-steam": "바로 가기가 Steam 라이브러리에 생성되었습니다."
|
||||
},
|
||||
"error": {
|
||||
"msg": "바로 가기 생성 중 오류가 발생했습니다."
|
||||
@@ -838,12 +839,14 @@
|
||||
"title": "공유 폴더 추가",
|
||||
"description": "「{folder}」폴더를 연결할 때 문제가 발생할 수 있습니다. 정말 추가하시겠습니까?"
|
||||
},
|
||||
"create-launcher-shortcut": {
|
||||
"title": "런처 바로가기 만들기",
|
||||
"description": "바탕화면에 실행 파일의 바로가기를 만들어 BSManager를 손쉽게 실행하세요.",
|
||||
"buttons": {
|
||||
"submit": "바로가기 만들기"
|
||||
}
|
||||
"create-launch-shortcut": {
|
||||
"title": "바로가기를 생성",
|
||||
"desc": "바로가기를 생성하면 BSManager를 거치지 않고 선택한 옵션으로 Beat Saber를 시작할 수 있습니다.",
|
||||
"launch-options": "실행 옵션",
|
||||
"advanced-launch": "고급 실행",
|
||||
"valid-btn": "바로가기 생성",
|
||||
"create-steam-shortcut": "Steam 바로가기를 생성",
|
||||
"steam-shortcut-tippy": "활성화하면 바탕 화면에 바로가기를 생성하는 대신 Steam에 생성됩니다."
|
||||
},
|
||||
"connect-to-meta": {
|
||||
"title": "Meta에 연결하기",
|
||||
|
||||
@@ -636,7 +636,8 @@
|
||||
"create-launch-shortcut": {
|
||||
"success": {
|
||||
"title": "Ярлык создан",
|
||||
"msg": "Ярлык находится на рабочем столе."
|
||||
"msg": "Ярлык находится на рабочем столе.",
|
||||
"msg-steam": "Ярлык был создан в библиотеке Steam."
|
||||
},
|
||||
"error": {
|
||||
"msg": "Ошибка создания ярлыка."
|
||||
@@ -843,7 +844,9 @@
|
||||
"desc": "Ярлык позволит вам запустить Beat Saber без помощи BSManager.",
|
||||
"launch-options": "Параметры запуска",
|
||||
"advanced-launch": "Расширенный запуск",
|
||||
"valid-btn": "Создать ярлык"
|
||||
"valid-btn": "Создать ярлык",
|
||||
"create-steam-shortcut": "Создать ярлык для Steam",
|
||||
"steam-shortcut-tippy": "Если включено, вместо создания ярлыка на рабочем столе, он будет создан в Steam."
|
||||
},
|
||||
"connect-to-meta": {
|
||||
"title": "Подключение к Meta",
|
||||
|
||||
@@ -636,7 +636,8 @@
|
||||
"create-launch-shortcut": {
|
||||
"success": {
|
||||
"title": "捷徑已創建",
|
||||
"msg": "捷徑已在桌面上創建"
|
||||
"msg": "捷徑已在桌面上創建",
|
||||
"msg-steam": "捷徑已在 Steam 庫中建立。"
|
||||
},
|
||||
"error": {
|
||||
"msg": "創建捷徑時發生錯誤"
|
||||
@@ -843,7 +844,9 @@
|
||||
"desc": "創建捷徑使得你可以在不通過 BSManager 的情況下以特定選項啟動 BeatSaber",
|
||||
"launch-options": "啟動選項",
|
||||
"advanced-launch": "高級啟動",
|
||||
"valid-btn": "創建捷徑"
|
||||
"valid-btn": "創建捷徑",
|
||||
"create-steam-shortcut": "建立 Steam 快捷方式",
|
||||
"steam-shortcut-tippy": "如果啟用,將不會在桌面建立捷徑,而是在 Steam 中建立。"
|
||||
},
|
||||
"connect-to-meta": {
|
||||
"title": "連接到 Meta",
|
||||
|
||||
@@ -636,7 +636,8 @@
|
||||
"create-launch-shortcut": {
|
||||
"success": {
|
||||
"title": "快捷方式已创建",
|
||||
"msg": "快捷方式已在桌面上创建"
|
||||
"msg": "快捷方式已在桌面上创建",
|
||||
"msg-steam": "快捷方式已在 Steam 库中创建。"
|
||||
},
|
||||
"error": {
|
||||
"msg": "创建快捷方式时发生错误"
|
||||
@@ -843,7 +844,9 @@
|
||||
"desc": "创建快捷方式使得你可以在不通过 BSManager 的情况下以特定选项启动 BeatSaber",
|
||||
"launch-options": "启动选项",
|
||||
"advanced-launch": "高级启动",
|
||||
"valid-btn": "创建快捷方式"
|
||||
"valid-btn": "创建快捷方式",
|
||||
"create-steam-shortcut": "创建 Steam 快捷方式",
|
||||
"steam-shortcut-tippy": "如果启用,将不会在桌面创建快捷方式,而是在 Steam 中创建。"
|
||||
},
|
||||
"connect-to-meta": {
|
||||
"title": "连接到 Meta",
|
||||
|
||||
@@ -27,7 +27,7 @@ ipc.on("bs-launch.need-start-as-admin", (_, reply) => {
|
||||
|
||||
ipc.on("create-launch-shortcut", (args, reply) => {
|
||||
const bsLauncher = BSLauncherService.getInstance();
|
||||
reply(from(bsLauncher.createLaunchShortcut(args)));
|
||||
reply(from(bsLauncher.createLaunchShortcut(args.options, args.steamShortcut)));
|
||||
});
|
||||
|
||||
ipc.on("bs-launch.restore-steamvr", (_, reply) => {
|
||||
|
||||
@@ -22,6 +22,8 @@ import { BSVersion } from "shared/bs-version.interface";
|
||||
import { BsStore } from "../../../shared/models/bs-store.enum";
|
||||
import { LaunchMod, LaunchMods } from "shared/models/bs-launch/launch-option.interface";
|
||||
import { StaticConfigurationService } from "../static-configuration.service";
|
||||
import { SteamService } from "../steam.service";
|
||||
import { tryit } from "shared/helpers/error.helpers";
|
||||
|
||||
export class BSLauncherService {
|
||||
private static instance: BSLauncherService;
|
||||
@@ -32,6 +34,7 @@ export class BSLauncherService {
|
||||
private readonly ipc: IpcService;
|
||||
private readonly remoteVersion: BSVersionLibService;
|
||||
private readonly steamLauncher: SteamLauncherService;
|
||||
private readonly steam: SteamService;
|
||||
private readonly oculusLauncher: OculusLauncherService;
|
||||
private readonly staticConfig: StaticConfigurationService;
|
||||
|
||||
@@ -51,6 +54,7 @@ export class BSLauncherService {
|
||||
this.steamLauncher = SteamLauncherService.getInstance();
|
||||
this.oculusLauncher = OculusLauncherService.getInstance();
|
||||
this.staticConfig = StaticConfigurationService.getInstance();
|
||||
this.steam = SteamService.getInstance();
|
||||
|
||||
this.bsmProtocolService.on("launch", link => {
|
||||
log.info("Launch from bsm protocol", link.toString());
|
||||
@@ -195,12 +199,28 @@ export class BSLauncherService {
|
||||
return this.bsmProtocolService.buildLink("launch", shortcutParams).toString();
|
||||
}
|
||||
|
||||
public async createLaunchShortcut(launchOptions: LaunchOption): Promise<boolean>{
|
||||
public async createLaunchShortcut(launchOptions: LaunchOption, steamShortcut?: boolean): Promise<boolean>{
|
||||
const shortcutUrl = this.createLaunchLink(launchOptions);
|
||||
|
||||
const shortcutName = ["Beat Saber", launchOptions.version.BSVersion, launchOptions.version.name].join(" ");
|
||||
const shortcutIconColor = new Color(launchOptions.version.color, "hex");
|
||||
|
||||
if(steamShortcut){
|
||||
const userId = await tryit(() => this.steam.getActiveUser());
|
||||
const exePath = app.getPath("exe");
|
||||
return this.steam.createShortcut({
|
||||
AppName: shortcutName,
|
||||
Exe: exePath,
|
||||
StartDir: path.dirname(exePath),
|
||||
LaunchOptions: this.createLaunchLink(launchOptions),
|
||||
icon: await this.createShortcutPng(shortcutIconColor),
|
||||
OpenVR: "\u0001"
|
||||
}, userId.result).then(() => true).catch(e => {
|
||||
log.error(e);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
return execOnOs({
|
||||
win32: async () => (
|
||||
shell.writeShortcutLink(path.join(app.getPath("desktop"), `${shortcutName}.lnk`), {
|
||||
@@ -227,7 +247,7 @@ export class BSLauncherService {
|
||||
const bsPath: string = await (async () => {
|
||||
const bsPath = await this.localVersionService.getInstalledVersionPath(launchOption.version);
|
||||
return bsPath ?? this.localVersionService.getVersionPath(launchOption.version);
|
||||
})().catch(e => {
|
||||
})().catch((e): null => {
|
||||
log.error(e);
|
||||
return null;
|
||||
});
|
||||
|
||||
@@ -2,49 +2,55 @@ import { RegDwordValue } from "regedit-rs"
|
||||
import path from "path";
|
||||
import { parse } from "@node-steam/vdf";
|
||||
import { readFile } from "fs/promises";
|
||||
import { pathExist } from "../helpers/fs.helpers";
|
||||
import log from "electron-log";
|
||||
import { app, shell } from "electron";
|
||||
import { getProcessId, isProcessRunning } from "main/helpers/os.helpers";
|
||||
import { isElevated } from "query-process";
|
||||
import { execOnOs } from "../helpers/env.helpers";
|
||||
import { pathExists, pathExistsSync, readdir, writeFile } from "fs-extra";
|
||||
import { SteamShortcut, SteamShortcutData } from "../../shared/models/steam/shortcut.model";
|
||||
|
||||
const { list } = (execOnOs({ win32: () => require("regedit-rs") }, true) ?? {}) as typeof import("regedit-rs");
|
||||
|
||||
export class SteamService {
|
||||
|
||||
private static readonly PROCESS_NAME: string = process.platform === "linux"
|
||||
? "steam-runtime-launcher-service"
|
||||
: "steam.exe";
|
||||
private static readonly PROCESS_NAME: string = process.platform === "linux" ? "steam-runtime-launcher-service" : "steam.exe";
|
||||
|
||||
private static instance: SteamService;
|
||||
|
||||
private steamPath: string = '';
|
||||
private steamPath: string = "";
|
||||
|
||||
private constructor(){}
|
||||
private constructor() {}
|
||||
|
||||
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 list("HKCU\\Software\\Valve\\Steam\\ActiveProcess");
|
||||
const key = res["HKCU\\Software\\Valve\\Steam\\ActiveProcess"];
|
||||
if(!key.exists){ throw new Error("Key \"HKCU\\Software\\Valve\\Steam\\ActiveProcess\" not exist"); }
|
||||
if (!key.exists) {
|
||||
throw new Error('Key "HKCU\\Software\\Valve\\Steam\\ActiveProcess" not exist');
|
||||
}
|
||||
const registryValue = key.values.ActiveUser as RegDwordValue;
|
||||
if(!registryValue){ throw new Error("Value \"ActiveUser\" not exist"); }
|
||||
if (!registryValue) {
|
||||
throw new Error('Value "ActiveUser" not exist');
|
||||
}
|
||||
return registryValue.value;
|
||||
}
|
||||
|
||||
public async isSteamRunning(): Promise<boolean>{
|
||||
public async isSteamRunning(): Promise<boolean> {
|
||||
const steamProcessRunning = await isProcessRunning(SteamService.PROCESS_NAME);
|
||||
if(process.platform === "linux") { return steamProcessRunning; }
|
||||
if (process.platform === "linux") {
|
||||
return steamProcessRunning;
|
||||
}
|
||||
const activeUser = await this.getActiveUser().catch(err => log.error(err));
|
||||
return steamProcessRunning && !!activeUser;
|
||||
}
|
||||
|
||||
public async getSteamPid(): Promise<number>{
|
||||
public async getSteamPid(): Promise<number> {
|
||||
return getProcessId(SteamService.PROCESS_NAME);
|
||||
}
|
||||
|
||||
@@ -53,26 +59,29 @@ export class SteamService {
|
||||
* @throws Can throw an error if the Steam process is running as admin
|
||||
* @returns true if the Steam process is running as administrator
|
||||
*/
|
||||
public async isElevated(): Promise<boolean>{
|
||||
public async isElevated(): Promise<boolean> {
|
||||
const steamPid = await this.getSteamPid();
|
||||
|
||||
if(!steamPid){ return false; }
|
||||
if (!steamPid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isElevated(steamPid);
|
||||
}
|
||||
|
||||
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");
|
||||
this.steamPath = path.join(app.getPath("home"), ".steam", "steam");
|
||||
return this.steamPath;
|
||||
case "win32": {
|
||||
const res = await list(['HKLM\\SOFTWARE\\Valve\\Steam', 'HKLM\\SOFTWARE\\WOW6432Node\\Valve\\Steam']);
|
||||
const win64 = res['HKLM\\SOFTWARE\\Valve\\Steam'];
|
||||
const win32 = res['HKLM\\SOFTWARE\\WOW6432Node\\Valve\\Steam'];
|
||||
const res = await list(["HKLM\\SOFTWARE\\Valve\\Steam", "HKLM\\SOFTWARE\\WOW6432Node\\Valve\\Steam"]);
|
||||
const win64 = res["HKLM\\SOFTWARE\\Valve\\Steam"];
|
||||
const win32 = res["HKLM\\SOFTWARE\\WOW6432Node\\Valve\\Steam"];
|
||||
|
||||
if (win64.exists && win64?.values?.InstallPath?.value) {
|
||||
this.steamPath = win64.values.InstallPath.value as string;
|
||||
@@ -93,15 +102,21 @@ export class SteamService {
|
||||
|
||||
let libraryFolders: any = path.join(steamPath, "steamapps", "libraryfolders.vdf");
|
||||
|
||||
if (!(await pathExist(libraryFolders))) { return null; }
|
||||
if (!(await pathExists(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]?.apps) { continue; }
|
||||
if (!libraryFolders?.[libKey]?.apps) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (libraryFolders[libKey].apps[gameId] != null) {
|
||||
return path.join(libraryFolders[libKey].path, "steamapps", "common", gameFolder);
|
||||
@@ -109,7 +124,6 @@ export class SteamService {
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
} catch (e) {
|
||||
log.error(e);
|
||||
return null;
|
||||
@@ -117,7 +131,6 @@ export class SteamService {
|
||||
}
|
||||
|
||||
public async openSteam(): Promise<void> {
|
||||
|
||||
await shell.openExternal("steam://open/games");
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -125,7 +138,9 @@ export class SteamService {
|
||||
const interval = setInterval(() => {
|
||||
const steamRunning = this.isSteamRunning().catch(() => false);
|
||||
steamRunning.then(running => {
|
||||
if(!running){ return; }
|
||||
if (!running) {
|
||||
return;
|
||||
}
|
||||
clearInterval(interval);
|
||||
resolve();
|
||||
});
|
||||
@@ -138,4 +153,70 @@ export class SteamService {
|
||||
}, 60_000);
|
||||
});
|
||||
}
|
||||
|
||||
private async getUserDataFolders(): Promise<string[]> {
|
||||
const steamPath = await this.getSteamPath();
|
||||
|
||||
if (!steamPath) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const configPath = path.join(steamPath, "userdata");
|
||||
|
||||
if (!pathExistsSync(configPath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const folders = readdir(configPath, { withFileTypes: true })
|
||||
.then(entries => {
|
||||
return entries.reduce((acc, entry) => {
|
||||
if (entry.isDirectory() && Number.isInteger(parseInt(entry.name))) {
|
||||
acc.push(path.join(configPath, entry.name));
|
||||
}
|
||||
return acc;
|
||||
}, [] as string[]);
|
||||
})
|
||||
.catch((err): string[] => {
|
||||
log.error("Error while reading steam user data folders", err);
|
||||
return [];
|
||||
});
|
||||
|
||||
return folders;
|
||||
}
|
||||
|
||||
private async getShortcutsPath(userId: number): Promise<string> {
|
||||
return path.join(await this.getSteamPath(), "userdata", userId.toString(), "config", "shortcuts.vdf");
|
||||
}
|
||||
|
||||
private async getShortcuts(userId: number): Promise<SteamShortcut[]> {
|
||||
const shortcutsPath = await this.getShortcutsPath(userId);
|
||||
const shortcutsString = await readFile(shortcutsPath, { encoding: "utf-8" });
|
||||
|
||||
return SteamShortcut.parseShortcutsRawData(shortcutsString);
|
||||
}
|
||||
|
||||
private async writeShortcuts(shortcuts: SteamShortcut[], userId: number): Promise<void> {
|
||||
const shortcutsPath = await this.getShortcutsPath(userId);
|
||||
const stringData = SteamShortcut.getShortcutsString(shortcuts);
|
||||
await writeFile(shortcutsPath, stringData, { encoding: "utf-8" });
|
||||
}
|
||||
|
||||
public async createShortcut(shortcutData: SteamShortcutData, userId?: number): Promise<void> {
|
||||
const userIds = userId ? [userId] : await (async (): Promise<number[]> => {
|
||||
const folders = await this.getUserDataFolders();
|
||||
return folders.map(folder => parseInt(path.basename(folder)));
|
||||
})();
|
||||
|
||||
for (const userId of userIds) {
|
||||
const shortcuts = await this.getShortcuts(userId).catch(e => {
|
||||
log.warn("Error while reading shortcuts", e);
|
||||
return [];
|
||||
});
|
||||
shortcuts.push(new SteamShortcut(shortcutData));
|
||||
|
||||
await this.writeShortcuts(shortcuts, userId);
|
||||
}
|
||||
|
||||
log.info("Shortcut created", shortcutData);
|
||||
}
|
||||
}
|
||||
|
||||
+36
-13
@@ -1,37 +1,52 @@
|
||||
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 { useTranslationV2 } 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 { useMemo, useState } from "react";
|
||||
import { BsNoteFill } from "renderer/components/svgs/icons/bs-note-fill.component";
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
import { ChevronTopIcon } from "renderer/components/svgs/icons/chevron-top-icon.component";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import { LaunchMod } from "shared/models/bs-launch/launch-option.interface";
|
||||
import { BsStore } from "shared/models/bs-store.enum";
|
||||
|
||||
export const CreateLaunchShortcutModal: ModalComponent<LaunchOption, BSVersion> = ({resolver, options: {data}}) => {
|
||||
export const CreateLaunchShortcutModal: ModalComponent<{ steamShortcut: boolean, launchOption: LaunchOption }, BSVersion> = ({resolver, options: {data}}) => {
|
||||
|
||||
const bsLauncher = useService(BSLauncherService);
|
||||
|
||||
const t = useTranslation();
|
||||
const { text: t } = useTranslationV2();
|
||||
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 [launchOption, setLaunchOptions] = useState(bsLauncher.getLaunchOptions(data));
|
||||
const [advanced, setAdvanced] = useState(!!launchOption.additionalArgs?.length);
|
||||
const [additionalArgsString, setAdditionalArgsString] = useState(launchOption.additionalArgs?.join("; ") ?? "");
|
||||
const [steamShortcut, setSteamShortcut] = useState(false);
|
||||
|
||||
const isSteamVersion = useMemo(() => {
|
||||
return data.steam || data.metadata?.store === BsStore.STEAM;
|
||||
}, [data]);
|
||||
|
||||
const completeModal = () => {
|
||||
|
||||
if(advanced) {
|
||||
launchOptions.additionalArgs = additionalArgsString.split(";").map(arg => arg.trim()).filter(arg => arg.length);
|
||||
launchOption.additionalArgs = additionalArgsString.split(";").map(arg => arg.trim()).filter(arg => arg.length);
|
||||
} else {
|
||||
launchOptions.additionalArgs = undefined;
|
||||
launchOption.additionalArgs = undefined;
|
||||
}
|
||||
|
||||
resolver({exitCode: ModalExitCode.COMPLETED, data: launchOptions});
|
||||
resolver({exitCode: ModalExitCode.COMPLETED, data: { launchOption, steamShortcut }});
|
||||
}
|
||||
|
||||
const toogleLaunchMod = (mod: LaunchMod, enabled: boolean) => {
|
||||
if(enabled) {
|
||||
setLaunchOptions(prev => ({...prev, launchMods: [...prev.launchMods, mod]}));
|
||||
} else {
|
||||
setLaunchOptions(prev => ({...prev, launchMods: prev.launchMods.filter(m => m !== mod)}));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -49,20 +64,20 @@ export const CreateLaunchShortcutModal: ModalComponent<LaunchOption, BSVersion>
|
||||
<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})} />
|
||||
<BsmCheckbox className="h-5 aspect-square relative z-[1]" checked={launchOption.launchMods.includes("oculus")} onChange={e => toogleLaunchMod("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})} />
|
||||
<BsmCheckbox className="h-5 aspect-square relative z-[1]" checked={launchOption.launchMods.includes("fpfc")} onChange={e => toogleLaunchMod("fpfc", 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})} />
|
||||
<BsmCheckbox className="h-5 aspect-square relative z-[1]" checked={launchOption.launchMods.includes("debug")} onChange={e => toogleLaunchMod("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>
|
||||
@@ -87,6 +102,14 @@ export const CreateLaunchShortcutModal: ModalComponent<LaunchOption, BSVersion>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isSteamVersion && (
|
||||
<Tippy placement="right" theme="default" content={t("modals.create-launch-shortcut.steam-shortcut-tippy")}>
|
||||
<div className="h-full flex items-center gap-1.5 mt-3 mb-4 w-fit pr-1">
|
||||
<BsmCheckbox className="h-5 aspect-square relative z-[1]" checked={steamShortcut} onChange={e => setSteamShortcut(() => e)} />
|
||||
<span>{t("modals.create-launch-shortcut.create-steam-shortcut")}</span>
|
||||
</div>
|
||||
</Tippy>
|
||||
)}
|
||||
<div className="grid grid-flow-col grid-cols-2 gap-4 mt-2">
|
||||
<BsmButton typeColor="cancel" className="rounded-md text-center transition-all" onClick={() => resolver({ exitCode: ModalExitCode.CANCELED })} withBar={false} text="misc.cancel" />
|
||||
<BsmButton typeColor="primary" className="rounded-md text-center transition-all" onClick={completeModal} withBar={false} text="modals.create-launch-shortcut.valid-btn" />
|
||||
|
||||
@@ -221,7 +221,7 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
|
||||
handleSearch();
|
||||
}}
|
||||
/>
|
||||
<BsmSelect className="bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-1 pb-0.5 text-center" options={sortOptions} selected={searchParams.sortOrder} onChange={sort => handleSortChange(sort)} />
|
||||
<BsmSelect className="bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-1 pb-0.5 text-center w-min" options={sortOptions} selected={searchParams.sortOrder} onChange={sort => handleSortChange(sort)} />
|
||||
</div>
|
||||
|
||||
{(() => {
|
||||
|
||||
@@ -120,10 +120,10 @@ export function VersionViewer() {
|
||||
const { exitCode, data } = await modalService.openModal(CreateLaunchShortcutModal, {data: state});
|
||||
if(exitCode !== ModalExitCode.COMPLETED){ return; }
|
||||
|
||||
lastValueFrom(bsLauncher.createLaunchShortcut(data)).then(() => {
|
||||
lastValueFrom(bsLauncher.createLaunchShortcut(data.launchOption, data.steamShortcut)).then(() => {
|
||||
notification.notifySuccess({
|
||||
title: "notifications.create-launch-shortcut.success.title",
|
||||
desc: "notifications.create-launch-shortcut.success.msg"
|
||||
desc: `notifications.create-launch-shortcut.success.${data.steamShortcut ? "msg-steam" : "msg"}`
|
||||
});
|
||||
}).catch(() => {
|
||||
notification.notifyError({
|
||||
|
||||
@@ -125,9 +125,9 @@ export class BSLauncherService {
|
||||
|
||||
}
|
||||
|
||||
public createLaunchShortcut(launchOptions: LaunchOption): Observable<boolean>{
|
||||
public createLaunchShortcut(launchOptions: LaunchOption, steamShortcut: boolean): Observable<boolean>{
|
||||
const options: LaunchOption = {...launchOptions, version: {...launchOptions.version, color: launchOptions.version.color || this.theme.getBsmColors()[1]}};
|
||||
return this.ipcService.sendV2("create-launch-shortcut", options);
|
||||
return this.ipcService.sendV2("create-launch-shortcut", { options, steamShortcut });
|
||||
}
|
||||
|
||||
public restoreSteamVR(): Promise<void>{
|
||||
|
||||
@@ -53,7 +53,7 @@ export interface IpcChannelMapping {
|
||||
"bs-installer.set-install-path": { request: { path: string, move: boolean }, response: string};
|
||||
|
||||
/* ** bs-launcher-ipcs ** */
|
||||
"create-launch-shortcut": { request: LaunchOption, response: boolean };
|
||||
"create-launch-shortcut": { request: { options: LaunchOption, steamShortcut?: boolean }, response: boolean };
|
||||
"bs-launch.need-start-as-admin": { request: void, response: boolean };
|
||||
"bs-launch.launch": { request: LaunchOption, response: BSLaunchEventData };
|
||||
"bs-launch.restore-steamvr": { request: void, response: void };
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
enum SteamShortcutKey {
|
||||
AppName = "AppName",
|
||||
Exe = "Exe",
|
||||
StartDir = "StartDir",
|
||||
appid = "appid",
|
||||
icon = "icon",
|
||||
ShortcutPath = "ShortcutPath",
|
||||
LaunchOptions = "LaunchOptions",
|
||||
IsHidden = "IsHidden",
|
||||
AllowDesktopConfig = "AllowDesktopConfig",
|
||||
OpenVR = "OpenVR",
|
||||
Devkit = "Devkit",
|
||||
DevkitGameID = "DevkitGameID",
|
||||
LastPlayTime = "LastPlayTime",
|
||||
FlatpakAppID = "FlatpakAppID",
|
||||
tags = "tags"
|
||||
}
|
||||
|
||||
type BaseShortcutData = {
|
||||
[K in SteamShortcutKey]: K extends "tags" ? string[] : string
|
||||
};
|
||||
|
||||
export interface SteamShortcutData extends Partial<BaseShortcutData> {
|
||||
// Mandatory and specific values
|
||||
AppName: string;
|
||||
Exe: string;
|
||||
StartDir: string;
|
||||
OpenVR?: "\x01" | "\x00";
|
||||
}
|
||||
|
||||
export class SteamShortcut {
|
||||
|
||||
public static parseShortcutsRawData(rawData: string): SteamShortcut[] {
|
||||
// Code taken from: https://developer.valvesoftware.com/wiki/Steam_Library_Shortcuts#Reading_the_shortcuts.vdf
|
||||
|
||||
const startIndex = rawData.indexOf("\u0000shortcuts\u0000");
|
||||
if (startIndex < 0) {
|
||||
return [];
|
||||
}
|
||||
const start = startIndex + "\u0000shortcuts\u0000".length;
|
||||
const end = rawData.lastIndexOf("\u0008\u0008");
|
||||
if (end < 0 || end <= start) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const shortcutsString = rawData.substring(start, end - start);
|
||||
|
||||
const result: SteamShortcutData[] = [];
|
||||
let currentShortcut: SteamShortcutData | null = null;
|
||||
let word = "";
|
||||
let key = "";
|
||||
let readingTags = false;
|
||||
let tagId = -1;
|
||||
|
||||
const shortcutKeysRegex = new RegExp(`[\u0001\u0002](${Object.values(SteamShortcutKey).join("|")})`, "i");
|
||||
|
||||
for (const c of shortcutsString) {
|
||||
|
||||
if (c === "\u0000") {
|
||||
if (word.endsWith(`\u0001${SteamShortcutKey.AppName}`)) {
|
||||
if (currentShortcut) {
|
||||
result.push(currentShortcut);
|
||||
}
|
||||
currentShortcut = {
|
||||
AppName: "",
|
||||
Exe: "",
|
||||
StartDir: "",
|
||||
icon: "",
|
||||
LaunchOptions: "",
|
||||
IsHidden: "\x00",
|
||||
};
|
||||
key = `\u0001${SteamShortcutKey.AppName}`;
|
||||
} else if (shortcutKeysRegex.test(word)) {
|
||||
key = word;
|
||||
} else if (word === SteamShortcutKey.tags) {
|
||||
readingTags = true;
|
||||
} else if (key !== "") {
|
||||
const currentKey = shortcutKeysRegex.exec(key).pop().replaceAll("\u0001", "").replaceAll("\u0002", "") as SteamShortcutKey;
|
||||
if (currentShortcut && currentKey && currentKey !== SteamShortcutKey.tags) {
|
||||
currentShortcut[currentKey] = word.replaceAll("\"", "") as string & ("\x01" | "\x00") // Make TS happy
|
||||
}
|
||||
key = "";
|
||||
} else if (readingTags) {
|
||||
if (word.startsWith("\u0001")) {
|
||||
tagId = parseInt(word.substring(1), 10);
|
||||
} else if (tagId >= 0 && currentShortcut) {
|
||||
currentShortcut.tags.push(word);
|
||||
tagId = -1;
|
||||
} else {
|
||||
readingTags = false;
|
||||
}
|
||||
}
|
||||
word = "";
|
||||
} else {
|
||||
word += c;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentShortcut) {
|
||||
result.push(currentShortcut);
|
||||
}
|
||||
|
||||
return result.map(shortcutData => new SteamShortcut(shortcutData));
|
||||
}
|
||||
|
||||
public static getShortcutsString(shortcuts: SteamShortcut[]): string {
|
||||
let shortcutsString = "\u0000shortcuts\u0000";
|
||||
for (let i = 0; i < shortcuts.length; i++) {
|
||||
const shortcut = shortcuts[i];
|
||||
if(!shortcut.data?.AppName || !shortcut.data?.Exe || !shortcut.data?.StartDir) {
|
||||
continue;
|
||||
}
|
||||
shortcutsString += `\u0000${i}\u0000`;
|
||||
shortcutsString += shortcut.getStringBytes();
|
||||
}
|
||||
shortcutsString += "\u0008\u0008";
|
||||
return shortcutsString;
|
||||
}
|
||||
|
||||
private data: SteamShortcutData;
|
||||
|
||||
public constructor(shortcutData: SteamShortcutData) {
|
||||
this.data = shortcutData;
|
||||
}
|
||||
|
||||
public getStringBytes(): string {
|
||||
|
||||
const isHidden = this.data?.IsHidden === "\x01";
|
||||
const allowDesktopConfig = this.data?.AllowDesktopConfig === "\x01";
|
||||
const allowOverlay = this.data?.AllowDesktopConfig === "\x01";
|
||||
const openVR = this.data?.OpenVR === "\x01";
|
||||
const devkit = this.data?.Devkit === "\x01";
|
||||
|
||||
let strShortcut = "";
|
||||
|
||||
strShortcut += `\x02appid\x00${this.data?.appid || "\x00\x00\x00"}\x00`;
|
||||
strShortcut += `\x01AppName\x00${this.data?.AppName ?? ""}\x00`;
|
||||
strShortcut += `\x01Exe\x00\"${this.data?.Exe ?? ""}\"\x00`;
|
||||
strShortcut += `\x01StartDir\x00\"${this.data?.StartDir ?? ""}\"\x00`;
|
||||
strShortcut += `\x01icon\x00${this.data?.icon ?? ""}\x00`;
|
||||
strShortcut += `\x01ShortcutPath\x00\x00`;
|
||||
strShortcut += `\x01LaunchOptions\x00${this.data?.LaunchOptions ?? ""}\x00`;
|
||||
strShortcut += `\x02IsHidden\x00${isHidden ? "\x01" : "\x00"}\x00\x00\x00`;
|
||||
strShortcut += `\x02AllowDesktopConfig\x00${allowDesktopConfig ? "\x01" : "\x00"}\x00\x00\x00`;
|
||||
strShortcut += `\x02AllowOverlay\x00${allowOverlay ? "\x01" : "\x00"}\x00\x00\x00`;
|
||||
strShortcut += `\x02OpenVR\x00${openVR ? "\x01" : "\x00"}\x00\x00\x00`;
|
||||
strShortcut += `\x02Devkit\x00${devkit ? "\x01" : "\x00"}\x00\x00\x00`;
|
||||
strShortcut += `\x01DevkitGameID\x00${this.data?.DevkitGameID ?? ""}\x00`;
|
||||
strShortcut += `\x02DevkitOverrideAppID\x00\x00\x00\x00\x00`;
|
||||
strShortcut += `\x02LastPlayTime\x00\x00\x00\x00\x00`;
|
||||
strShortcut += `\x00tags\x00${""}\x08\x08`;
|
||||
|
||||
return strShortcut;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user