mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
[feature-274] Add backup inforamtion modal before launch oculus version
This commit is contained in:
@@ -217,6 +217,11 @@ export async function ensurePathNotAlreadyExist(path: string): Promise<string> {
|
||||
return destPath;
|
||||
}
|
||||
|
||||
export async function isJunction(path: string): Promise<boolean>{
|
||||
const [stats, lstats] = await Promise.all([stat(path), lstat(path)]);
|
||||
return lstats.isSymbolicLink() && stats.isDirectory();
|
||||
}
|
||||
|
||||
export interface Progression<T = unknown> {
|
||||
total: number;
|
||||
current: number;
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { Observable, ReplaySubject, catchError, lastValueFrom, of, take, timeout } from "rxjs";
|
||||
import { StoreLauncherInterface } from "./store-launcher.interface";
|
||||
import { BSLaunchError, BSLaunchErrorData, BSLaunchEventData, LaunchOption } from "../../../shared/models/bs-launch";
|
||||
import { BSLaunchError, BSLaunchErrorData, BSLaunchEvent, BSLaunchEventData, LaunchOption } from "../../../shared/models/bs-launch";
|
||||
import { OculusService } from "../oculus.service";
|
||||
import { BS_EXECUTABLE, OCULUS_BS_BACKUP_DIR, OCULUS_BS_DIR } from "../../constants";
|
||||
import path from "path";
|
||||
import log from "electron-log";
|
||||
import { sToMs } from "../../../shared/helpers/time.helpers";
|
||||
import { pathExists, readdir, rename, stat, symlink, unlink } from "fs-extra";
|
||||
import { lstat, pathExists, readdir, rename, stat, symlink, unlink } from "fs-extra";
|
||||
import { AbstractLauncherService } from "./abstract-launcher.service";
|
||||
import { taskRunning } from "../../helpers/os.helpers";
|
||||
import { isJunction } from "../../helpers/fs.helpers";
|
||||
|
||||
export class OculusLauncherService extends AbstractLauncherService implements StoreLauncherInterface {
|
||||
|
||||
@@ -48,16 +49,11 @@ export class OculusLauncherService extends AbstractLauncherService implements St
|
||||
}
|
||||
|
||||
const libContents = await readdir(oculusLibPath, { withFileTypes: true});
|
||||
const symlinks = libContents.filter(dirent => dirent.isSymbolicLink());
|
||||
|
||||
const dirSymlinks = (await Promise.all(symlinks.map(async symlink => {
|
||||
const symlinkPath = path.join(oculusLibPath, symlink.name);
|
||||
const symlinkStats = await stat(symlinkPath);
|
||||
if(!symlinkStats.isDirectory()){ return null; }
|
||||
return symlink;
|
||||
const junctions = (await Promise.all(libContents.map(async dirent => {
|
||||
return (await isJunction(path.join(oculusLibPath, dirent.name))) ? dirent : null;
|
||||
}))).filter(Boolean);
|
||||
|
||||
const bsSymlinks = dirSymlinks.filter(dirent => dirent.name.startsWith(OCULUS_BS_DIR));
|
||||
const bsSymlinks = junctions.filter(dirent => dirent.name.startsWith(OCULUS_BS_DIR));
|
||||
|
||||
// get only symlinks created by BSM (with metadata.config)
|
||||
const bsmSymlinks = (await Promise.all(bsSymlinks.map(async symlink => {
|
||||
@@ -90,6 +86,31 @@ export class OculusLauncherService extends AbstractLauncherService implements St
|
||||
|
||||
// TODO : Convert all errors to CustomError
|
||||
public launch(launchOptions: LaunchOption): Observable<BSLaunchEventData> {
|
||||
|
||||
const prepareOriginalVersion: () => Promise<string> = async () => {
|
||||
await this.restoreOriginalBeatSaber();
|
||||
return this.oculus.getGameFolder(OCULUS_BS_DIR);
|
||||
}
|
||||
|
||||
const prepareDowngradedVersion: () => Promise<string> = async () => {
|
||||
|
||||
const oculusLib = await lastValueFrom(this.oculusLib$.pipe(take(1), timeout(sToMs(30)), catchError(() => of(null))));
|
||||
|
||||
if(!oculusLib){
|
||||
throw new Error("No Oculus library found");
|
||||
}
|
||||
|
||||
// Backup original Beat Saber folder
|
||||
await this.backupOriginalBeatSaber();
|
||||
|
||||
// Create symlink in the oculus library from the BSM BS version
|
||||
const symlinkTarget = await this.localVersions.getInstalledVersionPath(launchOptions.version);
|
||||
const symlinkPath = path.join(oculusLib, OCULUS_BS_DIR);
|
||||
await symlink(symlinkTarget, symlinkPath, "junction");
|
||||
|
||||
return symlinkPath;
|
||||
}
|
||||
|
||||
return new Observable<BSLaunchEventData>(obs => {
|
||||
(async () => {
|
||||
|
||||
@@ -98,26 +119,19 @@ export class OculusLauncherService extends AbstractLauncherService implements St
|
||||
if(bsRunning){
|
||||
throw ({type: BSLaunchError.BS_ALREADY_RUNNING, data: bsRunning}) as BSLaunchErrorData;
|
||||
}
|
||||
|
||||
const oculusLib = await lastValueFrom(this.oculusLib$.pipe(take(1), timeout(sToMs(30)), catchError(() => of(null))));
|
||||
|
||||
if(!oculusLib){
|
||||
throw new Error("No Oculus library found");
|
||||
}
|
||||
|
||||
|
||||
// Remove previously symlinks created by BSM
|
||||
await this.deleteBsSymlinks().catch(log.error);
|
||||
|
||||
// Backup original Beat Saber folder
|
||||
await this.backupOriginalBeatSaber();
|
||||
const bsPath = await (launchOptions.version.oculus ? prepareOriginalVersion() : prepareDowngradedVersion());
|
||||
|
||||
// Create symlink in the oculus library from the BSM BS version
|
||||
const symlinkTarget = await this.localVersions.getInstalledVersionPath(launchOptions.version);
|
||||
const symlinkPath = path.join(oculusLib, OCULUS_BS_DIR);
|
||||
await symlink(symlinkTarget, symlinkPath, "junction");
|
||||
if(!bsPath){
|
||||
throw ({type: BSLaunchError.BS_NOT_FOUND}) as BSLaunchErrorData;
|
||||
}
|
||||
|
||||
// Launch Beat Saber
|
||||
const exePath = path.join(symlinkPath, "Beat Saber.exe");
|
||||
const exePath = path.join(bsPath, "Beat Saber.exe");
|
||||
obs.next({type: BSLaunchEvent.BS_LAUNCHING});
|
||||
return this.launchBs(exePath, this.buildBsLaunchArgs(launchOptions)).catch(err => {
|
||||
throw ({type: BSLaunchError.BS_EXIT_ERROR, data: err}) as BSLaunchErrorData;
|
||||
});
|
||||
|
||||
@@ -181,7 +181,7 @@ export class BSLocalVersionService {
|
||||
*/
|
||||
public async getVersionPath(version: BSVersion): Promise<string>{
|
||||
if(version.steam){ return this.steamService.getGameFolder(BS_APP_ID, "Beat Saber") }
|
||||
if(version.oculus){ return this.oculusService.getGameFolder(OCULUS_BS_DIR); }
|
||||
if(version.oculus){ return this.oculusService.tryGetGameFolder([OCULUS_BS_DIR, OCULUS_BS_BACKUP_DIR]); }
|
||||
|
||||
return path.join(
|
||||
await this.installLocationService.versionsDirectory(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import regedit from "regedit";
|
||||
import path from "path";
|
||||
import { pathExist } from "../helpers/fs.helpers";
|
||||
import { isJunction, pathExist } from "../helpers/fs.helpers";
|
||||
import log from "electron-log";
|
||||
|
||||
export class OculusService {
|
||||
@@ -67,7 +67,7 @@ export class OculusService {
|
||||
for (const { path: libPath } of libsFolders) {
|
||||
const gameFullPath = path.join(libPath, rootLibDir, gameFolder);
|
||||
if (await pathExist(gameFullPath)) {
|
||||
return gameFullPath;
|
||||
return (await isJunction(gameFullPath)) ? null : gameFullPath;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { ModalComponent, ModalExitCode } from "renderer/services/modale.service"
|
||||
import BeatConflict from "../../../../../assets/images/apngs/beat-conflict.png";
|
||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import { BsmImage } from "renderer/components/shared/bsm-image.component";
|
||||
import { BsmCheckbox } from "renderer/components/shared/bsm-checkbox.component";
|
||||
import { useState } from "react";
|
||||
|
||||
export const OriginalOculusVersionBackupModal: ModalComponent<boolean, void> = ({ resolver }) => {
|
||||
|
||||
const [dontShowAgain, setDontShowAgain] = useState(false);
|
||||
|
||||
const submit = () => {
|
||||
resolver({ exitCode: ModalExitCode.COMPLETED, data: dontShowAgain });
|
||||
}
|
||||
|
||||
// TODO : Translate
|
||||
|
||||
return (
|
||||
<form className="max-w-[450px] text-gray-800 dark:text-gray-200">
|
||||
<h1 className="text-3xl uppercase tracking-wide w-full text-center text-gray-800 dark:text-gray-200">Attention</h1>
|
||||
<BsmImage className="mx-auto h-20" image={BeatConflict} />
|
||||
|
||||
<p className="mb-4">Pour lancer cette version, le dossier d'installation de Beat Saber se trouvant dans votre librairie Oculus va être renommé.</p>
|
||||
<p className="mb-4">Au besoin, vous pourrez le restaurer depuis BSManager en vous rendant dans les options de la version originale et en cliquant sur "Restaurer le dossier"</p>
|
||||
<p className="text-sm italic mb-4">Astuce : Vous pourrez lancer cette version directement depuis Oculus, tant que la version originale n'est pas restaurée.</p>
|
||||
|
||||
<div className="flex flex-row justify-start items-center gap-1.5 my-4" >
|
||||
<BsmCheckbox className="relative z-[1] w-6 aspect-square" checked={dontShowAgain} onChange={setDontShowAgain} />
|
||||
<span>Ne plus me rappler</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-flow-col grid-cols-2 gap-4">
|
||||
<BsmButton
|
||||
typeColor="cancel"
|
||||
className="rounded-md transition-all h-10 flex items-center justify-center"
|
||||
onClick={() => {
|
||||
resolver({ exitCode: ModalExitCode.CANCELED });
|
||||
}}
|
||||
withBar={false}
|
||||
text="misc.cancel"
|
||||
/>
|
||||
<BsmButton
|
||||
typeColor="primary"
|
||||
className="rounded-md transition-all h-10 flex items-center justify-center"
|
||||
onClick={submit}
|
||||
withBar={false}
|
||||
text="J'ai compris"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import { useState } from "react";
|
||||
import { distinctUntilChanged, map, of, Subscription, switchMap } from "rxjs";
|
||||
import { distinctUntilChanged, lastValueFrom, map, of, Subscription, switchMap } from "rxjs";
|
||||
import { BSLauncherService, LaunchMods } from "renderer/services/bs-launcher.service";
|
||||
import { ConfigurationService } from "renderer/services/configuration.service";
|
||||
import { BSUninstallerService } from "renderer/services/bs-uninstaller.service";
|
||||
@@ -56,12 +56,13 @@ export function BsVersionItem(props: { version: BSVersion }) {
|
||||
};
|
||||
|
||||
const handleDoubleClick = () => {
|
||||
launcherService.launch({
|
||||
const launch$ = launcherService.launch({
|
||||
version: state,
|
||||
oculus: !!configService.get<boolean>(LaunchMods.OCULUS_MOD),
|
||||
desktop: !!configService.get<boolean>(LaunchMods.DESKTOP_MOD),
|
||||
debug: !!configService.get<boolean>(LaunchMods.DEBUG_MOD),
|
||||
});
|
||||
return lastValueFrom(launch$).catch(() => {});
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
|
||||
@@ -11,6 +11,7 @@ import BSLogo from "../../../../../../assets/images/apngs/bs-logo.png";
|
||||
import { BsmImage } from "renderer/components/shared/bsm-image.component";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { BsStore } from "shared/models/bs-store.enum";
|
||||
import { lastValueFrom } from "rxjs";
|
||||
|
||||
type Props = { version: BSVersion };
|
||||
|
||||
@@ -58,13 +59,15 @@ export function LaunchSlide({ version }: Props) {
|
||||
.filter(arg => arg.length > 0)
|
||||
: undefined;
|
||||
|
||||
bsLauncherService.launch({
|
||||
const launch$ = bsLauncherService.launch({
|
||||
version,
|
||||
oculus: version.oculus ? false : oculusMode,
|
||||
desktop: desktopMode,
|
||||
debug: debugMode,
|
||||
additionalArgs
|
||||
});
|
||||
|
||||
return lastValueFrom(launch$).catch(() => {});
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,9 +2,12 @@ import { LaunchOption, BSLaunchEvent, BSLaunchWarning, BSLaunchEventData, BSLaun
|
||||
import { BSVersion } from 'shared/bs-version.interface';
|
||||
import { IpcService } from "./ipc.service";
|
||||
import { NotificationService } from "./notification.service";
|
||||
import { BehaviorSubject, Observable, filter, lastValueFrom } from "rxjs";
|
||||
import { BehaviorSubject, Observable, lastValueFrom, tap } from "rxjs";
|
||||
import { ConfigurationService } from "./configuration.service";
|
||||
import { ThemeService } from "./theme.service";
|
||||
import { BsStore } from "shared/models/bs-store.enum";
|
||||
import { ModalExitCode, ModalService } from "./modale.service";
|
||||
import { OriginalOculusVersionBackupModal } from "renderer/components/modal/modal-types/original-oculus-version-backup.modal";
|
||||
|
||||
export class BSLauncherService {
|
||||
private static instance: BSLauncherService;
|
||||
@@ -13,6 +16,7 @@ export class BSLauncherService {
|
||||
private readonly notificationService: NotificationService;
|
||||
private readonly config: ConfigurationService;
|
||||
private readonly theme: ThemeService;
|
||||
private readonly modals: ModalService;
|
||||
|
||||
public readonly versionRunning$: BehaviorSubject<BSVersion> = new BehaviorSubject(null);
|
||||
|
||||
@@ -26,6 +30,15 @@ export class BSLauncherService {
|
||||
this.notificationService = NotificationService.getInstance();
|
||||
this.config = ConfigurationService.getInstance();
|
||||
this.theme = ThemeService.getInstance();
|
||||
this.modals = ModalService.getInstance();
|
||||
}
|
||||
|
||||
private notRewindBackupOculus(): boolean{
|
||||
return this.config.get<boolean>("not-rewind-backup-oculus");
|
||||
}
|
||||
|
||||
private setNotRewindBackupOculus(value: boolean): void{
|
||||
this.config.set("not-rewind-backup-oculus", value);
|
||||
}
|
||||
|
||||
public getLaunchOptions(version: BSVersion): LaunchOption{
|
||||
@@ -37,35 +50,51 @@ export class BSLauncherService {
|
||||
additionalArgs: (this.config.get<string>("additionnal-args") || "").split(";").map(arg => arg.trim()).filter(arg => arg.length > 0)
|
||||
}
|
||||
}
|
||||
|
||||
public doLaunch(launchOptions: LaunchOption): Observable<BSLaunchEventData>{
|
||||
return this.ipcService.sendV2<BSLaunchEventData, LaunchOption>("bs-launch.launch", {args: launchOptions});
|
||||
}
|
||||
|
||||
public launch(launchOptions: LaunchOption): Observable<BSLaunchEventData> {
|
||||
const launchState$ = this.doLaunch(launchOptions);
|
||||
private handleLaunchEvents(events$: Observable<BSLaunchEventData>): Observable<BSLaunchEventData>{
|
||||
const eventToFilter = [...Object.values(BSLaunchWarning), BSLaunchEvent.STEAM_LAUNCHED]
|
||||
|
||||
this.versionRunning$.next(launchOptions.version);
|
||||
|
||||
launchState$.pipe(filter(event => {
|
||||
const eventToFilter = [...Object.values(BSLaunchWarning), BSLaunchEvent.STEAM_LAUNCHED]
|
||||
return !eventToFilter.includes(event.type);
|
||||
})).subscribe({
|
||||
return events$.pipe(tap({
|
||||
next: event => {
|
||||
if(eventToFilter.includes(event.type)){ return; }
|
||||
this.notificationService.notifySuccess({title: `notifications.bs-launch.success.titles.${event.type}`, desc: `notifications.bs-launch.success.msg.${event.type}`});
|
||||
},
|
||||
error: (err: BSLaunchErrorData) => {
|
||||
error: (err: BSLaunchErrorData) => { // TODO : Convert all errors to CustomError
|
||||
if(!Object.values(BSLaunchError).includes(err.type)){
|
||||
this.notificationService.notifyError({title: "notifications.bs-launch.errors.titles.UNKNOWN_ERROR", desc: "notifications.bs-launch.errors.msg.UNKNOWN_ERROR"});
|
||||
} else {
|
||||
this.notificationService.notifyError({title: `notifications.bs-launch.errors.titles.${err.type}`, desc: `notifications.bs-launch.errors.msg.${err.type}`})
|
||||
}
|
||||
}
|
||||
}).add(() => {
|
||||
this.versionRunning$.next(null);
|
||||
}))
|
||||
}
|
||||
|
||||
public doLaunch(launchOptions: LaunchOption): Observable<BSLaunchEventData>{
|
||||
return this.ipcService.sendV2<BSLaunchEventData, LaunchOption>("bs-launch.launch", {args: launchOptions});
|
||||
}
|
||||
|
||||
public launch(launchOptions: LaunchOption): Observable<BSLaunchEventData> {
|
||||
|
||||
return new Observable<BSLaunchEventData>(obs => {
|
||||
(async () => {
|
||||
|
||||
if(launchOptions.version.metadata?.store === BsStore.OCULUS && !this.notRewindBackupOculus()){
|
||||
const { exitCode, data: notRewind } = await this.modals.openModal(OriginalOculusVersionBackupModal);
|
||||
if(exitCode !== ModalExitCode.COMPLETED){ return; }
|
||||
this.setNotRewindBackupOculus(notRewind);
|
||||
}
|
||||
|
||||
const launch$ = this.handleLaunchEvents(this.doLaunch(launchOptions));
|
||||
|
||||
await lastValueFrom(launch$);
|
||||
|
||||
})().then(() => {
|
||||
obs.complete();
|
||||
}).catch(err => {
|
||||
obs.error(err);
|
||||
})
|
||||
});
|
||||
|
||||
return launchState$;
|
||||
}
|
||||
|
||||
public createLaunchShortcut(launchOptions: LaunchOption): Observable<void>{
|
||||
|
||||
Reference in New Issue
Block a user