mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
Merge pull request #320 from Zagrios/feature/liv-shortcuts
[feature] Liv shortcuts
This commit is contained in:
+2
-1
@@ -19,6 +19,7 @@ import { LocalModelsManagerService } from "./services/additional-content/local-m
|
||||
import { APP_NAME } from "./constants";
|
||||
import { BSLauncherService } from "./services/bs-launcher.service";
|
||||
import { IpcRequest } from "shared/models/ipc";
|
||||
import { LivShortcut } from "./services/liv/liv-shortcut.service";
|
||||
|
||||
const isDebug = process.env.NODE_ENV === "development" || process.env.DEBUG_PROD === "true";
|
||||
|
||||
@@ -63,7 +64,7 @@ const initServicesMustBeInitialized = () => {
|
||||
LocalMapsManagerService.getInstance();
|
||||
LocalPlaylistsManagerService.getInstance();
|
||||
LocalModelsManagerService.getInstance();
|
||||
|
||||
LivShortcut.getInstance();
|
||||
BSLauncherService.getInstance();
|
||||
}
|
||||
|
||||
|
||||
@@ -50,8 +50,7 @@ export class BSLauncherService {
|
||||
|
||||
this.bsmProtocolService.on("launch", link => {
|
||||
log.info("Launch from bsm protocol", link.toString());
|
||||
const shortcutParams = objectFromEntries(link.searchParams.entries()) as ShortcutParams;
|
||||
this.openShortcutLaunchWindow(shortcutParams).catch(log.error);
|
||||
this.openShortcutLaunchWindow(this.shortcutLinkToShortcutParams(link)).catch(log.error);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -179,6 +178,18 @@ export class BSLauncherService {
|
||||
})});
|
||||
}
|
||||
|
||||
public shortcutLinkToShortcutParams(shortcutLink: string|URL): ShortcutParams{
|
||||
if(typeof shortcutLink === "string"){
|
||||
shortcutLink = new URL(shortcutLink);
|
||||
}
|
||||
|
||||
return objectFromEntries(shortcutLink.searchParams.entries()) as ShortcutParams;
|
||||
}
|
||||
|
||||
public shortcutLinkToLaunchOptions(shortcutLink: string|URL): LaunchOption{
|
||||
return this.shortcutParamsToLaunchOption(this.shortcutLinkToShortcutParams(shortcutLink));
|
||||
}
|
||||
|
||||
private shortcutParamsToLaunchOption(params: ShortcutParams): LaunchOption{
|
||||
const res: LaunchOption = {
|
||||
version: {
|
||||
@@ -263,9 +274,13 @@ export class BSLauncherService {
|
||||
return iconPath;
|
||||
}
|
||||
|
||||
public async createLaunchShortcut(launchOptions: LaunchOption): Promise<boolean>{
|
||||
public createLaunchLink(launchOptions: LaunchOption): string{
|
||||
const shortcutParams = this.launchOptionToShortcutParams(launchOptions);
|
||||
const shortcutUrl = this.bsmProtocolService.buildLink("launch", shortcutParams).toString();
|
||||
return this.bsmProtocolService.buildLink("launch", shortcutParams).toString();
|
||||
}
|
||||
|
||||
public async createLaunchShortcut(launchOptions: LaunchOption): 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");
|
||||
|
||||
@@ -15,6 +15,7 @@ import { copyDirectoryWithJunctions, deleteFolder, getFoldersInFolder, pathExist
|
||||
import { FolderLinkerService } from "./folder-linker.service";
|
||||
import { ReadStream, createReadStream } from "fs-extra";
|
||||
import readline from "readline";
|
||||
import { Observable, Subject } from "rxjs";
|
||||
|
||||
export class BSLocalVersionService {
|
||||
private static instance: BSLocalVersionService;
|
||||
@@ -27,6 +28,7 @@ export class BSLocalVersionService {
|
||||
private readonly remoteVersionService: BSVersionLibService;
|
||||
private readonly configService: ConfigurationService;
|
||||
private readonly linker: FolderLinkerService;
|
||||
private readonly _loadedVersions$: Subject<BSVersion[]>;
|
||||
|
||||
public static getInstance(): BSLocalVersionService {
|
||||
if (!BSLocalVersionService.instance) {
|
||||
@@ -42,6 +44,8 @@ export class BSLocalVersionService {
|
||||
this.remoteVersionService = BSVersionLibService.getInstance();
|
||||
this.configService = ConfigurationService.getInstance();
|
||||
this.linker = FolderLinkerService.getInstance();
|
||||
|
||||
this._loadedVersions$ = new Subject<BSVersion[]>();
|
||||
}
|
||||
|
||||
private async getVersionFromGlobalGameManagerFile(versionFilePath: string): Promise<BSVersion> {
|
||||
@@ -233,6 +237,8 @@ export class BSLocalVersionService {
|
||||
|
||||
this.setCustomVersions(versions.filter(v => !!v.color));
|
||||
|
||||
this._loadedVersions$.next(versions);
|
||||
|
||||
return versions;
|
||||
}
|
||||
|
||||
@@ -310,4 +316,8 @@ export class BSLocalVersionService {
|
||||
|
||||
return (await linkedFolder).filter(folder => folder);
|
||||
}
|
||||
|
||||
public get loadedVersions$(): Observable<BSVersion[]>{
|
||||
return this._loadedVersions$.asObservable();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Subject, Subscription, filter } from "rxjs";
|
||||
import { DeepLinkService } from "./deep-link.service";
|
||||
import { URL } from "url";
|
||||
import { buildUrl, isValidUrl } from "../../shared/helpers/url.helpers";
|
||||
|
||||
export class BsmProtocolService {
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { BSLauncherService } from "../bs-launcher.service";
|
||||
import { LivEntry, LivService } from "./liv.service";
|
||||
import { LaunchOption } from "shared/models/bs-launch";
|
||||
import { app } from "electron";
|
||||
import { BSLocalVersionService } from "../bs-local-version.service";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { debounceTime, distinctUntilChanged, map } from "rxjs";
|
||||
import equal from "fast-deep-equal";
|
||||
import sanitize from "sanitize-filename";
|
||||
import { tryit } from "../../../shared/helpers/error.helpers";
|
||||
import log from "electron-log"
|
||||
|
||||
export class LivShortcut {
|
||||
|
||||
private static instance: LivShortcut;
|
||||
|
||||
public static getInstance(): LivShortcut {
|
||||
if (!LivShortcut.instance) {
|
||||
LivShortcut.instance = new LivShortcut();
|
||||
}
|
||||
return LivShortcut.instance;
|
||||
}
|
||||
|
||||
private readonly LIV_ID_PREFIX = "BSManager";
|
||||
|
||||
private readonly liv: LivService;
|
||||
private readonly launcher: BSLauncherService;
|
||||
private readonly versionManager: BSLocalVersionService;
|
||||
|
||||
private constructor() {
|
||||
this.liv = LivService.getInstance();
|
||||
this.launcher = BSLauncherService.getInstance();
|
||||
this.versionManager = BSLocalVersionService.getInstance();
|
||||
|
||||
this.liv.isLivInstalled().then(installed => {
|
||||
if(!installed){ return; }
|
||||
|
||||
this.versionManager.loadedVersions$.pipe(debounceTime(1000), distinctUntilChanged((prev, curr) => equal(prev, curr)), map(versions => versions.filter(v => !v.steam))).subscribe(versions => {
|
||||
(async () => {
|
||||
const clearedLaunchOptions = (await this.clearLivShortcuts().catch(() => [] as LivEntry[]))?.map(entry => {
|
||||
return tryit(() => this.launcher.shortcutLinkToLaunchOptions(entry.arguments)).result;
|
||||
});
|
||||
|
||||
versions.forEach(version => this.createLivShortcut({
|
||||
...(clearedLaunchOptions?.find(launchOpt => launchOpt.version.ino === version.ino) ?? {}),
|
||||
version,
|
||||
skipAlreadyRunning: true
|
||||
}));
|
||||
})().catch(err => log.error("Error while creating LIV shortcuts", err));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private buildShortcutId(version: BSVersion): string {
|
||||
const name = (() => {
|
||||
if(version.steam){ return "Steam"; }
|
||||
if(version.oculus){ return "Oculus"; }
|
||||
return version.name;
|
||||
})();
|
||||
return sanitize([this.LIV_ID_PREFIX, name, version.BSVersion].filter(Boolean).join("-").replaceAll(".", "-").replaceAll(" ", "-"));
|
||||
}
|
||||
|
||||
private buildShortcutName(version: BSVersion): string {
|
||||
const name = (() => {
|
||||
if(version.steam){ return "Steam"; }
|
||||
if(version.oculus){ return "Oculus"; }
|
||||
return version.name;
|
||||
})();
|
||||
return ["Beat Saber", name, `v${version.BSVersion}`].filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
private async buildLivEntry(launchOptions: LaunchOption): Promise<LivEntry> {
|
||||
return {
|
||||
id: this.buildShortcutId(launchOptions.version),
|
||||
name: this.buildShortcutName(launchOptions.version),
|
||||
installPath: await this.versionManager.getInstalledVersionPath(launchOptions.version),
|
||||
executable: app.getPath("exe"),
|
||||
arguments: this.launcher.createLaunchLink(launchOptions)
|
||||
};
|
||||
}
|
||||
|
||||
private createLivShortcut(launchOptions: LaunchOption): Promise<void>{
|
||||
return this.buildLivEntry(launchOptions).then(entry => this.liv.createLivShortcut(entry));
|
||||
}
|
||||
|
||||
private async clearLivShortcuts(): Promise<LivEntry[]>{
|
||||
const bsmShortcuts = (await this.liv.getLivShortcuts()).reduce((acc, shortcut) => {
|
||||
if(shortcut.id?.startsWith(this.LIV_ID_PREFIX)){
|
||||
acc.push(shortcut);
|
||||
}
|
||||
return acc;
|
||||
}, [] as LivEntry[]);
|
||||
|
||||
return this.liv.deleteLivShortcuts(bsmShortcuts.map(shortcut => shortcut.id)).then(() => bsmShortcuts);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { execOnOs } from "../../helpers/env.helpers";
|
||||
import path from "path";
|
||||
import regedit from "regedit";
|
||||
|
||||
export class LivService {
|
||||
|
||||
private static instance: LivService;
|
||||
|
||||
public static getInstance(): LivService {
|
||||
if (!LivService.instance) {
|
||||
LivService.instance = new LivService();
|
||||
}
|
||||
return LivService.instance;
|
||||
}
|
||||
|
||||
private readonly livRegeditKey: string = path.join("HKCU", "SOFTWARE", "LIV.App");
|
||||
private readonly livExternalAppsRegeditKey: string = path.join(this.livRegeditKey, "ExternalApplications");
|
||||
|
||||
private constructor() {
|
||||
|
||||
}
|
||||
|
||||
public isLivInstalled(): Promise<boolean> {
|
||||
return execOnOs({
|
||||
win32: async () => {
|
||||
const regRes = await regedit.promisified.list([this.livRegeditKey]).then(res => res[this.livRegeditKey]);
|
||||
return regRes?.exists;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async createLivShortcut(entry: LivEntry): Promise<void> {
|
||||
return execOnOs({
|
||||
win32: async () => {
|
||||
const livExternalAppRegeditKey = path.join(this.livExternalAppsRegeditKey, entry.id);
|
||||
await regedit.promisified.createKey([livExternalAppRegeditKey]);
|
||||
await regedit.promisified.putValue({
|
||||
[livExternalAppRegeditKey]: {
|
||||
"InstallPath": {
|
||||
value: entry.installPath,
|
||||
type: "REG_SZ"
|
||||
},
|
||||
"Executable": {
|
||||
value: entry.executable,
|
||||
type: "REG_SZ"
|
||||
},
|
||||
"Arguments": {
|
||||
value: entry.arguments,
|
||||
type: "REG_SZ"
|
||||
},
|
||||
"Name": {
|
||||
value: entry.name,
|
||||
type: "REG_SZ"
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async deleteLivShortcuts(ids: string[]): Promise<void> {
|
||||
return execOnOs({
|
||||
win32: async () => {
|
||||
const shotcutsKeys = ids.map(id => path.join(this.livExternalAppsRegeditKey, id));
|
||||
return regedit.promisified.deleteKey(shotcutsKeys);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public getLivShortcuts(): Promise<LivEntry[]> {
|
||||
return execOnOs({
|
||||
win32: async () => {
|
||||
const regRes = await regedit.promisified.list([this.livExternalAppsRegeditKey]).then(res => res[this.livExternalAppsRegeditKey]);
|
||||
|
||||
if(!regRes.exists){
|
||||
return [];
|
||||
}
|
||||
|
||||
const promises = regRes.keys.map(async key => {
|
||||
const shortcutKey = path.join(this.livExternalAppsRegeditKey, key);
|
||||
const entries = await regedit.promisified.list([shortcutKey]).then(res => res[shortcutKey]);
|
||||
|
||||
if(!entries.exists){
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
id: key,
|
||||
name: entries.values.Name.value,
|
||||
installPath: entries.values.InstallPath.value,
|
||||
executable: entries.values.Executable.value,
|
||||
arguments: entries.values.Arguments.value
|
||||
} as LivEntry;
|
||||
});
|
||||
|
||||
return Promise.all(promises).then(entries => entries.filter(Boolean));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export interface LivEntry {
|
||||
id: string,
|
||||
name: string,
|
||||
installPath: string,
|
||||
executable: string,
|
||||
arguments: string
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export function tryit<Return>(func: () => Return): {error: Error, result: Return} {
|
||||
try {
|
||||
return { error: null, result: func() };
|
||||
} catch (err) {
|
||||
return { error: err instanceof Error ? err : new Error(`${err}`), result: null }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user