[feature-274] can now download oculus version from the interface

This commit is contained in:
MathieuG-P
2023-10-18 01:13:46 +02:00
parent 08d1add47b
commit c0f2baffa6
17 changed files with 239 additions and 80 deletions
@@ -1,6 +1,6 @@
import { BSInstallerService, DownloadInfo } from "../services/bs-installer.service";
import { InstallationLocationService } from "../services/installation-location.service";
import { IpcService } from "../services/ipc.service";
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();
@@ -0,0 +1,15 @@
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));
});
+2 -1
View File
@@ -1,4 +1,3 @@
import "./bs-download-ipcs";
import "./os-controls-ipcs";
import "./bs-launcher-ipcs";
import "./bs-version-ipcs";
@@ -12,3 +11,5 @@ 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";
+2
View File
@@ -60,6 +60,8 @@ const createWindow = async (window: AppWindow = "launcher.html") => {
await installExtensions();
}
WindowManagerService.getInstance().openWindow(window);
BsOculusDownloaderService.getInstance().clearTokenCookie();
};
const initServicesMustBeInitialized = () => {
+4 -3
View File
@@ -4,7 +4,7 @@ import { CustomError } from "../../shared/models/exceptions/custom-error.class";
import { mkdirs, createWriteStream, pathExists, writeFile } from "fs-extra";
import path from "path";
import { inflate } from "pako"
import { Observable, lastValueFrom, tap } from "rxjs";
import { Observable, ReplaySubject, lastValueFrom, share, tap } from "rxjs";
import { Progression, hashFile } from "../helpers/fs.helpers";
export class OculusDownloader {
@@ -99,7 +99,7 @@ export class OculusDownloader {
return () => {
canceled = true;
}
});
}).pipe(share({connector: () => new ReplaySubject(1)}));
}
public downloadApp(options: OculusDownloaderOptions): Observable<Progression>{
@@ -148,9 +148,10 @@ export class OculusDownloader {
})().then(() => subscriber.complete()).catch(err => subscriber.error(err));
return () => {
console.log("C FINI MAIN")
this.isDownloading = false;
}
});
}).pipe(share({connector: () => new ReplaySubject(1)}));
}
public stopDownload(){
@@ -5,8 +5,9 @@ import log from "electron-log";
import { CustomError } from "../../shared/models/exceptions/custom-error.class";
import { OculusDownloader } from "../models/oculus-downloader.class";
import { Cookie, session } from "electron";
import { pathExist } from "../helpers/fs.helpers";
import { Progression, pathExist } from "../helpers/fs.helpers";
import { BSLocalVersionService } from "./bs-local-version.service";
import { Observable, Subscription } from "rxjs";
export class BsOculusDownloaderService {
@@ -72,7 +73,7 @@ export class BsOculusDownloaderService {
return cookie.expirationDate > msToS(Date.now());
}
private async getTokenFromCookie(): Promise<string | undefined> {
public async getTokenFromCookie(): 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)){
@@ -82,11 +83,11 @@ export class BsOculusDownloaderService {
return undefined;
}
public async getUserFromMetaAuth(saveToken: boolean): Promise<string>{
private async getUserTokenFromMetaAuth(saveToken: boolean): Promise<string>{
const redirectUrl = "https://developer.oculus.com/manage/";
const loginUrl = `https://auth.oculus.com/login/?redirect_uri=${encodeURIComponent(redirectUrl)}`;
const window = await this.windows.openWindow(loginUrl, { frame: true });
const window = await this.windows.openWindow(loginUrl, { frame: true, width: 650, height: 800 });
let timout: NodeJS.Timeout;
@@ -113,15 +114,17 @@ export class BsOculusDownloaderService {
});
}).finally(() => {
clearTimeout(timout);
if(window.isDestroyed()){ return; }
if(!saveToken){
window.webContents.session.clearStorageData();
}
if(!window.isDestroyed() && window.isClosable()){
if(window.isClosable()){
window.close();
}
clearTimeout(timout);
});
return promise;
@@ -146,31 +149,50 @@ export class BsOculusDownloaderService {
return destPath;
}
public async downloadVersion(version: BSVersion){
// await this.clearTokenCookie();
const token = await this.getUserFromMetaAuth(true);
const dest = await this.getPathNotAleardyExist(await this.versions.getVersionPath(version));
public downloadVersion(downloadInfo: OculusDownloadInfo){
const s = this.oculusDownloader.downloadApp({ accessToken: token, binaryId: "1387243574708751", destination: dest }).subscribe({
next: a => console.log(a),
error: a => console.error(a),
complete: () => console.log("complete")
});
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: "1387243574708751", destination: dest }).subscribe(obs);
})().catch(err => obs.error(err));
return () => {
sub?.unsubscribe();
this.oculusDownloader.stopDownload();
}
})
}
public async autoDownloadVersion(version: BSVersion){
const token = await this.getTokenFromCookie();
public autoDownloadVersion(version: BSVersion): Observable<Progression>{
if(!token){
throw new CustomError("No token has been found while try to auto download Beat Saber from Oculus", "TOKEN_NEEDED");
}
return new Observable<Progression>(obs => {
const dest = await this.getPathNotAleardyExist(await this.versions.getVersionPath(version));
let sub: Subscription;
const s = this.oculusDownloader.downloadApp({ accessToken: token, binaryId: "1387243574708751", destination: dest }).subscribe({
next: a => console.log(a),
error: a => console.error(a),
complete: () => console.log("complete")
(async () => {
const token = await this.getTokenFromCookie();
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: "1387243574708751", destination: dest }).subscribe(obs);
})().catch(err => obs.error(err));
return () => {
console.log("C FINI MAIN 2");
sub?.unsubscribe();
this.oculusDownloader.stopDownload();
}
});
}
@@ -178,4 +200,9 @@ export class BsOculusDownloaderService {
return session.defaultSession.clearStorageData({ storages: ["cookies"], origin: ".oculus.com" })
}
}
export interface OculusDownloadInfo {
version: BSVersion;
stay?: boolean;
}
@@ -0,0 +1,44 @@
import { ModalComponent, ModalExitCode } from "../../../../services/modale.service";
import { useState } from "react";
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { MetaIcon } from "renderer/components/svgs/icons/meta-icon.component";
import { BsmCheckbox } from "renderer/components/shared/bsm-checkbox.component";
export const LoginToMetaModal: ModalComponent<boolean> = ({ resolver }) => {
const t = useTranslation();
const [stay, setStay] = useState(false);
const submit = () => {
resolver({ exitCode: ModalExitCode.COMPLETED, data: stay });
};
return (
<form className="flex flex-col justify-center items-center w-96 gap-4">
<h1 className="text-3xl uppercase tracking-wide w-full text-center">Connect to Meta</h1>
{/* <BsmImage className="mx-auto h-24" image={BeatConflict} /> */}
<div className="flex justify-center items-center h-28 aspect-square bg-white rounded-full p-4">
<MetaIcon className="w-full h-full"/>
</div>
<p>
<b>Votre token de connexion à Meta est nécéssaire pour télécharger Beat Saber.</b>
</p>
<p>
En vous connectant à Meta, une fenètre de connexion s'ouvrira et vous pourrais alors débuter le processus de connexion à Meta.
Veuillez bien à accepter les cookie sinon quoi, il se pourrait que l'on arrive pas à récupérer votre token de connexion.
</p>
<div className="w-full flex flex-row justify-start items-center gap-1.5">
<BsmCheckbox className="relative z-[1] w-6 aspect-square" checked={stay} onChange={enable => setStay(() => enable)}/>
<span>{t("modals.steam-login.inputs.stay")}</span>
</div>
<BsmButton className="rounded-md flex justify-center items-center transition-all h-10 w-full" typeColor="primary" text="Connect to Meta" withBar={false} onClick={submit}/>
</form>
);
};
@@ -2,7 +2,7 @@ import { useState, useEffect } from "react";
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { ModalComponent, ModalExitCode, ModalService } from "renderer/services/modale.service";
import { WhyCredentialsModal } from "./why-credentials-modal.component";
import { WhySteamCredentialsModal } from "./why-steam-credentials-modal.component";
import { useService } from "renderer/hooks/use-service.hook";
import { Observable } from "rxjs";
import { useObservable } from "renderer/hooks/use-observable.hook";
@@ -10,7 +10,7 @@ import { QRCodeSVG } from "qrcode.react";
import { BsmCheckbox } from "renderer/components/shared/bsm-checkbox.component";
import { BsmBasicSpinner } from "renderer/components/shared/bsm-basic-spinner/bsm-basic-spinner.component";
export const LoginModal: ModalComponent<
export const LoginToSteamModal: ModalComponent<
{ username: string; password: string; stay: boolean, method: "form"|"qr" },
{ qrCode$: Observable<string>, logged$: Observable<string> }
> = ({ resolver, data }) => {
@@ -47,7 +47,7 @@ export const LoginModal: ModalComponent<
};
const whyCredentials = () => {
modal.openModal(WhyCredentialsModal);
modal.openModal(WhySteamCredentialsModal);
};
return (
@@ -1,11 +1,11 @@
import { ModalComponent, ModalExitCode } from "../../../services/modale.service";
import { ModalComponent, ModalExitCode } from "../../../../services/modale.service";
import { useState } from "react";
import BeatConflict from "../../../../../assets/images/apngs/beat-conflict.png";
import BeatConflict from "../../../../../../assets/images/apngs/beat-conflict.png";
import { BsmImage } from "renderer/components/shared/bsm-image.component";
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { useTranslation } from "renderer/hooks/use-translation.hook";
export const GuardModal: ModalComponent<string> = ({ resolver }) => {
export const SteamGuardModal: ModalComponent<string> = ({ resolver }) => {
const [guardCode, setGuardCode] = useState("");
const t = useTranslation();
@@ -1,6 +1,6 @@
import { ModalComponent, ModalExitCode } from "../../../services/modale.service";
import BeatWaiting from "../../../../../assets/images/apngs/beat-waiting.png";
import LoginMobileAuthImage from "../../../../../assets/images/steam/login_mobile_auth.png";
import { ModalComponent, ModalExitCode } from "../../../../services/modale.service";
import BeatWaiting from "../../../../../../assets/images/apngs/beat-waiting.png";
import LoginMobileAuthImage from "../../../../../../assets/images/steam/login_mobile_auth.png";
import { BsmImage } from "renderer/components/shared/bsm-image.component";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { Observable } from "rxjs";
@@ -3,7 +3,7 @@ import { useTranslation } from "renderer/hooks/use-translation.hook";
import { LinkOpenerService } from "renderer/services/link-opener.service";
import { ModalComponent } from "renderer/services/modale.service";
export const WhyCredentialsModal: ModalComponent<void> = () => {
export const WhySteamCredentialsModal: ModalComponent<void> = () => {
const t = useTranslation();
const linkOpener = useService(LinkOpenerService);
@@ -0,0 +1,18 @@
import React from 'react'
export function MetaIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" id="Layer_1" data-name="Layer 1" viewBox="0 0 287.56 191" {...props}>
<defs>
<linearGradient id="linear-gradient" x1="62.34" y1="101.45" x2="260.34" y2="91.45" gradientTransform="matrix(1, 0, 0, -1, 0, 192)" gradientUnits="userSpaceOnUse">
<stop offset="0" stopColor="#0064e1"/><stop offset="0.4" stopColor="#0064e1"/>
<stop offset="0.83" stopColor="#0073ee"/><stop offset="1" stopColor="#0082fb"/>
</linearGradient>
<linearGradient id="linear-gradient-2" x1="41.42" y1="53" x2="41.42" y2="126" gradientTransform="matrix(1, 0, 0, -1, 0, 192)" gradientUnits="userSpaceOnUse"><stop offset="0" stopColor="#0082fb"/><stop offset="1" stopColor="#0064e0"/></linearGradient>
</defs>
<path style={{fill: "#0081fb"}} d="M31.06,126c0,11,2.41,19.41,5.56,24.51A19,19,0,0,0,53.19,160c8.1,0,15.51-2,29.79-21.76,11.44-15.83,24.92-38,34-52l15.36-23.6c10.67-16.39,23-34.61,37.18-47C181.07,5.6,193.54,0,206.09,0c21.07,0,41.14,12.21,56.5,35.11,16.81,25.08,25,56.67,25,89.27,0,19.38-3.82,33.62-10.32,44.87C271,180.13,258.72,191,238.13,191V160c17.63,0,22-16.2,22-34.74,0-26.42-6.16-55.74-19.73-76.69-9.63-14.86-22.11-23.94-35.84-23.94-14.85,0-26.8,11.2-40.23,31.17-7.14,10.61-14.47,23.54-22.7,38.13l-9.06,16c-18.2,32.27-22.81,39.62-31.91,51.75C84.74,183,71.12,191,53.19,191c-21.27,0-34.72-9.21-43-23.09C3.34,156.6,0,141.76,0,124.85Z"/>
<path style={{fill: "url(#linear-gradient)"}} d="M24.49,37.3C38.73,15.35,59.28,0,82.85,0c13.65,0,27.22,4,41.39,15.61,15.5,12.65,32,33.48,52.63,67.81l7.39,12.32c17.84,29.72,28,45,33.93,52.22,7.64,9.26,13,12,19.94,12,17.63,0,22-16.2,22-34.74l27.4-.86c0,19.38-3.82,33.62-10.32,44.87C271,180.13,258.72,191,238.13,191c-12.8,0-24.14-2.78-36.68-14.61-9.64-9.08-20.91-25.21-29.58-39.71L146.08,93.6c-12.94-21.62-24.81-37.74-31.68-45C107,40.71,97.51,31.23,82.35,31.23c-12.27,0-22.69,8.61-31.41,21.78Z"/>
<path style={{fill: "url(#linear-gradient-2)"}} d="M82.35,31.23c-12.27,0-22.69,8.61-31.41,21.78C38.61,71.62,31.06,99.34,31.06,126c0,11,2.41,19.41,5.56,24.51L10.14,167.91C3.34,156.6,0,141.76,0,124.85,0,94.1,8.44,62.05,24.49,37.3,38.73,15.35,59.28,0,82.85,0Z"/>
</svg>
)
}
@@ -38,30 +38,13 @@ export function AvailableVersionsList() {
const downloading = useObservable(steamDownloader.currentBsVersionDownload$.pipe(map(v => !!v)));
const t = useTranslation();
const downloadFromSteam = (version: BSVersion) => {
return steamDownloader.downloadBsVersion(version)
.catch(() => {})
const startDownload = async () => {
const store = bsDownloader.getLastStoreDownloadedFrom() ?? await bsDownloader.chooseStoreToDownloadFrom();
return bsDownloader.downloadVersion(selectedVersion, store)
.catch(console.log)
.finally(() => setSelectedVersion(null));
}
const downloadFromOculus = (version: BSVersion) => {
return undefined; // TODO
}
const startDownload = () => {
return bsDownloader.downloadVersion(selectedVersion);
if(config.get("last-downloaded-from") === "steam"){
return downloadFromSteam(selectedVersion);
}
if(config.get("last-downloaded-from") === "oculus"){
return downloadFromOculus(selectedVersion);
}
};
const importVersion = async () => {
@@ -2,6 +2,8 @@ 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;
@@ -30,6 +30,8 @@ export class BsDownloaderService {
private constructor(){
this.config = ConfigurationService.getInstance();
this.modals = ModalService.getInstance();
this.steamDownloader = SteamDownloaderService.getInstance();
this.oculusDownloader = OculusDownloaderService.getInstance();
}
public getLastStoreDownloadedFrom(): BsStore | undefined {
@@ -42,7 +44,7 @@ export class BsDownloaderService {
return lastStore as BsStore;
}
private async chooseStoreToDownloadFrom(): Promise<BsStore | undefined> {
public async chooseStoreToDownloadFrom(): Promise<BsStore | undefined> {
const res = await this.modals.openModal(ChooseStore);
if(res.exitCode !== ModalExitCode.COMPLETED){
@@ -52,18 +54,15 @@ export class BsDownloaderService {
return res.data;
}
public async downloadVersion(version: BSVersion): Promise<BSVersion | undefined> {
const store = this.getLastStoreDownloadedFrom() ?? await this.chooseStoreToDownloadFrom();
public async downloadVersion(version: BSVersion, from: BsStore): Promise<BSVersion | void> {
if(store === BsStore.STEAM){
if(from === BsStore.STEAM){
return this.steamDownloader.downloadBsVersion(version);
}
if(store === BsStore.OCULUS){
if(from === BsStore.OCULUS){
return this.oculusDownloader.downloadBsVersion(version);
}
return undefined
}
public importVersion(): void {
@@ -1,4 +1,13 @@
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 {
@@ -12,10 +21,68 @@ export class OculusDownloaderService {
return OculusDownloaderService.instance;
}
private constructor(){}
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 version;
return (async () => {
const autoDownload = await lastValueFrom(this.tryAutoDownload(version)).then(() => true).catch(() => false);
console.log("LAAAA");
if(autoDownload){
return autoDownload;
}
const [confirm, stay] = await this.modals.openModal(LoginToMetaModal).then(res => [res.exitCode === ModalExitCode.COMPLETED, res.data]);
if(!confirm){
return confirm;
}
return lastValueFrom(this.doDownloadBsVersion({ version, stay })).then(() => true);
})().then(() => version)
.finally(() => this.progressBar.hide(true));
}
}
@@ -8,11 +8,11 @@ import { IpcService } from "../ipc.service";
import { ModalExitCode, ModalService } from "../modale.service";
import { NotificationService } from "../notification.service";
import { ProgressBarService } from "../progress-bar.service";
import { LoginModal } from "renderer/components/modal/modal-types/login-modal.component";
import { GuardModal } from "renderer/components/modal/modal-types/guard-modal.component";
import { LoginToSteamModal } from "renderer/components/modal/modal-types/bs-downgrade/login-to-steam-modal.component";
import { SteamGuardModal } from "renderer/components/modal/modal-types/bs-downgrade/steam-guard-modal.component";
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/steam-mobile-approve-modal.component";
import { SteamMobileApproveModal } from "renderer/components/modal/modal-types/bs-downgrade/steam-mobile-approve-modal.component";
export class SteamDownloaderService {
private static instance: SteamDownloaderService;
@@ -124,7 +124,7 @@ export class SteamDownloaderService {
take(1),
map(event => event.subType),
).subscribe(async () => {
const res = await this.modalService.openModal(GuardModal);
const res = await this.modalService.openModal(SteamGuardModal);
if(res.exitCode !== ModalExitCode.COMPLETED){
return this.stopDownload();
}
@@ -248,7 +248,7 @@ export class SteamDownloaderService {
const qrCode$ = qrCodeDownload$.pipe(filter(event => event.type === DepotDownloaderEventType.Info && event.subType === DepotDownloaderInfoEvent.QRCode), map(event => event.data as string));
const logged$ = qrCodeDownload$.pipe(filter(event => event.type === DepotDownloaderEventType.Info && event.subType === DepotDownloaderInfoEvent.SteamID), map(event => event.data as string), take(1));
const loginRes = await this.modalService.openModal(LoginModal, { qrCode$, logged$ });
const loginRes = await this.modalService.openModal(LoginToSteamModal, { qrCode$, logged$ });
if(loginRes.exitCode !== ModalExitCode.COMPLETED){
return Promise.resolve();