[feature-274] Refactoring downloaders service + handle importing Oculus version + logout from steam and oculus in settings + other things

This commit is contained in:
MathieuG-P
2023-10-27 02:00:24 +02:00
parent e9780a60f4
commit 0a1e1efcd9
33 changed files with 794 additions and 597 deletions
+3 -3
View File
@@ -110,9 +110,9 @@
}
},
"settings": {
"steam": {
"title": "Steam",
"description": "Wenn Du dich von Steam abmeldest, musst Du dich erneut anmelden, um eine neue Version von Beat Saber herunterzuladen.",
"steam-and-oculus": {
"title": "Steam & Oculus",
"description": "Abmelden ermöglicht es dir, beim nächsten Beat Saber Download den Account zu wechseln.",
"logout": "Abmelden"
},
"appearance": {
+3 -3
View File
@@ -110,9 +110,9 @@
}
},
"settings": {
"steam": {
"title": "Steam",
"description": "If you log out of Steam, you will have to log back in to download new versions of Beat Saber.",
"steam-and-oculus": {
"title": "Steam & Oculus",
"description": "Logging out will allow you to switch accounts on the next Beat Saber download.",
"logout": "Log out"
},
"appearance": {
+3 -3
View File
@@ -109,9 +109,9 @@
}
},
"settings": {
"steam": {
"title": "Steam",
"description": "Si desconectas tu cuenta de Steam, tendrás que volver a conectarla para descargar una nueva versión de Beat Saber.",
"steam-and-oculus": {
"title": "Steam & Oculus",
"description": "Desconectarte te permitirá cambiar de cuenta en la próxima descarga de Beat Saber.",
"logout": "Cerrar sesión"
},
"appearance": {
+3 -3
View File
@@ -109,9 +109,9 @@
}
},
"settings": {
"steam": {
"title": "Steam",
"description": "Si tu te déconnectes de Steam, tu devras te reconnecter pour télécharger une nouvelle version de Beat Saber.",
"steam-and-oculus": {
"title": "Steam & Oculus",
"description": "Te déconnecter te permettra de changer de compte au prochain téléchargement de Beat Saber.",
"logout": "Déconnexion"
},
"appearance": {
+3 -3
View File
@@ -110,9 +110,9 @@
}
},
"settings": {
"steam": {
"title": "Steam",
"description": "Если вы выйдете из Steam, вам придётся снова войти, если вы хотите скачать другие версии Beat Saber.",
"steam-and-oculus": {
"title": "Steam & Oculus",
"description": "Выход позволит вам сменить учетную запись при следующей загрузке Beat Saber.",
"logout": "Выйти"
},
"appearance": {
+55 -2
View File
@@ -1,7 +1,7 @@
import { CopyOptions, copy, createReadStream, ensureDir, move, symlink } from "fs-extra";
import { CopyOptions, copy, createReadStream, ensureDir, move, stat, symlink } from "fs-extra";
import { access, mkdir, rm, readdir, unlink, lstat, readlink } from "fs/promises";
import path from "path";
import { Observable } from "rxjs";
import { Observable, concatMap, from } from "rxjs";
import log from "electron-log";
import { BsmException } from "shared/models/bsm-exception.model";
import crypto from "crypto";
@@ -164,6 +164,59 @@ export function hashFile(filePath: string, algorithm = "sha256"): Promise<string
});
}
export async function dirSize(dirPath: string): Promise<number>{
const files = await readdir(dirPath, { withFileTypes: true });
const paths = files.map(async file => {
const fullPath = path.join(dirPath, file.name);
if (file.isDirectory()) return dirSize(fullPath);
if (file.isFile()) {
const { size } = await stat( fullPath );
return size;
}
return 0;
});
return (await Promise.all(paths)).flat(Infinity).reduce((acc, size ) => acc + size, 0);
}
export function rxCopy(src: string, dest: string, option?: CopyOptions): Observable<Progression> {
return from(dirSize(src)).pipe(
concatMap(totalSize => {
const progress: Progression = { current: 0, total: totalSize };
return new Observable<Progression>(sub => {
sub.next(progress);
copy(src, dest, {...option, filter: (src) => {
stat(src).then(stats => {
progress.current += stats.size;
sub.next(progress);
});
return true;
}})
.then(() => sub.complete()).catch(err => sub.error(err))
})
})
);
}
export async function ensurePathNotAlreadyExist(path: string): Promise<string> {
let destPath = path;
let folderExist = await pathExist(destPath);
let i = 0;
while (folderExist) {
i++;
destPath = `${path} (${i})`;
folderExist = await pathExist(destPath);
}
return destPath;
}
export interface Progression<T = unknown> {
total: number;
current: number;
@@ -1,51 +0,0 @@
import { BSInstallerService, DownloadInfo } from "../../services/bs-installer.service";
import { InstallationLocationService } from "../../services/installation-location.service";
import { IpcService } from "../../services/ipc.service";
import { from, of } from "rxjs";
const ipc = IpcService.getInstance();
ipc.on("is-dotnet-6-installed", (_, reply) => {
const installer = BSInstallerService.getInstance();
reply(from(installer.isDotNet6Installed()));
});
ipc.on("bs-download.installation-folder", (_, reply) => {
const installLocation = InstallationLocationService.getInstance();
reply(from(installLocation.installationDirectory()));
});
ipc.on<string>("bs-download.set-installation-folder", (req, reply) => {
const installerService = InstallationLocationService.getInstance();
reply(from(installerService.setInstallationDirectory(req.args)));
});
ipc.on<string>("bs-download.import-version", (req, reply) => {
const installer = BSInstallerService.getInstance();
reply(from(installer.importVersion(req.args)));
});
ipc.on<DownloadInfo>("auto-download-bs-version", (req, reply) => {
const bsInstaller = BSInstallerService.getInstance();
reply(bsInstaller.autoDownloadBsVersion(req.args));
});
ipc.on<DownloadInfo>("download-bs-version", (req, reply) => {
const bsInstaller = BSInstallerService.getInstance();
reply(bsInstaller.downloadBsVersion(req.args))
});
ipc.on<DownloadInfo>("download-bs-version-qr", (req, reply) => {
const bsInstaller = BSInstallerService.getInstance();
reply(bsInstaller.downloadBsVersionWithQRCode(req.args))
});
ipc.on("stop-download-bs-version", (_, reply) => {
const bsInstaller = BSInstallerService.getInstance();
reply(of(bsInstaller.stopDownload()));
});
ipc.on<string>("send-input-bs-download", (req, reply) => {
const bsInstaller = BSInstallerService.getInstance();
reply(of(bsInstaller.sendInput(req.args)));
});
@@ -1,15 +0,0 @@
import { BsOculusDownloaderService, OculusDownloadInfo } from "../../services/bs-oculus-downloader.service";
import { IpcService } from "../../services/ipc.service";
import { BSVersion } from "shared/bs-version.interface";
const ipc = IpcService.getInstance();
ipc.on<OculusDownloadInfo>("bs-oculus-download", async (req, reply) => {
const oculusDownloader = BsOculusDownloaderService.getInstance();
reply(oculusDownloader.downloadVersion(req.args));
});
ipc.on<BSVersion>("bs-oculus-auto-download", async (req, reply) => {
const oculusDownloader = BsOculusDownloaderService.getInstance();
reply(oculusDownloader.autoDownloadVersion(req.args));
});
@@ -0,0 +1,86 @@
import { BsOculusDownloaderService } from "../../services/bs-version-download/bs-oculus-downloader.service";
import { BsSteamDownloaderService, DownloadInfo, DownloadSteamInfo } from "../../services/bs-version-download/bs-steam-downloader.service";
import { InstallationLocationService } from "../../services/installation-location.service";
import { IpcService } from "../../services/ipc.service";
import { from, of } from "rxjs";
import { BSLocalVersionService, ImportVersionOptions } from "../../services/bs-local-version.service";
const ipc = IpcService.getInstance();
ipc.on<ImportVersionOptions>("import-version", (req, reply) => {
const versionManager = BSLocalVersionService.getInstance();
reply(versionManager.importVersion(req.args));
});
// #region Steam
ipc.on("is-dotnet-6-installed", (_, reply) => {
const installer = BsSteamDownloaderService.getInstance();
reply(from(installer.isDotNet6Installed()));
});
ipc.on("bs-download.installation-folder", (_, reply) => {
const installLocation = InstallationLocationService.getInstance();
reply(from(installLocation.installationDirectory()));
});
ipc.on<string>("bs-download.set-installation-folder", (req, reply) => {
const installerService = InstallationLocationService.getInstance();
reply(from(installerService.setInstallationDirectory(req.args)));
});
ipc.on<DownloadSteamInfo>("auto-download-bs-version", (req, reply) => {
const bsInstaller = BsSteamDownloaderService.getInstance();
reply(bsInstaller.autoDownloadBsVersion(req.args));
});
ipc.on<DownloadSteamInfo>("download-bs-version", (req, reply) => {
const bsInstaller = BsSteamDownloaderService.getInstance();
reply(bsInstaller.downloadBsVersion(req.args))
});
ipc.on<DownloadSteamInfo>("download-bs-version-qr", (req, reply) => {
const bsInstaller = BsSteamDownloaderService.getInstance();
reply(bsInstaller.downloadBsVersionWithQRCode(req.args))
});
ipc.on("stop-download-bs-version", (_, reply) => {
const bsInstaller = BsSteamDownloaderService.getInstance();
reply(of(bsInstaller.stopDownload()));
});
ipc.on<string>("send-input-bs-download", (req, reply) => {
const bsInstaller = BsSteamDownloaderService.getInstance();
reply(of(bsInstaller.sendInput(req.args)));
});
// #endregion
// #region Oculus
ipc.on<DownloadInfo>("bs-oculus-download", async (req, reply) => {
const oculusDownloader = BsOculusDownloaderService.getInstance();
reply(oculusDownloader.downloadVersion(req.args));
});
ipc.on<DownloadInfo>("bs-oculus-auto-download", async (req, reply) => {
const oculusDownloader = BsOculusDownloaderService.getInstance();
reply(oculusDownloader.autoDownloadVersion(req.args));
});
ipc.on("bs-oculus-stop-download", async (_, reply) => {
const oculusDownloader = BsOculusDownloaderService.getInstance();
reply(of(oculusDownloader.stopDownload()));
});
ipc.on("bs-oculus-has-auth-token", async (_, reply) => {
const oculusDownloader = BsOculusDownloaderService.getInstance();
reply(from(oculusDownloader.getAuthToken().then(token => !!token)));
});
ipc.on("bs-oculus-clear-auth-token", async (_, reply) => {
const oculusDownloader = BsOculusDownloaderService.getInstance();
reply(from(oculusDownloader.clearAuthToken()));
});
// #endregion
+1 -2
View File
@@ -11,5 +11,4 @@ import "./beat-saver-ipcs";
import "./bs-playlist-ipcs";
import "./model-saber.ipcs";
import "./bs-model-ipcs";
import "./bs-downgrade/bs-oculus-download-ipcs";
import "./bs-downgrade/bs-download-ipcs";
import "./bs-version-download/bs-download-ipcs";
-4
View File
@@ -20,8 +20,6 @@ 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";
import { BsOculusDownloaderService } from "./services/bs-oculus-downloader.service";
import { BSVersion } from "shared/bs-version.interface";
const isDebug = process.env.NODE_ENV === "development" || process.env.DEBUG_PROD === "true";
@@ -60,8 +58,6 @@ const createWindow = async (window: AppWindow = "launcher.html") => {
await installExtensions();
}
WindowManagerService.getInstance().openWindow(window);
BsOculusDownloaderService.getInstance().clearTokenCookie();
};
const initServicesMustBeInitialized = () => {
+16 -10
View File
@@ -1,10 +1,10 @@
import JSZip from "jszip";
import fetch from "node-fetch";
import { CustomError } from "../../shared/models/exceptions/custom-error.class";
import { mkdirs, createWriteStream, pathExists, writeFile, WriteStream } from "fs-extra";
import { mkdirs, createWriteStream, pathExists, WriteStream } from "fs-extra";
import path from "path";
import { inflate } from "pako"
import { EMPTY, Observable, ReplaySubject, catchError, filter, from, lastValueFrom, map, mergeMap, scan, share, tap } from "rxjs";
import { EMPTY, Observable, ReplaySubject, Subscriber, catchError, filter, from, lastValueFrom, mergeMap, scan, share, tap } from "rxjs";
import { Progression, hashFile } from "../helpers/fs.helpers";
export class OculusDownloader {
@@ -12,6 +12,8 @@ export class OculusDownloader {
private options: OculusDownloaderOptions;
private isDownloading: boolean;
private downloadSubscriber: Subscriber<Progression>;
private getDownloadManifestUrl(token: string, binaryId: string): string {
return `https://securecdn.oculus.com/binaries/download/?id=${binaryId}&access_token=${token}&get_manifest=1`;
}
@@ -59,7 +61,7 @@ export class OculusDownloader {
writeStream = createWriteStream(destination);
for (const segment of file.segments) {
if(canceled || !writeStream.writable){ return; }
if(canceled || !writeStream.writable || !this.isDownloading){ return; }
const arrBuffer = await downloadSegment(segment);
const inflated = inflate(arrBuffer);
@@ -130,6 +132,8 @@ export class OculusDownloader {
return new Observable<Progression>(subscriber => {
this.downloadSubscriber = subscriber;
if(this.isDownloading){
throw new CustomError("Already downloading", "ALREADY_DOWNLOADING");
}
@@ -150,10 +154,14 @@ export class OculusDownloader {
const filesDownloadObservable = from(files).pipe(
filter(() => this.isDownloading),
mergeMap(([filename, file]) => (
from(this.isFileIntegrityValid([filename, file], options.destination)).pipe( map(isValid => ({ filename, file, isValid })))
this.isFileIntegrityValid([filename, file], options.destination)).then(isValid => ({ filename, file, isValid })
)),
filter(({ isValid }) => !isValid),
mergeMap(({ filename, file }) => {
mergeMap(({ filename, file, isValid }) => {
if(isValid){
const res: Progression<OculusManifestFile> = { current: file.size, total: file.size, diff: file.size, data: file };
return from([res])
}
const target = path.join(options.destination, filename);
return this.downloadManifestFile(file, target).pipe(
catchError(err => {
@@ -161,7 +169,7 @@ export class OculusDownloader {
return EMPTY;
})
);
}, 10),
}, 5),
scan((acc, curr) => acc + curr.diff, 0),
);
@@ -172,8 +180,6 @@ export class OculusDownloader {
},
})));
await writeFile(path.join(options.destination, "type.info"), "oculus");
const integrity = await lastValueFrom(this.verifyIntegrity(manifest, options.destination)).catch(err => CustomError.throw(err, "VERIFY_INTEGRITY_FAILED"));
if(integrity.data.length > 0){
@@ -183,7 +189,6 @@ export class OculusDownloader {
})().then(() => subscriber.complete()).catch(err => subscriber.error(err));
return () => {
console.log("C FINI");
this.isDownloading = false;
}
}).pipe(share({connector: () => new ReplaySubject(1)}));
@@ -191,6 +196,7 @@ export class OculusDownloader {
public stopDownload(){
this.isDownloading = false;
this.downloadSubscriber?.complete();
}
}
+87 -23
View File
@@ -11,17 +11,19 @@ import log from "electron-log";
import { OculusService } from "./oculus.service";
import { DownloadLinkType } from "shared/models/mods";
import sanitize from "sanitize-filename";
import { copyDirectoryWithJunctions, deleteFolder, getFoldersInFolder, pathExist } from "../helpers/fs.helpers";
import { Progression, copyDirectoryWithJunctions, deleteFolder, ensurePathNotAlreadyExist, getFoldersInFolder, pathExist, rxCopy } from "../helpers/fs.helpers";
import { FolderLinkerService } from "./folder-linker.service";
import { ReadStream, createReadStream, readFile } from "fs-extra";
import { ReadStream, createReadStream, readFile, writeFile } from "fs-extra";
import readline from "readline";
import { Observable, Subject } from "rxjs";
import { Observable, Subject, catchError, finalize, from, map, switchMap, throwError } from "rxjs";
import { BsStore } from "../../shared/models/bs-store.enum";
import { CustomError } from "../../shared/models/exceptions/custom-error.class";
export class BSLocalVersionService {
private static instance: BSLocalVersionService;
private readonly CUSTOM_VERSIONS_KEY = "custom-versions";
private readonly METADATA_FILE = "metadata.config";
private readonly installLocationService: InstallationLocationService;
private readonly steamService: SteamService;
@@ -110,12 +112,8 @@ export class BSLocalVersionService {
folderVersion.ino = folderStats.ino;
}
const type: string = await readFile(path.join(bsPath, "type.info"), "utf-8").catch(() => null);
if(type === BsStore.OCULUS){
folderVersion.downloadedFrom = BsStore.OCULUS;
} else {
folderVersion.downloadedFrom = BsStore.STEAM;
}
const versionStore: BsStore = await this.getVersionMetadata(folderVersion, "store");
folderVersion.store = versionStore ?? BsStore.STEAM;
const customVersion = this.getCustomVersions().find(customVersion => {
return customVersion.BSVersion === folderVersion.BSVersion && customVersion.name === folderVersion.name;
@@ -124,24 +122,57 @@ export class BSLocalVersionService {
folderVersion.color = customVersion?.color;
return folderVersion;
}
}
private setCustomVersions(versions: BSVersion[]): void{
this.configService.set(this.CUSTOM_VERSIONS_KEY, versions);
}
public getAllVersionMetadata(version: BSVersion): Promise<Record<string, unknown>|null>{
return (async () => {
const versionPath = await this.getVersionPath(version);
const contents = await readFile(path.join(versionPath, this.METADATA_FILE), "utf-8");
return JSON.parse(contents);
})().catch(e => {
log.warn(e);
return null;
});
}
private addCustomVersion(version: BSVersion): void{
this.setCustomVersions([...this.getCustomVersions() ?? [], version]);
}
public getVersionMetadata<T = unknown>(version: BSVersion, key: string): Promise<T|null>{
return (async () => {
const metadata = await this.getAllVersionMetadata(version);
return metadata?.[key] as T;
})().catch(e => {
log.warn(e);
return null;
});
}
private getCustomVersions(): BSVersion[]{
return this.configService.get<BSVersion[]>(this.CUSTOM_VERSIONS_KEY) || [];
}
public setVersionMetadata(version: BSVersion, key: string, value: unknown): Promise<void>{
return (async () => {
const versionPath = await this.getVersionPath(version);
const metadataPath = path.join(versionPath, this.METADATA_FILE);
const metadata = await this.getAllVersionMetadata(version) || {};
metadata[key] = value;
await writeFile(metadataPath, JSON.stringify(metadata));
})().catch(e => {
log.error(e);
});
}
private deleteCustomVersion(version: BSVersion): void{
const customVersions = this.getCustomVersions() || [];
this.setCustomVersions(customVersions.filter(v => (v.name !== version.name || v.BSVersion !== version.BSVersion || v.color !== version.color)));
}
private setCustomVersions(versions: BSVersion[]): void{
this.configService.set(this.CUSTOM_VERSIONS_KEY, versions);
}
private addCustomVersion(version: BSVersion): void{
this.setCustomVersions([...this.getCustomVersions() ?? [], version]);
}
private getCustomVersions(): BSVersion[]{
return this.configService.get<BSVersion[]>(this.CUSTOM_VERSIONS_KEY) || [];
}
private deleteCustomVersion(version: BSVersion): void{
const customVersions = this.getCustomVersions() || [];
this.setCustomVersions(customVersions.filter(v => (v.name !== version.name || v.BSVersion !== version.BSVersion || v.color !== version.color)));
}
/**
@@ -309,6 +340,34 @@ export class BSLocalVersionService {
})
}
public importVersion(opt: ImportVersionOptions): Observable<Progression<BSVersion>>{
const { fromPath, store } = opt;
let failed = false;
let versionDest: {version: BSVersion, dest: string} = null;
return from(this.getVersionOfBSFolder(fromPath)).pipe(
map(version => version || CustomError.throw(new Error("Unable to get BS version of path"), "NOT_BS_FOLDER")),
switchMap(version => this.getVersionPath(version).then(dest => ({version, dest}))),
switchMap(({version, dest}) => ensurePathNotAlreadyExist(dest).then(uniquePath => {
const res = dest === uniquePath ? {version, dest} : {version: {...version, name: path.basename(uniquePath)}, dest: uniquePath} as {version: BSVersion, dest: string};
versionDest = res;
return res;
})),
switchMap(({version, dest}) => rxCopy(fromPath, dest, { dereference: true }).pipe(
map(progress => ({...progress, data: version}))
)),
catchError(err => {
failed = true;
return throwError(() => err);
}),
finalize(async () => {
if(failed){ return; }
await this.setVersionMetadata(versionDest.version, "store", store);
})
);
}
public async getLinkedFolders(version: BSVersion): Promise<string[]>{
const versionPath = await this.getVersionPath(version);
const [rootFolders, beatSaberDataFolders] = await Promise.all([getFoldersInFolder(versionPath), getFoldersInFolder(path.join(versionPath, "Beat Saber_Data"))]);
@@ -329,3 +388,8 @@ export class BSLocalVersionService {
return this._loadedVersions$.asObservable();
}
}
export interface ImportVersionOptions {
fromPath: string;
store: BsStore
}
@@ -1,13 +1,16 @@
import { BSVersion } from "../../shared/bs-version.interface";
import { WindowManagerService } from "./window-manager.service";
import { minToMs, msToS } from "../../shared/helpers/time.helpers";
import { BSVersion } from "../../../shared/bs-version.interface";
import { WindowManagerService } from "../window-manager.service";
import { minToMs, msToS } from "../../../shared/helpers/time.helpers";
import log from "electron-log";
import { CustomError } from "../../shared/models/exceptions/custom-error.class";
import { OculusDownloader } from "../models/oculus-downloader.class";
import { CustomError } from "../../../shared/models/exceptions/custom-error.class";
import { OculusDownloader } from "../../models/oculus-downloader.class";
import { Cookie, session } from "electron";
import { Progression, pathExist } from "../helpers/fs.helpers";
import { BSLocalVersionService } from "./bs-local-version.service";
import { Observable, Subscription } from "rxjs";
import { Progression, ensurePathNotAlreadyExist } from "../../helpers/fs.helpers";
import { BSLocalVersionService } from "../bs-local-version.service";
import { Observable, catchError, finalize, from, map, switchMap, throwError } from "rxjs";
import path from "path";
import { DownloadInfo } from "./bs-steam-downloader.service";
import { BsStore } from "../../../shared/models/bs-store.enum";
export class BsOculusDownloaderService {
@@ -73,8 +76,8 @@ export class BsOculusDownloaderService {
return cookie.expirationDate > msToS(Date.now());
}
public async getTokenFromCookie(): Promise<string | undefined> {
const cookie = await session.defaultSession.cookies.get({ name: "oc_www_at" }).then(a => a.at(0));
public async getAuthToken(): Promise<string | undefined> {
const cookie = await session.defaultSession.cookies.get({ name: "oc_www_at" }).then(a => a?.at(0));
if(this.isCookieValid(cookie) && this.isUserTokenValid(cookie.value)){
return cookie.value;
@@ -117,7 +120,7 @@ export class BsOculusDownloaderService {
clearTimeout(timout);
if(!keepToken){
this.clearTokenCookie();
this.clearAuthToken();
}
if(window.isClosable() && !window.isDestroyed()){
@@ -128,72 +131,76 @@ export class BsOculusDownloaderService {
return promise;
}
/**
* DUPLICATION FROM BS-INSTALLER.SERVICE (TODO : need to be refactored)
* @param path
* @returns
*/
private async getPathNotAleardyExist(path: string): Promise<string> {
let destPath = path;
let folderExist = await pathExist(destPath);
let i = 0;
while (folderExist) {
i++;
destPath = `${path} (${i})`;
folderExist = await pathExist(destPath);
}
return destPath;
private async createDownloadVersion(version: BSVersion): Promise<{version: BSVersion, dest: string}>{
const dest = await ensurePathNotAlreadyExist(await this.versions.getVersionPath(version));
return {
version: {...version, ...(path.basename(dest) !== version.BSVersion && { name: path.basename(dest) })},
dest
};
}
public downloadVersion(downloadInfo: OculusDownloadInfo){
return new Observable<Progression>(obs => {
let sub: Subscription;
(async () => {
const token = await this.getUserTokenFromMetaAuth(downloadInfo.stay);
const dest = await this.getPathNotAleardyExist(await this.versions.getVersionPath(downloadInfo.version));
sub = this.oculusDownloader.downloadApp({ accessToken: token, binaryId: downloadInfo.version.OculusBinaryId, destination: dest }).subscribe(obs);
})().catch(err => obs.error(err));
return () => {
sub?.unsubscribe();
this.oculusDownloader.stopDownload();
}
})
public stopDownload(){
this.oculusDownloader.stopDownload();
}
public autoDownloadVersion(version: BSVersion): Observable<Progression>{
return new Observable<Progression>(obs => {
let sub: Subscription;
(async () => {
const token = await this.getTokenFromCookie();
public downloadVersion(downloadInfo: DownloadInfo): Observable<Progression<BSVersion>>{
return from(this.getUserTokenFromMetaAuth(downloadInfo.stay)).pipe(
map(token => {
if(!token){
throw new CustomError("No token has been found while try to auto download Beat Saber from Oculus", "OCULUS_TOKEN_NEEDED");
}
const dest = await this.getPathNotAleardyExist(await this.versions.getVersionPath(version));
sub = this.oculusDownloader.downloadApp({ accessToken: token, binaryId: version.OculusBinaryId, destination: dest }).subscribe(obs);
})().catch(err => obs.error(err));
return () => {
sub?.unsubscribe();
this.oculusDownloader.stopDownload();
}
});
return token;
}),
switchMap(token => {
if(!downloadInfo.isVerification){
return this.createDownloadVersion(downloadInfo.bsVersion).then(({version, dest}) => ({token, version, dest}))
}
return this.versions.getVersionPath(downloadInfo.bsVersion).then(path => ({token, version: downloadInfo.bsVersion, dest: path}));
}),
switchMap(({token, version, dest}) => {
return this.oculusDownloader.downloadApp({ accessToken: token, binaryId: version.OculusBinaryId, destination: dest }).pipe(map(
progress => ({...progress, data: version})
));
}),
finalize(() => this.oculusDownloader.stopDownload())
);
}
public clearTokenCookie(): Promise<void>{
public autoDownloadVersion(downloadInfo: DownloadInfo): Observable<Progression<BSVersion>>{
let failed = false;
let downloadVersion: BSVersion
return from(this.getAuthToken()).pipe(
map(token => {
if(!token){
throw new CustomError("No token has been found while try to auto download Beat Saber from Oculus", "OCULUS_TOKEN_NEEDED");
}
return token;
}),
switchMap(token => {
if(!downloadInfo.isVerification){
return this.createDownloadVersion(downloadInfo.bsVersion).then(({version, dest}) => ({token, version, dest}));
}
return this.versions.getVersionPath(downloadInfo.bsVersion).then(path => ({token, version: downloadInfo.bsVersion, dest: path}));
}),
switchMap(({token, version, dest}) => {
downloadVersion = version;
return this.oculusDownloader.downloadApp({ accessToken: token, binaryId: version.OculusBinaryId, destination: dest }).pipe(
map(progress => ({...progress, data: version})),
);
}),
catchError(err => {
failed = true;
return throwError(() => err);
}),
finalize(() => from(!failed && this.versions.setVersionMetadata(downloadVersion, "store", BsStore.OCULUS))),
finalize(() => this.oculusDownloader.stopDownload()),
);
}
public clearAuthToken(): Promise<void>{
return session.defaultSession.clearStorageData({ storages: ["cookies"], origin: ".oculus.com" })
}
@@ -1,20 +1,21 @@
import { BS_APP_ID, BS_DEPOT } from "../constants";
import { BS_APP_ID, BS_DEPOT } from "../../constants";
import path from "path";
import { BSVersion } from "shared/bs-version.interface";
import { UtilsService } from "./utils.service";
import { UtilsService } from "../utils.service";
import { spawnSync } from "child_process";
import log from "electron-log";
import { InstallationLocationService } from "./installation-location.service";
import { BSLocalVersionService } from "./bs-local-version.service";
import { copy, ensureDir } from "fs-extra";
import { pathExist } from "../helpers/fs.helpers";
import { Observable, map } from "rxjs";
import { DepotDownloaderArgsOptions, DepotDownloaderErrorEvent, DepotDownloaderEvent, DepotDownloaderEventType, DepotDownloaderInfoEvent } from "../../shared/models/depot-downloader.model";
import { DepotDownloader } from "../models/depot-downloader.class";
import { InstallationLocationService } from "../installation-location.service";
import { BSLocalVersionService } from "../bs-local-version.service";
import { ensureDir } from "fs-extra";
import { ensurePathNotAlreadyExist } from "../../helpers/fs.helpers";
import { Observable, catchError, finalize, map, throwError } from "rxjs";
import { DepotDownloaderArgsOptions, DepotDownloaderErrorEvent, DepotDownloaderEvent, DepotDownloaderEventType, DepotDownloaderInfoEvent } from "../../../shared/models/depot-downloader.model";
import { DepotDownloader } from "../../models/depot-downloader.class";
import { app } from "electron";
import { BsStore } from "../../../shared/models/bs-store.enum";
export class BSInstallerService {
private static instance: BSInstallerService;
export class BsSteamDownloaderService {
private static instance: BsSteamDownloaderService;
private readonly utils: UtilsService;
private readonly installLocationService: InstallationLocationService;
@@ -33,10 +34,10 @@ export class BSInstallerService {
}
public static getInstance() {
if (!BSInstallerService.instance) {
BSInstallerService.instance = new BSInstallerService();
if (!BsSteamDownloaderService.instance) {
BsSteamDownloaderService.instance = new BsSteamDownloaderService();
}
return BSInstallerService.instance;
return BsSteamDownloaderService.instance;
}
private getDepotDownloaderExePath(): string {
@@ -59,10 +60,10 @@ export class BSInstallerService {
}
}
private async buildDepotDownloaderInstance(downloadInfos: DownloadInfo, qr?: boolean): Promise<DepotDownloader> {
private async buildDepotDownloaderInstance(downloadInfos: DownloadSteamInfo, qr?: boolean): Promise<{depotDownloader: DepotDownloader, depotDownloaderOptions: DepotDownloaderArgsOptions, version: BSVersion}> {
const versionPath = await this.localVersionService.getVersionPath(downloadInfos.bsVersion);
const dest = downloadInfos.isVerification ? versionPath : await this.getPathNotAleardyExist(versionPath);
const dest = downloadInfos.isVerification ? versionPath : await ensurePathNotAlreadyExist(versionPath);
const downloadVersion: BSVersion = { ...downloadInfos.bsVersion, ...(path.basename(dest) !== downloadInfos.bsVersion.BSVersion && { name: path.basename(dest) }) };
const depotDownloaderOptions: DepotDownloaderArgsOptions = {
@@ -83,20 +84,22 @@ export class BSInstallerService {
const exePath = this.getDepotDownloaderExePath();
const args = DepotDownloader.buildArgs(depotDownloaderOptions);
return new DepotDownloader({
const depotDownloader = new DepotDownloader({
command: isLinux ? 'dotnet' : exePath,
args: isLinux ? [exePath, ...args] : args,
options: { cwd: await this.installLocationService.versionsDirectory() },
echoStartData: downloadVersion
}, log);
return { depotDownloader, depotDownloaderOptions, version: downloadVersion }
}
private buildDepotDownloaderObservable(downloadInfos: DownloadInfo, qr?: boolean): Observable<DepotDownloaderEvent> {
private buildDepotDownloaderObservable(downloadInfos: DownloadSteamInfo, qr?: boolean): Observable<DepotDownloaderEvent> {
return new Observable(sub => {
const depotDownloaderBuildPromise = this.buildDepotDownloaderInstance(downloadInfos, qr);
depotDownloaderBuildPromise.then(depotDownloader => {
depotDownloaderBuildPromise.then(({ depotDownloader, version }) => {
if(this.depotDownloader?.running){
this.depotDownloader.stop();
@@ -104,12 +107,23 @@ export class BSInstallerService {
this.depotDownloader = depotDownloader;
depotDownloader.$events().pipe(map(event => {
if(event.type === DepotDownloaderEventType.Error){
throw event;
}
return event;
})).subscribe(sub);
let failed = false;
depotDownloader.$events().pipe(
map(event => {
if(event.type === DepotDownloaderEventType.Error){
throw event;
}
return event;
}),
catchError(err => {
failed = true;
return throwError(() => err);
}),
finalize(() => {
!failed && this.localVersionService.setVersionMetadata(version, "store", BsStore.STEAM);
})
).subscribe(sub);
}).catch(err => sub.error({
type: DepotDownloaderEventType.Error,
@@ -118,16 +132,16 @@ export class BSInstallerService {
} as DepotDownloaderEvent));
return () => {
depotDownloaderBuildPromise.then(depotDownloader => depotDownloader.stop());
depotDownloaderBuildPromise.then(({ depotDownloader }) => depotDownloader.stop());
}
});
}
public downloadBsVersion(downloadInfos: DownloadInfo): Observable<DepotDownloaderEvent> {
public downloadBsVersion(downloadInfos: DownloadSteamInfo): Observable<DepotDownloaderEvent> {
return this.buildDepotDownloaderObservable(downloadInfos);
}
public autoDownloadBsVersion(downloadInfos: DownloadInfo): Observable<DepotDownloaderEvent> {
public autoDownloadBsVersion(downloadInfos: DownloadSteamInfo): Observable<DepotDownloaderEvent> {
return this.buildDepotDownloaderObservable({...downloadInfos, password: null, stay: true}).pipe(map(event => {
if(event.type === DepotDownloaderEventType.Info && event.subType === DepotDownloaderInfoEvent.Password){
throw new Error("Ask for password while auto download");
@@ -136,7 +150,7 @@ export class BSInstallerService {
}));
}
public downloadBsVersionWithQRCode(downloadInfos: DownloadInfo): Observable<DepotDownloaderEvent> {
public downloadBsVersionWithQRCode(downloadInfos: DownloadSteamInfo): Observable<DepotDownloaderEvent> {
return this.buildDepotDownloaderObservable(downloadInfos, true);
}
@@ -147,42 +161,17 @@ export class BSInstallerService {
public stopDownload(): void {
this.depotDownloader?.stop();
}
private async getPathNotAleardyExist(path: string): Promise<string> {
let destPath = path;
let folderExist = await pathExist(destPath);
let i = 0;
while (folderExist) {
i++;
destPath = `${path} (${i})`;
folderExist = await pathExist(destPath);
}
return destPath;
}
public async importVersion(path: string): Promise<BSVersion> {
const version = await this.localVersionService.getVersionOfBSFolder(path);
if (!version) {
throw new Error("NOT_BS_FOLDER");
}
const destPath = await this.getPathNotAleardyExist(await this.localVersionService.getVersionPath(version));
await copy(path, destPath, { dereference: true });
return version;
}
}
export interface DownloadInfo {
bsVersion: BSVersion;
isVerification?: boolean;
stay?: boolean;
}
export interface DownloadSteamInfo extends DownloadInfo {
username?: string;
password?: string;
stay?: boolean;
isVerification?: boolean;
}
export interface DownloadEvent {
@@ -34,6 +34,8 @@ export const ChooseStore: ModalComponent<BsStore> = ({ resolver }) => {
}
})();
// TODO : Translate
return (
<form className="flex flex-col gap-3">
<h1 className="text-3xl uppercase tracking-wide w-full text-center text-gray-800 dark:text-gray-200">{t("which platform ?")}</h1>
@@ -41,7 +43,7 @@ export const ChooseStore: ModalComponent<BsStore> = ({ resolver }) => {
<div className="flex flex-row w-full flex-grow gap-4">
<div className="flex flex-col flex-grow basis-0 gap-2 text-center px-5 pt-3 pb-1 rounded-md border-main-color-3 border-2 cursor-pointer" onMouseEnter={() => setOculusHover(true)} onMouseLeave={() => setOculusHover(false)} onClick={() => chooseStore(BsStore.OCULUS)} style={{backgroundColor: oculusHover ? bg.dim : bg.bright}}>
<OculusIcon className="flex-grow aspect-square text-black bg-white rounded-full p-5"/>
<h2 className="font-bold">Oculus PC</h2>
<h2 className="font-bold">Oculus Store (PC)</h2>
</div>
<div className="flex flex-col flex-grow basis-0 gap-2 text-center px-5 pt-3 pb-1 rounded-md border-main-color-3 border-2 cursor-pointer" onMouseEnter={() => setSteamHover(true)} onMouseLeave={() => setSteamHover(false)} onClick={() => chooseStore(BsStore.STEAM)} style={{backgroundColor: steamHover ? bg.dim : bg.bright}}>
<SteamIcon className="flex-grow"/>
@@ -3,18 +3,34 @@ import { BsmImage } from "renderer/components/shared/bsm-image.component";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { ModalComponent, ModalExitCode } from "renderer/services/modale.service";
import BeatImpatient from "../../../../../assets/images/apngs/beat-impatient.png";
import { BsmCheckbox } from "renderer/components/shared/bsm-checkbox.component";
import Tippy from "@tippyjs/react";
import { useState } from "react";
import { BsStore } from "shared/models/bs-store.enum";
export const ImportVersionModal: ModalComponent<void> = ({ resolver }) => {
export const ImportVersionModal: ModalComponent<BsStore> = ({ resolver }) => {
const t = useTranslation();
const [isOculus, setIsOculus] = useState(false);
const submit = () => {
resolver({ exitCode: ModalExitCode.COMPLETED, data: isOculus ? BsStore.OCULUS : BsStore.STEAM });
}
return (
<form>
<h1 className="text-3xl uppercase tracking-wide w-full text-center text-gray-800 dark:text-gray-200">{t("modals.bs-import-version.title")}</h1>
<BsmImage className="mx-auto h-20" image={BeatImpatient} />
<p className="max-w-sm text-gray-800 dark:text-gray-200">{t("modals.bs-import-version.description")}</p>
<div className="grid grid-flow-col grid-cols-2 gap-4 mt-4">
<Tippy content="Cocher si c'est une version Oculus" placement="right" arrow={false} className="!bg-neutral-900 font-bold">
<div className="relative flex flex-row items-center gap-1.5 mb-4 mt-3 cursor-help w-fit">
<BsmCheckbox className="relative h-5 w-5 z-[1]" checked={isOculus} onChange={checked => setIsOculus(checked)}/>
<span className="font-bold italic">Version Oculus</span> {/* TODO Translate */}
</div>
</Tippy>
<div className="grid grid-flow-col grid-cols-2 gap-4">
<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={() => resolver({ exitCode: ModalExitCode.COMPLETED })} withBar={false} text="modals.bs-import-version.buttons.submit" />
<BsmButton typeColor="primary" className="rounded-md text-center transition-all" onClick={submit} withBar={false} text="modals.bs-import-version.buttons.submit" />
</div>
</form>
);
@@ -1,6 +1,5 @@
import { BSVersion } from "shared/bs-version.interface";
import { Link, useLocation } from "react-router-dom";
import { SteamDownloaderService } from "renderer/services/bs-downgrade/steam-downloader.service";
import { useState } from "react";
import { distinctUntilChanged, map, of, Subscription, switchMap } from "rxjs";
import { BSLauncherService, LaunchMods } from "renderer/services/bs-launcher.service";
@@ -15,13 +14,17 @@ import Tippy from "@tippyjs/react";
import { useService } from "renderer/hooks/use-service.hook";
import equal from "fast-deep-equal";
import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
import { BsDownloaderService } from "renderer/services/bs-version-download/bs-downloader.service";
import { ProgressBarService } from "renderer/services/progress-bar.service";
export function BsVersionItem(props: { version: BSVersion }) {
const downloaderService = useService(SteamDownloaderService);
const bsDownloader = useService(BsDownloaderService);
const verionManagerService = useService(BSVersionManagerService);
const launcherService = useService(BSLauncherService);
const configService = useService(ConfigurationService);
const bsUninstallerService = useService(BSUninstallerService);
const progressBar = useService(ProgressBarService);
const { state } = useLocation() as { state: BSVersion };
const { fontSize, ref } = useFitText();
@@ -33,14 +36,14 @@ export function BsVersionItem(props: { version: BSVersion }) {
useOnUpdate(() => {
const subs: Subscription[] = []
subs.push(downloaderService.currentBsVersionDownload$.pipe(map(download => equal(download, props.version)), distinctUntilChanged()).subscribe(isDownloading => {
subs.push(bsDownloader.downloadingVersion$.pipe(map(download => equal(download, props.version)), distinctUntilChanged()).subscribe(isDownloading => {
setIsDownloading(() => isDownloading);
}));
subs.push(downloaderService.currentBsVersionDownload$.pipe(
subs.push(bsDownloader.downloadingVersion$.pipe(
map(download => equal(download, props.version)),
distinctUntilChanged(),
switchMap(isDownloading => isDownloading ? downloaderService.downloadProgress$ : of(0)),
switchMap(isDownloading => isDownloading ? progressBar.progress$ : of(0)),
).subscribe(progress => {
setDownloadProgress(() => progress);
}));
@@ -62,9 +65,9 @@ export function BsVersionItem(props: { version: BSVersion }) {
};
const cancel = () => {
const versionDownload = downloaderService.currentBsVersionDownload$.value;
const wasVerification = downloaderService.isVerification;
downloaderService.stopDownload().then(() => {
const versionDownload = bsDownloader.downloadingVersion;
const wasVerification = bsDownloader.isVerifying;
bsDownloader.stopDownload().then(() => {
if(wasVerification){ return; }
bsUninstallerService.uninstall(versionDownload).then(res => res && verionManagerService.askInstalledVersions());
});
@@ -11,15 +11,15 @@ import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
import Tippy from "@tippyjs/react";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { useService } from "renderer/hooks/use-service.hook";
import { SteamDownloaderService } from "renderer/services/bs-downgrade/steam-downloader.service";
import { distinctUntilChanged } from "rxjs";
import equal from "fast-deep-equal";
import { BsDownloaderService } from "renderer/services/bs-version-download/bs-downloader.service";
export function NavBar() {
const versionManager = useService(BSVersionManagerService);
const versionDownloader = useService(SteamDownloaderService);
const versionDownloader = useService(BsDownloaderService);
const downloadingVersion = useObservable(versionDownloader.currentBsVersionDownload$.pipe(distinctUntilChanged(equal)));
const downloadingVersion = useObservable(versionDownloader.downloadingVersion$.pipe(distinctUntilChanged(equal)));
const installedVersions = useObservable(versionManager.installedVersions$);
const color = useThemeColor("first-color");
@@ -10,6 +10,7 @@ import { LaunchModToogle } from "./launch-mod-toogle.component";
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";
type Props = { version: BSVersion };
@@ -73,7 +74,7 @@ export function LaunchSlide({ version }: Props) {
<h1 className="relative text-4xl font-bold italic -top-3">{version.name ? `${version.BSVersion} - ${version.name}` : version.BSVersion}</h1>
</div>
<div className="grid grid-flow-col gap-6">
{!version.oculus && <LaunchModToogle infoText="pages.version-viewer.launch-mods.oculus-description" icon="oculus" onClick={() => setMode(LaunchMods.OCULUS_MOD, !oculusMode)} active={oculusMode} text="pages.version-viewer.launch-mods.oculus" />}
{!(version.oculus || version.store === BsStore.OCULUS) && <LaunchModToogle infoText="pages.version-viewer.launch-mods.oculus-description" icon="oculus" onClick={() => setMode(LaunchMods.OCULUS_MOD, !oculusMode)} active={oculusMode} text="pages.version-viewer.launch-mods.oculus" />}
<LaunchModToogle infoText="pages.version-viewer.launch-mods.desktop-description" icon="desktop" onClick={() => setMode(LaunchMods.DESKTOP_MOD, !desktopMode)} active={desktopMode} text="pages.version-viewer.launch-mods.desktop" />
<LaunchModToogle infoText="pages.version-viewer.launch-mods.debug-description" icon="terminal" onClick={() => setMode(LaunchMods.DEBUG_MOD, !debugMode)} active={debugMode} text="pages.version-viewer.launch-mods.debug" />
</div>
@@ -1,5 +1,4 @@
import { AvailableVersionsSlider } from "../components/available-versions/available-versions-slider.component";
import { SteamDownloaderService } from "../services/bs-downgrade/steam-downloader.service";
import { Slideshow } from "renderer/components/slideshow/slideshow.component";
import { createContext, useMemo, useState } from "react";
import { BsmButton } from "renderer/components/shared/bsm-button.component";
@@ -7,80 +6,38 @@ import { AnimatePresence, motion } from "framer-motion";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { BSVersionManagerService } from "renderer/services/bs-version-manager.service";
import { BsmDropdownButton } from "renderer/components/shared/bsm-dropdown-button.component";
import { ProgressBarService } from "renderer/services/progress-bar.service";
import { ModalExitCode, ModalService } from "renderer/services/modale.service";
import { ImportVersionModal } from "renderer/components/modal/modal-types/import-version-modal.component";
import { IpcService } from "renderer/services/ipc.service";
import { NotificationService } from "renderer/services/notification.service";
import { lastValueFrom, map } from "rxjs";
import { useService } from "renderer/hooks/use-service.hook";
import { BSVersion } from "shared/bs-version.interface";
import { useObservable } from "renderer/hooks/use-observable.hook";
import { ConfigurationService } from "renderer/services/configuration.service";
import { BsDownloaderService } from "renderer/services/bs-downgrade/bs-downloader.service";
import { BsDownloaderService } from "renderer/services/bs-version-download/bs-downloader.service";
export const AvailableVersionsContext = createContext<{ selectedVersion: BSVersion; setSelectedVersion: (version: BSVersion) => void }>(null);
export function AvailableVersionsList() {
const steamDownloader = useService(SteamDownloaderService);
const versionManager = useService(BSVersionManagerService);
const progressBar = useService(ProgressBarService);
const modal = useService(ModalService);
const ipc = useService(IpcService);
const notification = useService(NotificationService);
const config = useService(ConfigurationService);
const bsDownloader = useService(BsDownloaderService);
const versionManager = useService(BSVersionManagerService);
const [selectedVersion, setSelectedVersion] = useState<BSVersion>(null);
const contextValue = useMemo(() => ({ selectedVersion, setSelectedVersion }), [selectedVersion]);
const downloading = useObservable(steamDownloader.currentBsVersionDownload$.pipe(map(v => !!v)));
const downloading = useObservable(bsDownloader.downloadingVersion$.pipe(map(v => !!v)));
const t = useTranslation();
const startDownload = async () => {
const store = bsDownloader.getLastStoreDownloadedFrom() ?? await bsDownloader.chooseStoreToDownloadFrom();
const store = bsDownloader.getLastStoreDownloadedFrom() ?? await bsDownloader.chooseStoreToDownloadFrom().catch(() => null);
if(!store){ return; }
return bsDownloader.downloadVersion(selectedVersion, store)
.catch(console.log)
.finally(() => setSelectedVersion(null));
};
const importVersion = async () => {
if (!progressBar.require()) {
return;
}
const modalRes = await modal.openModal(ImportVersionModal);
if (modalRes.exitCode !== ModalExitCode.COMPLETED) {
return;
}
const folderRes = await lastValueFrom(ipc.sendV2<{ canceled: boolean; filePaths: string[] }>("choose-folder"))
if (!folderRes || folderRes.canceled || !folderRes.filePaths?.length) {
return;
}
notification.notifySuccess({ title: "notifications.bs-import-version.success.start-import.title", desc: "notifications.bs-import-version.success.start-import.desc", duration: 6_000 });
progressBar.showFake(0.008);
const toImport = folderRes.filePaths.at(0);
const imported = await steamDownloader.importVersion(toImport);
if (imported) {
versionManager.askInstalledVersions();
notification.notifySuccess({ title: "notifications.bs-import-version.success.imported.title", duration: 3_000 });
progressBar.complete();
} else {
notification.notifyError({ title: "notifications.types.error", desc: "notifications.bs-import-version.errors.import-error.desc" });
}
progressBar.hide(true);
const importVersion = () => {
return lastValueFrom(versionManager.importVersion()).catch(() => {});
};
return (
+24 -9
View File
@@ -6,7 +6,7 @@ import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { BsmIconType } from "renderer/components/svgs/bsm-icon.component";
import { DefaultConfigKey, ThemeConfig } from "renderer/config/default-configuration.config";
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
import { SteamDownloaderService } from "renderer/services/bs-downgrade/steam-downloader.service";
import { SteamDownloaderService } from "renderer/services/bs-version-download/steam-downloader.service";
import { ConfigurationService } from "renderer/services/configuration.service";
import { I18nService } from "renderer/services/i18n.service";
import { IpcService } from "renderer/services/ipc.service";
@@ -35,7 +35,7 @@ import { useService } from "renderer/hooks/use-service.hook";
import { lastValueFrom } from "rxjs";
import { BsmException } from "shared/models/bsm-exception.model";
import { useObservable } from "renderer/hooks/use-observable.hook";
import { AuthUserService } from "renderer/services/auth-user.service";
import { OculusDownloaderService } from "renderer/services/bs-version-download/oculus-downloader.service";
export function SettingsPage() {
@@ -43,7 +43,8 @@ export function SettingsPage() {
const themeService = useService(ThemeService);
const ipcService = useService(IpcService);
const modalService = useService(ModalService);
const downloaderService = useService(SteamDownloaderService);
const steamDownloader = useService(SteamDownloaderService);
const oculusDownloader = useService(OculusDownloaderService);
const progressBarService = useService(ProgressBarService);
const notificationService = useService(NotificationService);
const i18nService = useService(I18nService);
@@ -52,7 +53,6 @@ export function SettingsPage() {
const playlistsManager = useService(PlaylistsManagerService);
const modelsManager = useService(ModelsManagerService);
const versionLinker = useService(VersionFolderLinkerService);
const authService = useService(AuthUserService);
const { firstColor, secondColor } = useThemeColor();
@@ -79,11 +79,12 @@ export function SettingsPage() {
const [mapDeepLinksEnabled, setMapDeepLinksEnabled] = useState(false);
const [playlistsDeepLinkEnabled, setPlaylistsDeepLinkEnabled] = useState(false);
const [modelsDeepLinkEnabled, setModelsDeepLinkEnabled] = useState(false);
const [hasDownloaderSession, setHasDownloaderSession] = useState(false);
const appVersion = useObservable(ipcService.sendV2<string>("current-version"));
const steamSessionExist = useObservable(authService.sessionExist$);
useEffect(() => {
loadInstallationFolder();
loadDownloadersSession();
mapsManager.isDeepLinksEnabled().then(enabled => setMapDeepLinksEnabled(() => enabled));
playlistsManager.isDeepLinksEnabled().then(enabled => setPlaylistsDeepLinkEnabled(() => enabled));
modelsManager.isDeepLinksEnabled().then(enabled => setModelsDeepLinkEnabled(() => enabled));
@@ -97,9 +98,23 @@ export function SettingsPage() {
};
const loadInstallationFolder = () => {
downloaderService.getInstallationFolder().then(res => setInstallationFolder(res));
steamDownloader.getInstallationFolder().then(res => setInstallationFolder(res));
};
const loadDownloadersSession = () => {
if(steamDownloader.sessionExist()){ return setHasDownloaderSession(true); }
oculusDownloader.hasAuthToken().then(hasToken => {
setHasDownloaderSession(hasToken);
});
}
const clearDownloadersSession = () => {
steamDownloader.deleteSteamSession();
oculusDownloader.clearAuthToken();
loadDownloadersSession();
}
const setFirstColorSetting = (hex: string) => configService.set("first-color", hex);
const setSecondColorSetting = (hex: string) => configService.set("second-color", hex);
@@ -131,7 +146,7 @@ export function SettingsPage() {
notificationService.notifySuccess({ title: "notifications.settings.move-folder.success.titles.transfer-started", desc: "notifications.settings.move-folder.success.descs.transfer-started" });
lastValueFrom(downloaderService.setInstallationFolder(fileChooserRes.filePaths[0])).then(res => {
lastValueFrom(steamDownloader.setInstallationFolder(fileChooserRes.filePaths[0])).then(res => {
progressBarService.complete();
progressBarService.hide(true);
@@ -207,8 +222,8 @@ export function SettingsPage() {
<BsmButton className="inline-block grow-0 bg-transparent sticky h-full w-full top-20 right-20 !m-0 rounded-full p-1" onClick={() => nav(-1)} icon="close" withBar={false} />
</div>
<SettingContainer title="pages.settings.steam.title" description="pages.settings.steam.description">
<BsmButton onClick={() => authService.deleteSteamSession()} className="w-fit px-3 py-[2px] text-white rounded-md" withBar={false} text="pages.settings.steam.logout" typeColor="error" disabled={!steamSessionExist}/>
<SettingContainer title="pages.settings.steam-and-oculus.title" description="pages.settings.steam-and-oculus.description">
<BsmButton onClick={clearDownloadersSession} className="w-fit px-3 py-[2px] text-white rounded-md" withBar={false} text="pages.settings.steam-and-oculus.logout" typeColor="error" disabled={!hasDownloaderSession}/>
</SettingContainer>
<SettingContainer title="pages.settings.appearance.title" description="pages.settings.appearance.description">
@@ -8,7 +8,6 @@ import { BSUninstallerService } from "../services/bs-uninstaller.service";
import { BSVersionManagerService } from "../services/bs-version-manager.service";
import { ModalExitCode, ModalService } from "../services/modale.service";
import DefautVersionImage from "../../../assets/images/default-version-img.jpg";
import { SteamDownloaderService } from "renderer/services/bs-downgrade/steam-downloader.service";
import { IpcService } from "renderer/services/ipc.service";
import { LaunchSlide } from "renderer/components/version-viewer/slides/launch/launch-slide.component";
import { ModsSlide } from "renderer/components/version-viewer/slides/mods/mods-slide.component";
@@ -21,13 +20,14 @@ 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";
import { BsDownloaderService } from "renderer/services/bs-version-download/bs-downloader.service";
export function VersionViewer() {
const bsUninstallerService = useService(BSUninstallerService);
const bsVersionManagerService = useService(BSVersionManagerService);
const modalService = useService(ModalService);
const bsDownloaderService = useService(SteamDownloaderService);
const bsDownloader = useService(BsDownloaderService);
const ipcService = useService(IpcService);
const bsLauncher = useService(BSLauncherService);
const notification = useService(NotificationService);
@@ -43,7 +43,7 @@ export function VersionViewer() {
navigate(`/bs-version/${version.BSVersion}`, { state: version });
};
const openFolder = () => ipcService.sendLazy("bs-version.open-folder", { args: state });
const verifyFiles = () => bsDownloaderService.verifyBsVersionFiles(state);
const verifyFiles = () => bsDownloader.verifyBsVersion(state);
const uninstall = async () => {
const modalCompleted = await modalService.openModal(UninstallModal, state);
@@ -1,49 +0,0 @@
import { Observable } from "rxjs";
import { map } from "rxjs/operators";
import { ConfigurationService } from "./configuration.service";
// TODO : No need for a separated service, just use steam-downloader.service.ts
export class AuthUserService {
private static instance: AuthUserService;
private readonly configService: ConfigurationService;
private readonly STEAM_USERNAME_KEY = "STEAM-USERNAME";
private readonly STEAM_ID_KEY = "STEAM-ID";
public static getInstance(): AuthUserService {
if (!AuthUserService.instance) {
AuthUserService.instance = new AuthUserService();
}
return AuthUserService.instance;
}
private constructor() {
this.configService = ConfigurationService.getInstance();
}
public sessionExist(): boolean {
return !!this.configService.get(this.STEAM_USERNAME_KEY);
}
public get sessionExist$(): Observable<boolean> {
return this.configService.watch(this.STEAM_USERNAME_KEY).pipe(map(v => !!v));
}
public setSteamSession(username: string, stay = true): void {
this.configService.set(this.STEAM_USERNAME_KEY, username, stay);
}
public setSteamID(steamID: string): void {
this.configService.set(this.STEAM_ID_KEY, steamID);
}
public getSteamUsername(): string {
return this.configService.get(this.STEAM_USERNAME_KEY);
}
public deleteSteamSession(): void {
this.configService.delete(this.STEAM_USERNAME_KEY);
}
}
@@ -1,73 +0,0 @@
import { BsStore } from "shared/models/bs-store.enum";
import { ConfigurationService } from "../configuration.service";
import { BehaviorSubject } from "rxjs";
import { BSVersion } from "shared/bs-version.interface";
import { ModalExitCode, ModalService } from "../modale.service";
import { ChooseStore } from "renderer/components/modal/modal-types/bs-downgrade/choose-store-modal.component";
import { SteamDownloaderService } from "./steam-downloader.service";
import { OculusDownloaderService } from "./oculus-downloader.service";
import { CustomError } from "shared/models/exceptions/custom-error.class";
export class BsDownloaderService {
private static instance: BsDownloaderService;
public static getInstance(): BsDownloaderService {
if (!BsDownloaderService.instance) {
BsDownloaderService.instance = new BsDownloaderService();
}
return BsDownloaderService.instance;
}
private readonly config: ConfigurationService;
private readonly modals: ModalService;
private readonly steamDownloader: SteamDownloaderService;
private readonly oculusDownloader: OculusDownloaderService; // TODO : create oculus downloader
private readonly downloadingVersion$ = new BehaviorSubject<BSVersion>(null); // <= TODO : will replace obs in SteamDownloaderService
private constructor(){
this.config = ConfigurationService.getInstance();
this.modals = ModalService.getInstance();
this.steamDownloader = SteamDownloaderService.getInstance();
this.oculusDownloader = OculusDownloaderService.getInstance();
}
public getLastStoreDownloadedFrom(): BsStore | undefined {
const lastStore = this.config.get("lastStoreDownloadedFrom");
if(!lastStore){
return undefined;
}
return lastStore as BsStore;
}
public async chooseStoreToDownloadFrom(): Promise<BsStore | undefined> {
const res = await this.modals.openModal(ChooseStore);
if(res.exitCode !== ModalExitCode.COMPLETED){
return undefined;
}
return res.data;
}
public async downloadVersion(version: BSVersion, from: BsStore): Promise<BSVersion | void> {
if(from === BsStore.STEAM || !version.OculusBinaryId){
return this.steamDownloader.downloadBsVersion(version);
}
if(from === BsStore.OCULUS){
return this.oculusDownloader.downloadBsVersion(version);
}
}
public importVersion(): void {
// TODO : open Modal
// Will replace method in SteamDownloaderService
}
}
@@ -1,86 +0,0 @@
import { Observable, catchError, lastValueFrom, map, of, tap, throwError } from "rxjs";
import { BSVersion } from "shared/bs-version.interface";
import { IpcService } from "../ipc.service";
import { Progression } from "main/helpers/fs.helpers";
import { ProgressBarService } from "../progress-bar.service";
import { CustomError } from "shared/models/exceptions/custom-error.class";
import { NotificationService } from "../notification.service";
import { OculusDownloadInfo } from "main/services/bs-oculus-downloader.service";
import { ModalExitCode, ModalService } from "../modale.service";
import { LoginToMetaModal } from "renderer/components/modal/modal-types/bs-downgrade/login-to-meta-modal.component";
export class OculusDownloaderService {
private static instance: OculusDownloaderService;
public static getInstance(): OculusDownloaderService {
if (!OculusDownloaderService.instance) {
OculusDownloaderService.instance = new OculusDownloaderService();
}
return OculusDownloaderService.instance;
}
private readonly ipc: IpcService;
private readonly progressBar: ProgressBarService;
private readonly notifications: NotificationService;
private readonly modals: ModalService;
private constructor(){
this.ipc = IpcService.getInstance();
this.progressBar = ProgressBarService.getInstance();
this.notifications = NotificationService.getInstance();
this.modals = ModalService.getInstance();
}
private handleDownloadErrors(err: CustomError): void{
if(!err?.code){
// handle unknown error
}
this.notifications.notifyError({title: err.code});
}
private handleDownload(download: Observable<Progression>): Observable<Progression> {
const progress$ = download.pipe(map(progress => (progress.current / progress.total) * 100), catchError(() => of(0)));
this.progressBar.show(progress$, true);
return download.pipe(tap({
error: err => this.handleDownloadErrors(err),
}));
}
private tryAutoDownload(version: BSVersion): Observable<Progression>{
return this.handleDownload(
this.ipc.sendV2("bs-oculus-auto-download", { args: version })
);
}
private doDownloadBsVersion(downloadInfo: OculusDownloadInfo): Observable<Progression>{
return this.handleDownload(
this.ipc.sendV2("bs-oculus-download", { args: downloadInfo })
);
}
public async downloadBsVersion(version: BSVersion): Promise<BSVersion> {
return (async () => {
const autoDownload = await lastValueFrom(this.tryAutoDownload(version)).then(() => true).catch(() => false);
if(autoDownload){
return autoDownload;
}
const [completed, stay] = await this.modals.openModal(LoginToMetaModal).then(res => [res.exitCode === ModalExitCode.COMPLETED, res.data]);
if(!completed){
return completed;
}
return lastValueFrom(this.doDownloadBsVersion({ version, stay })).then(() => true);
})().then(() => version)
.finally(() => this.progressBar.hide(true));
}
}
@@ -0,0 +1,16 @@
import { BehaviorSubject, Observable } from "rxjs";
import { BSVersion } from "shared/bs-version.interface";
export abstract class AbstractBsDownloaderService {
private static readonly __downloadingVersion$ = new BehaviorSubject<BSVersion>(null);
private static readonly __isVerifying$ = new BehaviorSubject<boolean>(false);
protected get _downloadingVersion$(){ return AbstractBsDownloaderService.__downloadingVersion$; }
protected get _isVerifying$(){ return AbstractBsDownloaderService.__isVerifying$; }
public get downloadingVersion$(): Observable<BSVersion>{ return this._downloadingVersion$.asObservable(); }
public get downloadingVersion(): BSVersion{ return this._downloadingVersion$.getValue(); }
public get isVerifying$(): Observable<boolean>{ return this._isVerifying$.asObservable(); }
public get isVerifying(): boolean{ return this._isVerifying$.getValue(); }
}
@@ -0,0 +1,97 @@
import { BsStore } from "shared/models/bs-store.enum";
import { ConfigurationService } from "../configuration.service";
import { BSVersion } from "shared/bs-version.interface";
import { ModalExitCode, ModalService } from "../modale.service";
import { ChooseStore } from "renderer/components/modal/modal-types/bs-downgrade/choose-store-modal.component";
import { SteamDownloaderService } from "./steam-downloader.service";
import { OculusDownloaderService } from "./oculus-downloader.service";
import { DownloaderServiceInterface } from "./bs-store-downloader.interface";
import { AbstractBsDownloaderService } from "./abstract-bs-downloader.service";
import { BSVersionManagerService } from "../bs-version-manager.service";
export class BsDownloaderService extends AbstractBsDownloaderService {
private static instance: BsDownloaderService;
public static getInstance(): BsDownloaderService {
if (!BsDownloaderService.instance) {
BsDownloaderService.instance = new BsDownloaderService();
}
return BsDownloaderService.instance;
}
private readonly config: ConfigurationService;
private readonly modals: ModalService;
private readonly steamDownloader: SteamDownloaderService;
private readonly oculusDownloader: OculusDownloaderService;
private readonly versionManager: BSVersionManagerService;
private constructor(){
super();
this.config = ConfigurationService.getInstance();
this.modals = ModalService.getInstance();
this.steamDownloader = SteamDownloaderService.getInstance();
this.oculusDownloader = OculusDownloaderService.getInstance();
this.versionManager = BSVersionManagerService.getInstance();
}
private getStoreDownloader(bsStore: BsStore): DownloaderServiceInterface{
switch(bsStore){
case BsStore.OCULUS:
return this.oculusDownloader;
case BsStore.STEAM:
return this.steamDownloader;
default:
throw new Error("Unknown store");
}
}
private resetDownloadState(){
this._downloadingVersion$.next(null);
this._isVerifying$.next(false);
}
public getLastStoreDownloadedFrom(): BsStore | undefined {
const lastStore = this.config.get("lastStoreDownloadedFrom");
if(!lastStore){
return undefined;
}
return lastStore as BsStore;
}
public async chooseStoreToDownloadFrom(): Promise<BsStore | undefined> {
const res = await this.modals.openModal(ChooseStore);
if(res.exitCode !== ModalExitCode.COMPLETED){
return undefined;
}
return res.data;
}
public downloadVersion(version: BSVersion, from: BsStore): Promise<BSVersion> {
return this.getStoreDownloader(from).downloadBsVersion(version).finally(() => {
this.resetDownloadState();
this.versionManager.askInstalledVersions();
});
}
public verifyBsVersion(version: BSVersion){
this._isVerifying$.next(true);
return this.getStoreDownloader(version.store).verifyBsVersion(version).finally(() => {
this.resetDownloadState();
this.versionManager.askInstalledVersions();
});
}
public async stopDownload(): Promise<void> {
return Promise.all([
this.steamDownloader.stopDownload(),
this.oculusDownloader.stopDownload()
]).then(() => {});
}
}
@@ -0,0 +1,6 @@
import { BSVersion } from "shared/bs-version.interface";
export interface DownloaderServiceInterface {
downloadBsVersion(version: BSVersion): Promise<BSVersion>;
verifyBsVersion(version: BSVersion): Promise<BSVersion>;
}
@@ -0,0 +1,118 @@
import { Observable, Subscription, catchError, finalize, lastValueFrom, map, of, take, tap } from "rxjs";
import { BSVersion } from "shared/bs-version.interface";
import { IpcService } from "../ipc.service";
import { Progression } from "main/helpers/fs.helpers";
import { ProgressBarService } from "../progress-bar.service";
import { CustomError } from "shared/models/exceptions/custom-error.class";
import { NotificationService } from "../notification.service";
import { ModalExitCode, ModalService } from "../modale.service";
import { LoginToMetaModal } from "renderer/components/modal/modal-types/bs-downgrade/login-to-meta-modal.component";
import { DownloaderServiceInterface } from "./bs-store-downloader.interface";
import { AbstractBsDownloaderService } from "./abstract-bs-downloader.service";
import { DownloadInfo } from "main/services/bs-version-download/bs-steam-downloader.service";
export class OculusDownloaderService extends AbstractBsDownloaderService implements DownloaderServiceInterface{
private static instance: OculusDownloaderService;
public static getInstance(): OculusDownloaderService {
if (!OculusDownloaderService.instance) {
OculusDownloaderService.instance = new OculusDownloaderService();
}
return OculusDownloaderService.instance;
}
private readonly ipc: IpcService;
private readonly progressBar: ProgressBarService;
private readonly notifications: NotificationService;
private readonly modals: ModalService;
private constructor(){
super();
this.ipc = IpcService.getInstance();
this.progressBar = ProgressBarService.getInstance();
this.notifications = NotificationService.getInstance();
this.modals = ModalService.getInstance();
}
private handleDownloadErrors(err: CustomError): void{
if(!err?.code){
// handle unknown error
}
this.notifications.notifyError({title: err.code});
}
private handleDownload(download: Observable<Progression<BSVersion>>): Observable<Progression<BSVersion>> {
const progress$ = download.pipe(map(progress => (progress.current / progress.total) * 100), catchError(() => of(0)));
this.progressBar.show(progress$, true);
const subs: Subscription[] = [];
subs.push(
download.pipe(take(1), catchError(() => of(null))).subscribe(data => data && this._downloadingVersion$.next(data.data))
)
return download.pipe(
tap({
error: err => this.handleDownloadErrors(err),
}),
finalize(() => subs.forEach(sub => sub.unsubscribe()))
);
}
private tryAutoDownload(downloadInfo: DownloadInfo): Observable<Progression<BSVersion>>{
return this.handleDownload(
this.ipc.sendV2<Progression<BSVersion>>("bs-oculus-auto-download", { args: downloadInfo })
);
}
private startDownloadBsVersion(downloadInfo: DownloadInfo): Observable<Progression<BSVersion>>{
return this.handleDownload(
this.ipc.sendV2<Progression<BSVersion>>("bs-oculus-download", { args: downloadInfo })
);
}
private async doDownloadBsVersion(bsVersion: BSVersion, isVerification: boolean): Promise<BSVersion> {
return (async () => {
const autoDownload = await lastValueFrom(this.tryAutoDownload({ bsVersion, isVerification })).then(() => true).catch(() => false);
if(autoDownload){
return autoDownload;
}
const [completed, stay] = await this.modals.openModal(LoginToMetaModal).then(res => [res.exitCode === ModalExitCode.COMPLETED, res.data]);
if(!completed){
return completed;
}
return lastValueFrom(this.startDownloadBsVersion({ bsVersion, isVerification, stay })).then(() => true);
})().then(() => bsVersion)
.finally(() => this.progressBar.hide(true));
}
public downloadBsVersion(version: BSVersion): Promise<BSVersion> {
return this.doDownloadBsVersion(version, false);
}
public async verifyBsVersion(version: BSVersion): Promise<BSVersion> {
return this.doDownloadBsVersion(version, true);
}
public stopDownload(): Promise<void>{
return lastValueFrom(this.ipc.sendV2<void>("bs-oculus-stop-download"));
}
public hasAuthToken(): Promise<boolean>{
return lastValueFrom(this.ipc.sendV2<boolean>("bs-oculus-has-auth-token"));
}
public clearAuthToken(): Promise<void>{
return lastValueFrom(this.ipc.sendV2<void>("bs-oculus-clear-auth-token"));
}
}
@@ -1,9 +1,7 @@
import { DownloadInfo } from "main/services/bs-installer.service";
import { DownloadSteamInfo } from "main/services/bs-version-download/bs-steam-downloader.service";
import { BehaviorSubject, Observable, ReplaySubject, Subscription, lastValueFrom, throwError } from "rxjs";
import { filter, map, share, take, tap, throttleTime } from "rxjs/operators";
import { BSVersion } from "shared/bs-version.interface";
import { AuthUserService } from "../auth-user.service";
import { BSVersionManagerService } from "../bs-version-manager.service";
import { IpcService } from "../ipc.service";
import { ModalExitCode, ModalService } from "../modale.service";
import { NotificationService } from "../notification.service";
@@ -13,22 +11,13 @@ import { SteamGuardModal } from "renderer/components/modal/modal-types/bs-downgr
import { LinkOpenerService } from "../link-opener.service";
import { DepotDownloaderErrorEvent, DepotDownloaderEvent, DepotDownloaderEventType, DepotDownloaderInfoEvent, DepotDownloaderWarningEvent } from "../../../shared/models/depot-downloader.model";
import { SteamMobileApproveModal } from "renderer/components/modal/modal-types/bs-downgrade/steam-mobile-approve-modal.component";
import { DownloaderServiceInterface } from "./bs-store-downloader.interface";
import { AbstractBsDownloaderService } from "./abstract-bs-downloader.service";
export class SteamDownloaderService {
export class SteamDownloaderService extends AbstractBsDownloaderService implements DownloaderServiceInterface{
private static instance: SteamDownloaderService;
private readonly modalService: ModalService;
private readonly ipcService: IpcService;
private readonly bsVersionManager: BSVersionManagerService;
private readonly authService: AuthUserService;
private readonly progressBarService: ProgressBarService;
private readonly notificationService: NotificationService;
private readonly linkOpener: LinkOpenerService;
private readonly isVerification$ = new BehaviorSubject(false);
public readonly currentBsVersionDownload$ = new BehaviorSubject<BSVersion>(null);
public readonly downloadProgress$ = new BehaviorSubject(0);
public static getInstance(): SteamDownloaderService {
if (!SteamDownloaderService.instance) {
SteamDownloaderService.instance = new SteamDownloaderService();
@@ -36,11 +25,20 @@ export class SteamDownloaderService {
return SteamDownloaderService.instance;
}
private readonly modalService: ModalService;
private readonly ipcService: IpcService;
private readonly progressBarService: ProgressBarService;
private readonly notificationService: NotificationService;
private readonly linkOpener: LinkOpenerService;
private readonly STEAM_SESSION_USERNAME_KEY = "STEAM-USERNAME";
public readonly downloadProgress$ = new BehaviorSubject(0);
private constructor() {
super();
this.ipcService = IpcService.getInstance();
this.modalService = ModalService.getInstance();
this.bsVersionManager = BSVersionManagerService.getInstance();
this.authService = AuthUserService.getInstance();
this.progressBarService = ProgressBarService.getInstance();
this.notificationService = NotificationService.getInstance();
this.linkOpener = LinkOpenerService.getInstance();
@@ -50,6 +48,11 @@ export class SteamDownloaderService {
return lastValueFrom(this.ipcService.sendV2<boolean>("is-dotnet-6-installed"));
}
private setSteamSession(username: string): void { localStorage.setItem(this.STEAM_SESSION_USERNAME_KEY, username); }
private getSteamUsername(): string { return localStorage.getItem(this.STEAM_SESSION_USERNAME_KEY); }
public deleteSteamSession(): void { localStorage.removeItem(this.STEAM_SESSION_USERNAME_KEY); }
public sessionExist(): boolean { return !!localStorage.getItem(this.STEAM_SESSION_USERNAME_KEY); }
private async showDotNetNotInstalledError(): Promise<void> {
const choice = await this.notificationService.notifyError({
duration: 11_000,
@@ -63,28 +66,14 @@ export class SteamDownloaderService {
}
}
public get isDownloading(): boolean {
return !!this.currentBsVersionDownload$.value;
}
public async getInstallationFolder(): Promise<string> {
return lastValueFrom(this.ipcService.sendV2<string>("bs-download.installation-folder"));
}
public get isVerification(): boolean {
return this.isVerification$.value;
}
public setInstallationFolder(path: string): Observable<string> {
return this.ipcService.sendV2<string>("bs-download.set-installation-folder", { args: path });
}
public async importVersion(pathToImport: string): Promise<boolean> {
return lastValueFrom(this.ipcService.sendV2<void>("bs-download.import-version", { args: pathToImport }))
.then(() => true)
.catch(() => false);
}
// ### Downloading
private handleInfoEvents(events$: Observable<DepotDownloaderEvent>): Subscription[] {
@@ -97,7 +86,7 @@ export class SteamDownloaderService {
).subscribe(startData => {
const downloadVersion = JSON.parse(startData) as BSVersion;
if(typeof downloadVersion === "object" && downloadVersion?.BSVersion){
this.currentBsVersionDownload$.next(downloadVersion);
this._downloadingVersion$.next(downloadVersion);
}
}));
@@ -135,7 +124,7 @@ export class SteamDownloaderService {
filter(event => event.subType === DepotDownloaderInfoEvent.Finished),
take(1),
).subscribe(() => {
if(this.isVerification){
if(this.isVerifying){
return this.notificationService.notifySuccess({title: "notifications.bs-download.success.titles.verification-finished"});
}
return this.notificationService.notifySuccess({title: "notifications.bs-download.success.titles.download-success"});
@@ -187,7 +176,7 @@ export class SteamDownloaderService {
}).pipe(
tap({
error: (e) => {
this.authService.deleteSteamSession();
this.deleteSteamSession();
!silent && this.hanndleErrorEvent(e)
}
}),
@@ -196,26 +185,26 @@ export class SteamDownloaderService {
}
private tryAutoDownloadBsVersion(downloadInfo: DownloadInfo){
private tryAutoDownloadBsVersion(downloadInfo: DownloadSteamInfo){
if(!this.authService.sessionExist()){
if(!this.sessionExist()){
return throwError(() => new Error("No session"));
}
const infos: DownloadInfo = {...downloadInfo, username: this.authService.getSteamUsername()}
const infos: DownloadSteamInfo = {...downloadInfo, username: this.getSteamUsername()}
return this.wrapDownload(
this.ipcService.sendV2<DepotDownloaderEvent>("auto-download-bs-version", { args: infos }),
true
);
}
private startDownload(downloadInfo: DownloadInfo){
private startDownload(downloadInfo: DownloadSteamInfo){
return this.wrapDownload(
this.ipcService.sendV2<DepotDownloaderEvent>("download-bs-version", { args: downloadInfo })
);
}
private startQrCodeDownload(downloadInfo: DownloadInfo){
private startQrCodeDownload(downloadInfo: DownloadSteamInfo){
return this.wrapDownload(
this.ipcService.sendV2<DepotDownloaderEvent>("download-bs-version-qr", { args: downloadInfo })
);
@@ -228,7 +217,6 @@ export class SteamDownloaderService {
}
this.progressBarService.show(this.downloadProgress$, true);
this.isVerification$.next(isVerification);
const downloadPromise = (async () => {
@@ -238,7 +226,7 @@ export class SteamDownloaderService {
return Promise.reject(new Error("DotNet not installed"));
}
const downloadInfo: DownloadInfo = {bsVersion, isVerification}
const downloadInfo: DownloadSteamInfo = {bsVersion, isVerification}
const autoDownload = await lastValueFrom(this.tryAutoDownloadBsVersion(downloadInfo)).then(() => true).catch(() => false);
@@ -255,7 +243,7 @@ export class SteamDownloaderService {
}
if(loginRes.data.stay){
this.authService.setSteamSession(loginRes.data.username);
this.setSteamSession(loginRes.data.username);
}
const download$ = loginRes.data.method === "qr" ? qrCodeDownload$ : this.startDownload({...downloadInfo, username: loginRes.data.username, password: loginRes.data.password, stay: loginRes.data.stay});
@@ -268,10 +256,7 @@ export class SteamDownloaderService {
return downloadPromise.then(() => {}).finally(() => {
this.downloadProgress$.next(0);
this.currentBsVersionDownload$.next(null);
this.progressBarService.hide(true);
this.isVerification$.next(false);
this.bsVersionManager.askInstalledVersions();
});
}
@@ -284,10 +269,6 @@ export class SteamDownloaderService {
return this.doDownloadBsVersion(version).then(() => version);
}
public verifyBsVersionFiles(version: BSVersion): Promise<BSVersion> {
return this.doDownloadBsVersion(version, true).then(() => version);
}
public verifyBsVersion(version: BSVersion): Promise<BSVersion> {
return this.doDownloadBsVersion(version, true).then(() => version);
}
@@ -1,19 +1,23 @@
import { BSVersion } from "shared/bs-version.interface";
import { BehaviorSubject, Observable } from "rxjs";
import { BehaviorSubject, Observable, Subscription, lastValueFrom, map, shareReplay, throwError } from "rxjs";
import { IpcService } from "./ipc.service";
import { ModalExitCode, ModalService } from "./modale.service";
import { NotificationService } from "./notification.service";
import { ProgressBarService } from "./progress-bar.service";
import { EditVersionModal } from "renderer/components/modal/modal-types/edit-version-modal.component";
import { popElement } from "shared/helpers/array.helpers";
import { ImportVersionModal } from "renderer/components/modal/modal-types/import-version-modal.component";
import { Progression } from "main/helpers/fs.helpers";
import { ImportVersionOptions } from "main/services/bs-local-version.service";
export class BSVersionManagerService {
private static instance: BSVersionManagerService;
private readonly ipcService: IpcService;
private readonly modalService: ModalService;
private readonly notificationService: NotificationService;
private readonly progressBarService: ProgressBarService;
private readonly notification: NotificationService;
private readonly progressBar: ProgressBarService;
private readonly modals: ModalService;
public readonly installedVersions$: BehaviorSubject<BSVersion[]> = new BehaviorSubject([]);
public readonly availableVersions$: BehaviorSubject<BSVersion[]> = new BehaviorSubject([]);
@@ -21,8 +25,9 @@ export class BSVersionManagerService {
private constructor() {
this.ipcService = IpcService.getInstance();
this.modalService = ModalService.getInstance();
this.notificationService = NotificationService.getInstance();
this.progressBarService = ProgressBarService.getInstance();
this.notification = NotificationService.getInstance();
this.progressBar = ProgressBarService.getInstance();
this.modals = ModalService.getInstance();
this.askAvailableVersions().then(() => this.askInstalledVersions());
}
@@ -70,7 +75,7 @@ export class BSVersionManagerService {
}
return this.ipcService.send<BSVersion>("bs-version.edit", { args: { version, name: modalRes.data.name, color: modalRes.data.color } }).then(res => {
if (!res.success) {
this.notificationService.notifyError({
this.notification.notifyError({
title: `notifications.custom-version.errors.titles.${res.error.title}`,
...(res.error.message && { desc: `notifications.custom-version.errors.msg.${res.error.message}` }),
});
@@ -82,7 +87,7 @@ export class BSVersionManagerService {
}
public async cloneVersion(version: BSVersion): Promise<BSVersion> {
if (!this.progressBarService.require()) {
if (!this.progressBar.require()) {
return null;
}
const modalRes = await this.modalService.openModal(EditVersionModal, { version, clone: true });
@@ -92,22 +97,76 @@ export class BSVersionManagerService {
if (modalRes.data.name?.length < 2) {
return null;
}
this.progressBarService.showFake(0.01);
this.progressBar.showFake(0.01);
return this.ipcService.send<BSVersion>("bs-version.clone", { args: { version, name: modalRes.data.name, color: modalRes.data.color } }).then(res => {
this.progressBarService.hide(true);
this.progressBar.hide(true);
if (!res.success) {
this.notificationService.notifyError({
this.notification.notifyError({
title: `notifications.custom-version.errors.titles.${res.error.title}`,
...(res.error.message && { desc: `notifications.custom-version.errors.msg.${res.error.message}` }),
});
return null;
}
this.notificationService.notifySuccess({ title: "notifications.custom-version.success.titles.CloningFinished" });
this.notification.notifySuccess({ title: "notifications.custom-version.success.titles.CloningFinished" });
this.askInstalledVersions();
return res.data;
});
}
public importVersion(): Observable<Progression<BSVersion>> {
if(!this.progressBar.require()){
return throwError(() => new Error("Action already in progress"));
}
const obs$ = new Observable<Progression<BSVersion>>(obs => {
const subs: Subscription[] = [];
(async () => {
const resModal = await this.modals.openModal(ImportVersionModal);
if(resModal.exitCode !== ModalExitCode.COMPLETED){
return;
}
const store = resModal.data;
const folderRes = await lastValueFrom(this.ipcService.sendV2<{ canceled: boolean; filePaths: string[] }>("choose-folder"));
if(!folderRes || folderRes.canceled || !folderRes.filePaths?.length){
return;
}
const import$ = this.ipcService.sendV2<Progression<BSVersion>, ImportVersionOptions>("import-version", { args: {fromPath: folderRes.filePaths.at(0), store} });
subs.push(import$.subscribe(obs));
await lastValueFrom(import$);
this.notification.notifySuccess({ title: "notifications.bs-import-version.success.imported.title", duration: 3_000 });
})().then(() => {
obs.complete()
}).catch(err => {
this.notification.notifyError({ title: "notifications.types.error", desc: "notifications.bs-import-version.errors.import-error.desc" });
obs.error(err)
}).finally(() => {
this.askInstalledVersions();
this.progressBar.hide(true);
});
return () => {
subs.forEach(sub => sub.unsubscribe());
}
}).pipe(
shareReplay({ bufferSize: 1, refCount: true })
);
// TODO : progression wrong
this.progressBar.show(obs$.pipe(map(progress => (progress.current / progress.total) * 100)), true);
return obs$;
}
public getVersionPath(version: BSVersion): Observable<string> {
return this.ipcService.sendV2("get-version-full-path", { args: version });
}
+1 -1
View File
@@ -15,7 +15,7 @@ export interface BSVersion extends PartialBSVersion {
steam?: boolean;
oculus?: boolean;
color?: string;
downloadedFrom?: BsStore;
store?: BsStore;
OculusBinaryId?: string;
}