mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
Merge pull request #293 from Zagrios/feature/login-with-qr-code
[feature] login to Steam with QRCode + handle login approval from Steam app
This commit is contained in:
@@ -1,7 +1,5 @@
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { useState, memo } from "react";
|
||||
import { BsDownloaderService } from "renderer/services/bs-downloader.service";
|
||||
import { distinctUntilChanged, map } from "rxjs/operators";
|
||||
import { useState, memo, ComponentProps } from "react";
|
||||
import defaultImage from "../../../../assets/images/default-version-img.jpg";
|
||||
import dateFormat from "dateformat";
|
||||
import { BsmImage } from "../shared/bsm-image.component";
|
||||
@@ -11,51 +9,38 @@ import { LinkOpenerService } from "renderer/services/link-opener.service";
|
||||
import { motion } from "framer-motion";
|
||||
import { GlowEffect } from "../shared/glow-effect.component";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import equal from "fast-deep-equal";
|
||||
|
||||
export const AvailableVersionItem = memo(function AvailableVersionItem(props: { version: BSVersion }) {
|
||||
const bsDownloaderService = useService(BsDownloaderService)
|
||||
type Props = {
|
||||
version: BSVersion;
|
||||
selected: boolean;
|
||||
onClick: ComponentProps<"li">["onClick"];
|
||||
}
|
||||
|
||||
export const AvailableVersionItem = memo(function AvailableVersionItem({version, selected, onClick}: Props) {
|
||||
const linkOpener = useService(LinkOpenerService)
|
||||
|
||||
const selected = useObservable(
|
||||
bsDownloaderService.selectedBsVersion$.pipe(distinctUntilChanged(), map(version => version?.BSVersion === props.version.BSVersion)),
|
||||
false
|
||||
);
|
||||
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const t = useTranslation();
|
||||
|
||||
const formatedDate = (() => {
|
||||
return dateFormat(+props.version.ReleaseDate * 1000, "ddd. d mmm yyyy");
|
||||
})();
|
||||
|
||||
const toggleSelect = () => {
|
||||
if (bsDownloaderService.isDownloading) {
|
||||
return;
|
||||
}
|
||||
if (selected) {
|
||||
bsDownloaderService.selectedBsVersion$.next(null);
|
||||
} else {
|
||||
bsDownloaderService.selectedBsVersion$.next(props.version);
|
||||
}
|
||||
};
|
||||
|
||||
const openReleasePage = () => {
|
||||
linkOpener.open(props.version.ReleaseURL);
|
||||
linkOpener.open(version.ReleaseURL);
|
||||
};
|
||||
|
||||
const formatedDate = (() => dateFormat(+version.ReleaseDate * 1000, "ddd. d mmm yyyy"))();
|
||||
|
||||
return (
|
||||
<motion.li className="group relative w-72 h-60 transition-transform active:scale-[.98]" onClick={toggleSelect} onHoverStart={() => setHovered(true)} onHoverEnd={() => setHovered(false)}>
|
||||
<motion.li className="group relative w-72 h-60 transition-transform active:scale-[.98]" onClick={onClick} onHoverStart={() => setHovered(true)} onHoverEnd={() => setHovered(false)}>
|
||||
<GlowEffect visible={hovered || selected} className="absolute" />
|
||||
<div className={`relative flex flex-col overflow-hidden rounded-md w-72 h-60 cursor-pointer group-hover:shadow-none duration-300 bg-light-main-color-2 dark:bg-main-color-2 ${!selected && "shadow-lg shadow-gray-900"}`}>
|
||||
<BsmImage image={props.version.ReleaseImg ? props.version.ReleaseImg : defaultImage} errorImage={defaultImage} placeholder={defaultImage} className="absolute top-0 right-0 w-full h-full opacity-40 blur-xl object-cover" loading="lazy" />
|
||||
<BsmImage image={props.version.ReleaseImg ? props.version.ReleaseImg : defaultImage} errorImage={defaultImage} placeholder={defaultImage} className="bg-black w-full h-3/4 object-cover" loading="lazy" />
|
||||
<BsmImage image={version.ReleaseImg ? version.ReleaseImg : defaultImage} errorImage={defaultImage} placeholder={defaultImage} className="absolute top-0 right-0 w-full h-full opacity-40 blur-xl object-cover" loading="lazy" />
|
||||
<BsmImage image={version.ReleaseImg ? version.ReleaseImg : defaultImage} errorImage={defaultImage} placeholder={defaultImage} className="bg-black w-full h-3/4 object-cover" loading="lazy" />
|
||||
<div className="z-[1] p-2 w-full flex items-center justify-between grow">
|
||||
<div>
|
||||
<h2 className="block text-xl font-bold text-white tracking-wider">{props.version.BSVersion}</h2>
|
||||
<h2 className="block text-xl font-bold text-white tracking-wider">{version.BSVersion}</h2>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-400">{formatedDate}</span>
|
||||
</div>
|
||||
{props.version.ReleaseURL && (
|
||||
{version.ReleaseURL && (
|
||||
// eslint-disable-next-line jsx-a11y/anchor-is-valid -- link will be reworked
|
||||
<a
|
||||
onClickCapture={e => {
|
||||
@@ -72,4 +57,4 @@ export const AvailableVersionItem = memo(function AvailableVersionItem(props: {
|
||||
</div>
|
||||
</motion.li>
|
||||
);
|
||||
});
|
||||
}, equal);
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
import { BSVersionManagerService } from "renderer/services/bs-version-manager.service";
|
||||
import { filter, map } from "rxjs/operators";
|
||||
import { useContext } from "react";
|
||||
import { AvailableVersionItem } from "./available-version-item.component";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { AvailableVersionsContext } from "renderer/pages/available-versions-list.components";
|
||||
import equal from "fast-deep-equal";
|
||||
|
||||
export function AvailableVersionsSlide(props: { year: string }) {
|
||||
|
||||
const versionsService = useService(BSVersionManagerService);
|
||||
|
||||
const availableVersions = useObservable(
|
||||
versionsService.availableVersions$.pipe(filter(versions => !!versions?.length), map(() => versionsService.getAvaibleVersionsOfYear(props.year))),
|
||||
[]
|
||||
);
|
||||
type Props = {
|
||||
versions: BSVersion[]
|
||||
}
|
||||
|
||||
export function AvailableVersionsSlide({ versions }: Props) {
|
||||
|
||||
const context = useContext(AvailableVersionsContext);
|
||||
|
||||
const setSelectedVersion = (version: BSVersion) => {
|
||||
if(equal(version, context.selectedVersion)){
|
||||
return context.setSelectedVersion(null);
|
||||
}
|
||||
context.setSelectedVersion(version);
|
||||
}
|
||||
|
||||
return (
|
||||
<ol className="w-full flex items-start justify-center gap-6 shrink-0 content-start flex-wrap p-4 overflow-x-hidden overflow-y-scroll scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900">
|
||||
{availableVersions.map(version => (
|
||||
<AvailableVersionItem key={version.BSManifest} version={version} />
|
||||
{versions.map(version => (
|
||||
<AvailableVersionItem key={version.BSManifest} version={version} selected={equal(version, context.selectedVersion)} onClick={() => setSelectedVersion(version)}/>
|
||||
))}
|
||||
</ol>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { BSVersionManagerService } from "../../services/bs-version-manager.service";
|
||||
import { AvailableVersionsSlide } from "./available-versions-slide.component";
|
||||
import { filter, map } from "rxjs/operators";
|
||||
import { TabNavBar } from "../shared/tab-nav-bar.component";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
@@ -9,22 +8,28 @@ import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
export function AvailableVersionsSlider() {
|
||||
const versionManagerService = useService(BSVersionManagerService);
|
||||
|
||||
const availableVersions = useObservable(versionManagerService.availableVersions$);
|
||||
const [yearIndex, setYearIndex] = useState(0);
|
||||
const availableYears = useObservable(
|
||||
versionManagerService.availableVersions$.pipe(filter(versions => !!versions?.length), map(() => versionManagerService.getAvailableYears())),
|
||||
[]
|
||||
);
|
||||
|
||||
const availableYears = (() => {
|
||||
if(!availableVersions?.length) { return []; }
|
||||
return [...new Set(availableVersions.map(v => v.year))].sort((a, b) => b.localeCompare(a))
|
||||
})();
|
||||
|
||||
const setSelectedYear = (index: number) => {
|
||||
setYearIndex(index);
|
||||
};
|
||||
|
||||
const getVersionOfYear = (year: string) => {
|
||||
return availableVersions.filter(v => v.year === year).sort((a, b) => +b.ReleaseDate - +a.ReleaseDate);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full h-fit max-h-full flex flex-col items-center grow min-h-0 gap-3">
|
||||
<TabNavBar tabIndex={yearIndex} tabsText={availableYears} onTabChange={setSelectedYear} />
|
||||
<ol className="w-full min-h-0 flex transition-transform duration-300" style={{ transform: `translate(${-(yearIndex * 100)}%, 0)` }}>
|
||||
{availableYears.map(year => (
|
||||
<AvailableVersionsSlide key={year} year={year} />
|
||||
<AvailableVersionsSlide key={year} versions={getVersionOfYear(year)} />
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import { BsmImage } from "renderer/components/shared/bsm-image.component";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { ModalComponent, ModalExitCode, ModalService } from "renderer/services/modale.service";
|
||||
import BeatImpatient from "../../../../../assets/images/apngs/beat-impatient.png";
|
||||
import { WhyCredentialsModal } from "./why-credentials-modal.component";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { Observable } from "rxjs";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
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<{ username: string; password: string; stay: boolean }> = ({ resolver }) => {
|
||||
export const LoginModal: ModalComponent<
|
||||
{ username: string; password: string; stay: boolean, method: "form"|"qr" },
|
||||
{ qrCode$: Observable<string>, logged$: Observable<string> }
|
||||
> = ({ resolver, data }) => {
|
||||
|
||||
const modal = useService(ModalService);
|
||||
|
||||
@@ -15,13 +21,29 @@ export const LoginModal: ModalComponent<{ username: string; password: string; st
|
||||
const [password, setPassword] = useState("");
|
||||
const [stay, setStay] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const qrCodeUrl = useObservable(data.qrCode$);
|
||||
const t = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const sub = data.logged$.subscribe({
|
||||
next: logged => {
|
||||
if(!logged) return;
|
||||
// If the logged event is emitted, it means that the user is logged in by the QR code
|
||||
resolver({ exitCode: ModalExitCode.COMPLETED, data: { username, password, stay, method: "qr" } });
|
||||
},
|
||||
error: () => resolver({ exitCode: ModalExitCode.NO_CHOICE}),
|
||||
complete: () => resolver({ exitCode: ModalExitCode.NO_CHOICE})
|
||||
});
|
||||
|
||||
return () => sub.unsubscribe();
|
||||
}, []);
|
||||
|
||||
const loggin = () => {
|
||||
if (!username || !password) {
|
||||
return;
|
||||
}
|
||||
resolver({ exitCode: ModalExitCode.COMPLETED, data: { username, password, stay } });
|
||||
resolver({ exitCode: ModalExitCode.COMPLETED, data: { username, password, stay, method: "form" } });
|
||||
};
|
||||
|
||||
const whyCredentials = () => {
|
||||
@@ -29,48 +51,48 @@ export const LoginModal: ModalComponent<{ username: string; password: string; st
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
className="max-w-xs"
|
||||
onSubmit={e => {
|
||||
e.preventDefault();
|
||||
loggin();
|
||||
}}
|
||||
>
|
||||
<h1 className="text-3xl uppercase tracking-wide w-full text-center text-gray-800 dark:text-gray-200">{t("modals.steam-login.title")}</h1>
|
||||
<BsmImage className="mx-auto h-20" image={BeatImpatient} />
|
||||
|
||||
<div className="mb-2">
|
||||
<label className="block font-bold cursor-pointer tracking-wide text-gray-800 dark:text-gray-200" htmlFor="username">
|
||||
{t("modals.steam-login.inputs.username.label")}
|
||||
</label>
|
||||
<input className="w-full bg-light-main-color-1 dark:bg-main-color-1 px-1 py-[2px] rounded-md outline-none h-8" onChange={e => setUsername(e.target.value)} value={username} type="text" name="username" id="username" placeholder={t("modals.steam-login.inputs.username.placeholder")} />
|
||||
</div>
|
||||
<div className="mb-2 flex flex-col w-full">
|
||||
<label className="block font-bold cursor-pointer tracking-wide text-gray-800 dark:text-gray-200" htmlFor="password">
|
||||
{t("modals.steam-login.inputs.password.label")}
|
||||
</label>
|
||||
<div className="bg-light-main-color-1 dark:bg-main-color-1 rounded-md flex box-border h-8">
|
||||
<input className="grow px-1 py-[2px] outline-none bg-transparent" onChange={e => setPassword(e.target.value)} value={password} type={showPassword ? "text" : "password"} name="password" id="password" placeholder={t("modals.steam-login.inputs.password.placeholder")} />
|
||||
<BsmButton className="shrink-0 m-1 rounded-md p-0.5 !bg-light-main-color-3 dark:!bg-main-color-3" icon={showPassword ? "eye-cross" : "eye"} withBar={false} onClick={() => setShowPassword(prev => !prev)} />
|
||||
<form className="w-[40rem] text-gray-800 dark:text-gray-200" onSubmit={e => {e.preventDefault(); loggin()}}>
|
||||
<h1 className="text-3xl uppercase tracking-wide w-full text-center mb-5">{t("modals.steam-login.title")}</h1>
|
||||
<div className="flex justify-center items-stretch gap-5 min-w-0">
|
||||
<div className="grow flex flex-col gap-2">
|
||||
<div>
|
||||
<label className="block font-bold cursor-pointer tracking-wide" htmlFor="username">
|
||||
{t("modals.steam-login.inputs.username.label")}
|
||||
</label>
|
||||
<input className="w-full bg-light-main-color-1 dark:bg-main-color-1 px-1 py-[2px] rounded-md outline-none h-9" onChange={e => setUsername(e.target.value)} value={username} type="text" name="username" id="username" placeholder={t("modals.steam-login.inputs.username.placeholder")} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block font-bold cursor-pointer tracking-wide" htmlFor="password">
|
||||
{t("modals.steam-login.inputs.password.label")}
|
||||
</label>
|
||||
<div className="bg-light-main-color-1 dark:bg-main-color-1 rounded-md flex box-border h-9">
|
||||
<input className="grow px-1 py-[2px] outline-none bg-transparent" onChange={e => setPassword(e.target.value)} value={password} type={showPassword ? "text" : "password"} name="password" id="password" placeholder={t("modals.steam-login.inputs.password.placeholder")} />
|
||||
<BsmButton className="shrink-0 m-1 rounded-md p-0.5 !bg-light-main-color-3 dark:!bg-main-color-3" icon={showPassword ? "eye-cross" : "eye"} withBar={false} onClick={() => setShowPassword(prev => !prev)} />
|
||||
</div>
|
||||
{password?.length > 64 && <span className="text-orange-700 dark:text-orange-400 text-xs whitespace-normal min-w-0">{t("modals.steam-login.inputs.password.max-length-warning")}</span>}
|
||||
</div>
|
||||
<div className="flex flex-row justify-start items-center gap-1.5 py-3">
|
||||
<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 typeColor="primary" className="rounded-md text-center transition-all h-10" type="submit" withBar={false} text="modals.steam-login.buttons.submit" />
|
||||
<div className="grow flex justify-center items-center gap-1 flex-col">
|
||||
<a className="flex justify-center items-center text-[.85rem] underline" href="https://help.steampowered.com/wizard/HelpWithLogin" target="_blank">{t("modals.steam-login.need-help-to-connect")}</a>
|
||||
<span className="flex justify-center items-center text-[.85rem] underline cursor-pointer" onClick={whyCredentials}>{t("modals.steam-login.why-credentials")}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col max-w-[13rem]">
|
||||
<span className="block font-bold cursor-pointer tracking-wide ">{t("modals.steam-login.inputs.qr.label")}</span>
|
||||
<div className="w-52 h-52 p-3 bg-light-main-color-1 dark:bg-white rounded-md max-w-xs mb-1 flex items-center justify-center">
|
||||
{(qrCodeUrl ?
|
||||
<QRCodeSVG className="w-full h-full text-light-main-color-1 dark:text-white" value={qrCodeUrl} bgColor="currentColor" level="H"/> :
|
||||
<BsmBasicSpinner className="w-full h-full p-11 text-neutral-300" />
|
||||
)}
|
||||
</div>
|
||||
<span className="text-center whitespace-break-spaces text-[.85rem]">
|
||||
{t("modals.steam-login.inputs.qr.note.use-the")}<a className="underline" href="https://store.steampowered.com/mobile" target="_blank">{t("modals.steam-login.inputs.qr.note.steam-mobile-app")}</a> {t("modals.steam-login.inputs.qr.note.to-connect-with-qr")}
|
||||
</span>
|
||||
</div>
|
||||
{password?.length > 64 && <span className="text-orange-700 dark:text-orange-400 text-xs whitespace-normal max-w-full w-full overflow-hidden">{t("modals.steam-login.inputs.password.max-length-warning")}</span>}
|
||||
</div>
|
||||
|
||||
<span onClick={whyCredentials} className="underline my-4 block cursor-pointer">
|
||||
{t("modals.steam-login.why-credentials")}
|
||||
</span>
|
||||
|
||||
<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" type="submit" withBar={false} text="modals.steam-login.buttons.submit" />
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
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";
|
||||
import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
|
||||
|
||||
export const SteamMobileApproveModal: ModalComponent<void, { logged$: Observable<unknown> }> = ({ resolver, data }) => {
|
||||
|
||||
const t = useTranslation();
|
||||
|
||||
useOnUpdate(() => {
|
||||
const sub = data.logged$.subscribe({
|
||||
next: () => resolver({ exitCode: ModalExitCode.COMPLETED }),
|
||||
error: () => resolver({ exitCode: ModalExitCode.NO_CHOICE }),
|
||||
complete: () => resolver({ exitCode: ModalExitCode.NO_CHOICE }),
|
||||
});
|
||||
|
||||
return () => sub.unsubscribe();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<form className="max-w-md flex flex-col items-center">
|
||||
<h1 className="text-3xl uppercase tracking-wide w-full text-center text-gray-800 dark:text-gray-200 m-0">{t("modals.steam-auth-approve.title")}</h1>
|
||||
|
||||
<p className="font-bold my-3">{t("modals.steam-auth-approve.protected-by-mobile-auth")}</p>
|
||||
|
||||
<div className="relative flex w-full bg-light-main-color-1 dark:bg-main-color-1 rounded-md gap-5">
|
||||
<BsmImage className="w-fit ml-7 mt-5" image={LoginMobileAuthImage}/>
|
||||
<div className="flex flex-col justify-center gap-5 pr-2">
|
||||
<p className="text-lg">{t("modals.steam-auth-approve.use-steam-app-to-approve")}</p>
|
||||
</div>
|
||||
<BsmImage className="absolute bottom-1 right-1 w-10 h-10 spin-loading" image={BeatWaiting}/>
|
||||
</div>
|
||||
<a className="underline text-sm mt-2.5" href="https://help.steampowered.com/wizard/HelpWithLoginInfo?lost=8&issueid=402" target="_blank">{t("modals.steam-auth-approve.not-access-to-steam-app")}</a>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -1,8 +1,8 @@
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import { BsDownloaderService } from "renderer/services/bs-downloader.service";
|
||||
import { useEffect, useState } from "react";
|
||||
import { combineLatest, Subscription } from "rxjs";
|
||||
import { useState } from "react";
|
||||
import { distinctUntilChanged, map, of, Subscription, switchMap } from "rxjs";
|
||||
import { BSLauncherService, LaunchMods } from "renderer/services/bs-launcher.service";
|
||||
import { ConfigurationService } from "renderer/services/configuration.service";
|
||||
import { BSUninstallerService } from "renderer/services/bs-uninstaller.service";
|
||||
@@ -13,6 +13,8 @@ import { NavBarItem } from "./nav-bar-item.component";
|
||||
import useFitText from "use-fit-text";
|
||||
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";
|
||||
|
||||
export function BsVersionItem(props: { version: BSVersion }) {
|
||||
const downloaderService = useService(BsDownloaderService);
|
||||
@@ -24,10 +26,28 @@ export function BsVersionItem(props: { version: BSVersion }) {
|
||||
const { state } = useLocation() as { state: BSVersion };
|
||||
const { fontSize, ref } = useFitText();
|
||||
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
const [downloadPercent, setDownloadPercent] = useState(0);
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
const [downloadProgress, setDownloadProgress] = useState(0);
|
||||
const secondColor = useThemeColor("second-color");
|
||||
|
||||
useOnUpdate(() => {
|
||||
const subs: Subscription[] = []
|
||||
|
||||
subs.push(downloaderService.currentBsVersionDownload$.pipe(map(download => equal(download, props.version)), distinctUntilChanged()).subscribe(isDownloading => {
|
||||
setIsDownloading(() => isDownloading);
|
||||
}));
|
||||
|
||||
subs.push(downloaderService.currentBsVersionDownload$.pipe(
|
||||
map(download => equal(download, props.version)),
|
||||
distinctUntilChanged(),
|
||||
switchMap(isDownloading => isDownloading ? downloaderService.downloadProgress$ : of(0)),
|
||||
).subscribe(progress => {
|
||||
setDownloadProgress(() => progress);
|
||||
}));
|
||||
|
||||
return () => subs.forEach(sub => sub.unsubscribe());
|
||||
}, [props.version]);
|
||||
|
||||
const isActive = (): boolean => {
|
||||
return props.version?.BSVersion === state?.BSVersion && props?.version.steam === state?.steam && props?.version.oculus === state?.oculus && props?.version.name === state?.name;
|
||||
};
|
||||
@@ -44,33 +64,12 @@ export function BsVersionItem(props: { version: BSVersion }) {
|
||||
const cancel = () => {
|
||||
const versionDownload = downloaderService.currentBsVersionDownload$.value;
|
||||
const wasVerification = downloaderService.isVerification;
|
||||
downloaderService.cancelDownload().then(async res => {
|
||||
if (!res.success) {
|
||||
return;
|
||||
}
|
||||
if (!wasVerification) {
|
||||
bsUninstallerService.uninstall(versionDownload).then(res => res && verionManagerService.askInstalledVersions());
|
||||
}
|
||||
downloaderService.stopDownload().then(() => {
|
||||
if(wasVerification){ return; }
|
||||
bsUninstallerService.uninstall(versionDownload).then(res => res && verionManagerService.askInstalledVersions());
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const subs: Subscription[] = [];
|
||||
const downloadSub = combineLatest([downloaderService.currentBsVersionDownload$, downloaderService.downloadProgress$]).subscribe(vals => {
|
||||
if (vals[0]?.BSVersion === props.version.BSVersion && vals[0]?.steam === props.version.steam && vals[0].oculus === props.version.oculus && vals[0]?.name === props.version.name) {
|
||||
setDownloading(true);
|
||||
setDownloadPercent(vals[1]);
|
||||
} else {
|
||||
setDownloading(false);
|
||||
setDownloadPercent(0);
|
||||
}
|
||||
});
|
||||
subs.push(downloadSub);
|
||||
return () => {
|
||||
subs.forEach(s => s.unsubscribe());
|
||||
};
|
||||
}, []);
|
||||
|
||||
const renderIcon = () => {
|
||||
const classes = "w-[19px] h-[19px] mr-[5px] shrink-0";
|
||||
if (props.version.steam) {
|
||||
@@ -100,7 +99,7 @@ export function BsVersionItem(props: { version: BSVersion }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<NavBarItem onCancel={cancel} progress={downloading ? downloadPercent : 0} isActive={isActive() && !downloading} isDownloading={downloading}>
|
||||
<NavBarItem onCancel={cancel} progress={downloadProgress} isActive={isActive() && !isDownloading} isDownloading={isDownloading}>
|
||||
{props.version.name ? (
|
||||
<Tippy content={props.version.BSVersion} placement="right-end" arrow={false} className="font-bold !bg-neutral-900" duration={[100, 0]} animation="shift-away-subtle">
|
||||
<Link onDoubleClick={handleDoubleClick} to={`/bs-version/${props.version.BSVersion}`} state={props.version} className="w-full flex items-center justify-start content-center max-w-full">
|
||||
|
||||
@@ -72,7 +72,7 @@ export function BsmButton({ className, style, imgClassName, iconClassName, icon,
|
||||
const handleClick = (e: MouseEvent<HTMLDivElement>) => !disabled && onClick?.(e);
|
||||
|
||||
return (
|
||||
<div ref={ref} onClick={handleClick} title={t(title)} className={`${className} overflow-hidden cursor-pointer group ${!withBar && !disabled && (!!typeColor || !!color) && "hover:brightness-[1.15]"} ${disabled && "brightness-75 cursor-not-allowed"} ${renderTypeColor}`} style={{ ...style, backgroundColor: primaryColor || color }}>
|
||||
<div ref={ref} onClick={handleClick} title={t(title)} className={`${className} overflow-hidden group ${!withBar && !disabled && (!!typeColor || !!color) && "hover:brightness-[1.15]"} ${disabled ? "brightness-75 cursor-not-allowed" : "cursor-pointer"} ${renderTypeColor}`} style={{ ...style, backgroundColor: primaryColor || color }}>
|
||||
{image && <BsmImage image={image} className={imgClassName} />}
|
||||
{icon && <BsmIcon icon={icon} className={iconClassName ?? "h-full w-full text-gray-800 dark:text-white"} style={{ color: iconColor || textColor }} />}
|
||||
{text &&
|
||||
|
||||
@@ -32,7 +32,7 @@ export default function TitleBar({ template = "index.html" }: { template: AppWin
|
||||
return setPreviewVersion("BETA");
|
||||
}
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
const [maximized, setMaximized] = useState(false);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AvailableVersionsSlider } from "../components/available-versions/available-versions-slider.component";
|
||||
import { BsDownloaderService } from "../services/bs-downloader.service";
|
||||
import { Slideshow } from "renderer/components/slideshow/slideshow.component";
|
||||
import { useState } from "react";
|
||||
import { createContext, useMemo, useState } from "react";
|
||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
@@ -12,28 +12,32 @@ 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 { timer } from "rxjs";
|
||||
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";
|
||||
|
||||
export const AvailableVersionsContext = createContext<{ selectedVersion: BSVersion; setSelectedVersion: (version: BSVersion) => void }>(null);
|
||||
|
||||
export function AvailableVersionsList() {
|
||||
const bsDownloaderService = useService(BsDownloaderService);
|
||||
const versionManagerService = useService(BSVersionManagerService);
|
||||
const bsDownloader = useService(BsDownloaderService);
|
||||
const versionManager = useService(BSVersionManagerService);
|
||||
const progressBar = useService(ProgressBarService);
|
||||
const modal = useService(ModalService);
|
||||
const ipc = useService(IpcService);
|
||||
const installer = useService(BsDownloaderService);
|
||||
const notification = useService(NotificationService);
|
||||
|
||||
const versionSelected = useObservable(bsDownloaderService.selectedBsVersion$);
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
const [selectedVersion, setSelectedVersion] = useState<BSVersion>(null);
|
||||
const contextValue = useMemo(() => ({ selectedVersion, setSelectedVersion }), [selectedVersion]);
|
||||
|
||||
const downloading = useObservable(bsDownloader.currentBsVersionDownload$.pipe(map(v => !!v)));
|
||||
const t = useTranslation();
|
||||
|
||||
const startDownload = () => {
|
||||
setDownloading(() => true);
|
||||
bsDownloaderService.download(versionSelected).finally(() => {
|
||||
setDownloading(() => false);
|
||||
});
|
||||
bsDownloader.downloadBsVersion(selectedVersion)
|
||||
.catch(() => {})
|
||||
.finally(() => setSelectedVersion(null));
|
||||
};
|
||||
|
||||
const importVersion = async () => {
|
||||
@@ -47,7 +51,7 @@ export function AvailableVersionsList() {
|
||||
return;
|
||||
}
|
||||
|
||||
const folderRes = await ipc.sendV2<{ canceled: boolean; filePaths: string[] }>("choose-folder").toPromise();
|
||||
const folderRes = await lastValueFrom(ipc.sendV2<{ canceled: boolean; filePaths: string[] }>("choose-folder"))
|
||||
|
||||
if (!folderRes || folderRes.canceled || !folderRes.filePaths?.length) {
|
||||
return;
|
||||
@@ -62,10 +66,9 @@ export function AvailableVersionsList() {
|
||||
const imported = await installer.importVersion(toImport);
|
||||
|
||||
if (imported) {
|
||||
versionManagerService.askInstalledVersions();
|
||||
versionManager.askInstalledVersions();
|
||||
notification.notifySuccess({ title: "notifications.bs-import-version.success.imported.title", duration: 3_000 });
|
||||
progressBar.complete();
|
||||
await timer(400).toPromise();
|
||||
} else {
|
||||
notification.notifyError({ title: "notifications.types.error", desc: "notifications.bs-import-version.errors.import-error.desc" });
|
||||
}
|
||||
@@ -75,13 +78,16 @@ export function AvailableVersionsList() {
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full flex items-center flex-col pt-2">
|
||||
|
||||
<Slideshow className="absolute w-full h-full top-0" />
|
||||
<h1 className="text-gray-100 text-2xl mb-4 z-[1]">{t("pages.available-versions.title")}</h1>
|
||||
|
||||
<AvailableVersionsSlider />
|
||||
|
||||
<AvailableVersionsContext.Provider value={contextValue}>
|
||||
<AvailableVersionsSlider />
|
||||
</AvailableVersionsContext.Provider>
|
||||
|
||||
<AnimatePresence>
|
||||
{versionSelected && !downloading && (
|
||||
{selectedVersion && !downloading && (
|
||||
<motion.div initial={{ y: "150%" }} animate={{ y: "0%" }} exit={{ y: "150%" }} className="absolute bottom-5" onClick={startDownload}>
|
||||
<BsmButton text="misc.download" className="relative text-gray-800 dark:text-gray-100 rounded-md text-3xl font-bold italic tracking-wide px-3 pb-2 pt-1 shadow-md shadow-black" />
|
||||
</motion.div>
|
||||
@@ -92,7 +98,7 @@ export function AvailableVersionsList() {
|
||||
className="absolute top-5 right-5 h-9 w-9 bg-light-main-color-2 dark:bg-main-color-2 rounded-md"
|
||||
icon="settings"
|
||||
items={[
|
||||
{ icon: "sync", text: "pages.available-versions.dropdown.refresh", onClick: () => versionManagerService.askAvailableVersions() },
|
||||
{ icon: "sync", text: "pages.available-versions.dropdown.refresh", onClick: () => versionManager.askAvailableVersions() },
|
||||
{ icon: "download", text: "pages.available-versions.dropdown.import-version", onClick: importVersion },
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -34,6 +34,8 @@ import { VersionFolderLinkerService } from "renderer/services/version-folder-lin
|
||||
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";
|
||||
|
||||
export function SettingsPage() {
|
||||
|
||||
@@ -50,6 +52,7 @@ export function SettingsPage() {
|
||||
const playlistsManager = useService(PlaylistsManagerService);
|
||||
const modelsManager = useService(ModelsManagerService);
|
||||
const versionLinker = useService(VersionFolderLinkerService);
|
||||
const authService = useService(AuthUserService);
|
||||
|
||||
const { firstColor, secondColor } = useThemeColor();
|
||||
|
||||
@@ -66,6 +69,9 @@ export function SettingsPage() {
|
||||
})
|
||||
.sort((a, b) => a.text.localeCompare(b.text));
|
||||
|
||||
const nav = useNavigate();
|
||||
const t = useTranslation();
|
||||
|
||||
const [themeIdSelected, setThemeIdSelected] = useState(themeItem.find(e => e.value === themeService.getTheme()).id);
|
||||
const [languageSelected, setLanguageSelected] = useState(languagesItems.find(e => e.value === i18nService.currentLanguage).id);
|
||||
const [installationFolder, setInstallationFolder] = useState(null);
|
||||
@@ -73,13 +79,11 @@ export function SettingsPage() {
|
||||
const [mapDeepLinksEnabled, setMapDeepLinksEnabled] = useState(false);
|
||||
const [playlistsDeepLinkEnabled, setPlaylistsDeepLinkEnabled] = useState(false);
|
||||
const [modelsDeepLinkEnabled, setModelsDeepLinkEnabled] = useState(false);
|
||||
const [appVersion, setAppVersion] = useState("");
|
||||
const nav = useNavigate();
|
||||
const t = useTranslation();
|
||||
const appVersion = useObservable(ipcService.sendV2<string>("current-version"));
|
||||
const steamSessionExist = useObservable(authService.sessionExist$);
|
||||
|
||||
useEffect(() => {
|
||||
loadInstallationFolder();
|
||||
lastValueFrom(ipcService.sendV2<string>("current-version")).then(res => setAppVersion(res));
|
||||
mapsManager.isDeepLinksEnabled().then(enabled => setMapDeepLinksEnabled(() => enabled));
|
||||
playlistsManager.isDeepLinksEnabled().then(enabled => setPlaylistsDeepLinkEnabled(() => enabled));
|
||||
modelsManager.isDeepLinksEnabled().then(enabled => setModelsDeepLinkEnabled(() => enabled));
|
||||
@@ -203,6 +207,10 @@ 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>
|
||||
|
||||
<SettingContainer title="pages.settings.appearance.title" description="pages.settings.appearance.description">
|
||||
<div className="relative w-full h-8 bg-light-main-color-1 dark:bg-main-color-1 flex justify-center rounded-md py-1">
|
||||
<SettingColorChooser color={firstColor} onChange={setFirstColorSetting} />
|
||||
|
||||
@@ -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.download(state, true);
|
||||
const verifyFiles = () => bsDownloaderService.verifyBsVersionFiles(state);
|
||||
|
||||
const uninstall = async () => {
|
||||
const modalCompleted = await modalService.openModal(UninstallModal, state);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { DownloadEvent, DownloadInfo } from "main/services/bs-installer.service";
|
||||
import { BehaviorSubject, Observable } from "rxjs";
|
||||
import { distinctUntilChanged, filter, throttleTime } from "rxjs/operators";
|
||||
import { IpcResponse } from "shared/models/ipc";
|
||||
import { DownloadInfo } from "main/services/bs-installer.service";
|
||||
import { BehaviorSubject, Observable, ReplaySubject, Subscription, lastValueFrom, throwError } from "rxjs";
|
||||
import { distinctUntilChanged, 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";
|
||||
@@ -12,6 +11,9 @@ 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 { LinkOpenerService } from "./link-opener.service";
|
||||
import { DepotDownloaderErrorEvent, DepotDownloaderEvent, DepotDownloaderEventType, DepotDownloaderInfoEvent, DepotDownloaderWarningEvent } from "../../shared/models/depot-downloader.model";
|
||||
import equal from "fast-deep-equal";
|
||||
import { SteamMobileApproveModal } from "renderer/components/modal/modal-types/steam-mobile-approve-modal.component";
|
||||
|
||||
export class BsDownloaderService {
|
||||
private static instance: BsDownloaderService;
|
||||
@@ -24,11 +26,9 @@ export class BsDownloaderService {
|
||||
private readonly notificationService: NotificationService;
|
||||
private readonly linkOpener: LinkOpenerService;
|
||||
|
||||
private _isVerification: boolean = false;
|
||||
|
||||
public readonly currentBsVersionDownload$: BehaviorSubject<BSVersion> = new BehaviorSubject(null);
|
||||
public readonly downloadProgress$: BehaviorSubject<number> = new BehaviorSubject(0);
|
||||
public readonly selectedBsVersion$: BehaviorSubject<BSVersion> = new BehaviorSubject(null);
|
||||
private readonly isVerification$ = new BehaviorSubject(false);
|
||||
public readonly currentBsVersionDownload$ = new BehaviorSubject<BSVersion>(null);
|
||||
public readonly downloadProgress$ = new BehaviorSubject(0);
|
||||
|
||||
public static getInstance(): BsDownloaderService {
|
||||
if (!BsDownloaderService.instance) {
|
||||
@@ -45,81 +45,19 @@ export class BsDownloaderService {
|
||||
this.progressBarService = ProgressBarService.getInstance();
|
||||
this.notificationService = NotificationService.getInstance();
|
||||
this.linkOpener = LinkOpenerService.getInstance();
|
||||
this.asignListerners();
|
||||
}
|
||||
|
||||
private asignListerners(): void {
|
||||
this.ipcService
|
||||
.watch<number>("bs-download.[Progress]")
|
||||
.pipe(distinctUntilChanged())
|
||||
.subscribe(response => this.downloadProgress$.next(response.data));
|
||||
|
||||
this.ipcService
|
||||
.watch<string>("bs-download.[SteamID]")
|
||||
.pipe(filter(r => r.success && !!r.data))
|
||||
.subscribe(response => this.authService.setSteamID(response.data));
|
||||
|
||||
this.ipcService
|
||||
.watch<string>("bs-download.[Warning]")
|
||||
.pipe(
|
||||
filter(v => !!v && !!v.data),
|
||||
distinctUntilChanged(),
|
||||
throttleTime(10_000)
|
||||
)
|
||||
.subscribe(warning => {
|
||||
this.notificationService.notifyWarning({ title: "notifications.types.warning", desc: `notifications.bs-download.warnings.msg.${warning.data}` });
|
||||
});
|
||||
|
||||
this.ipcService
|
||||
.watch<string>("bs-download.[Error]")
|
||||
.pipe(
|
||||
filter(v => !!v && !!v.data),
|
||||
distinctUntilChanged(),
|
||||
throttleTime(1000)
|
||||
)
|
||||
.subscribe(err => {
|
||||
this.notificationService.notifyError({ title: "notifications.types.error", desc: `notifications.bs-download.errors.msg.${err.data}` });
|
||||
});
|
||||
|
||||
this.ipcService.watch<void>("bs-download.[2FA]").subscribe(async response => {
|
||||
if (!response.success) {
|
||||
this.ipcService.sendLazy("bs-download.kill");
|
||||
return;
|
||||
this.currentBsVersionDownload$.pipe(distinctUntilChanged(equal)).subscribe(version => {
|
||||
if(version) {
|
||||
this.bsVersionManager.setInstalledVersions([...this.bsVersionManager.installedVersions$.value, version ]);
|
||||
}
|
||||
const res = await this.modalService.openModal(GuardModal);
|
||||
if (res.exitCode !== ModalExitCode.COMPLETED) {
|
||||
this.progressBarService.hide(true);
|
||||
this.ipcService.sendLazy("bs-download.kill");
|
||||
return;
|
||||
}
|
||||
this.ipcService.sendLazy("bs-download.[2FA]", { args: res.data });
|
||||
});
|
||||
|
||||
this.ipcService.watch<BSVersion>("start-download-version").subscribe(res => {
|
||||
this.currentBsVersionDownload$.next(res.data);
|
||||
});
|
||||
|
||||
this.currentBsVersionDownload$.subscribe(version => {
|
||||
if (version) {
|
||||
this.bsVersionManager.setInstalledVersions([...this.bsVersionManager.installedVersions$.value, version]);
|
||||
} else {
|
||||
else {
|
||||
this.bsVersionManager.askInstalledVersions();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private resetDownload(): void {
|
||||
this.currentBsVersionDownload$.next(null);
|
||||
this.downloadProgress$.next(0);
|
||||
this.selectedBsVersion$.next(null);
|
||||
}
|
||||
|
||||
public cancelDownload(): Promise<IpcResponse<boolean>> {
|
||||
return this.ipcService.send<boolean>("bs-download.kill");
|
||||
}
|
||||
|
||||
public isDotNet6Installed(): Promise<boolean> {
|
||||
return this.ipcService.send<boolean>("is-dotnet-6-installed").then(res => res.success && res.data);
|
||||
return lastValueFrom(this.ipcService.sendV2<boolean>("is-dotnet-6-installed"));
|
||||
}
|
||||
|
||||
private async showDotNetNotInstalledError(): Promise<void> {
|
||||
@@ -135,66 +73,16 @@ export class BsDownloaderService {
|
||||
}
|
||||
}
|
||||
|
||||
public async download(bsVersion: BSVersion, isVerification?: boolean, isFirstCall = true): Promise<IpcResponse<DownloadEvent>> {
|
||||
// TODO : to remake cause we don't need recursion anymore (will be rework with qr code)
|
||||
|
||||
if (isFirstCall && !this.progressBarService.require()) {
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
if (isFirstCall && !(await this.isDotNet6Installed())) {
|
||||
await this.showDotNetNotInstalledError();
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
this.progressBarService.show(this.downloadProgress$);
|
||||
this._isVerification = isVerification;
|
||||
|
||||
let promise;
|
||||
if (!this.authService.sessionExist()) {
|
||||
const res = await this.modalService.openModal(LoginModal);
|
||||
if (res.exitCode !== ModalExitCode.COMPLETED) {
|
||||
this.progressBarService.hide(true);
|
||||
return { success: false };
|
||||
}
|
||||
this.authService.setSteamSession(res.data.username, res.data.stay);
|
||||
promise = this.ipcService.send<DownloadEvent, DownloadInfo>("bs-download.start", { args: { bsVersion, username: res.data.username, password: res.data.password, stay: res.data.stay, isVerification } });
|
||||
} else {
|
||||
promise = this.ipcService.send<DownloadEvent, DownloadInfo>("bs-download.start", { args: { bsVersion, username: this.authService.getSteamUsername(), isVerification } });
|
||||
}
|
||||
|
||||
let res = await promise;
|
||||
|
||||
if (res.data?.type === "[Password]") {
|
||||
this.authService.deleteSteamSession();
|
||||
res = await this.download(bsVersion, isVerification, false);
|
||||
}
|
||||
|
||||
this.progressBarService.hide(true);
|
||||
this.resetDownload();
|
||||
if (res.success && isFirstCall) {
|
||||
this.notificationService.notifySuccess({ title: `notifications.bs-download.success.titles.${isVerification ? "verification-finished" : "download-success"}`, duration: 3000 });
|
||||
} else if (res.data === "dotnet" && isFirstCall) {
|
||||
await this.showDotNetNotInstalledError();
|
||||
return res;
|
||||
} else if (res.data && isFirstCall) {
|
||||
this.notificationService.notifyError({ title: `notifications.types.error`, desc: `notifications.bs-download.errors.msg.${res.data}`, duration: 3000 });
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
public get isDownloading(): boolean {
|
||||
return !!this.currentBsVersionDownload$.value;
|
||||
}
|
||||
|
||||
public async getInstallationFolder(): Promise<string> {
|
||||
const res = await this.ipcService.send<string>("bs-download.installation-folder");
|
||||
return res.success ? res.data : "";
|
||||
return lastValueFrom(this.ipcService.sendV2<string>("bs-download.installation-folder"));
|
||||
}
|
||||
|
||||
public get isVerification(): boolean {
|
||||
return this._isVerification;
|
||||
return this.isVerification$.value;
|
||||
}
|
||||
|
||||
public setInstallationFolder(path: string): Observable<string> {
|
||||
@@ -202,7 +90,219 @@ export class BsDownloaderService {
|
||||
}
|
||||
|
||||
public async importVersion(pathToImport: string): Promise<boolean> {
|
||||
const res = await this.ipcService.send<void>("bs-download.import-version", { args: pathToImport });
|
||||
return res.success;
|
||||
return lastValueFrom(this.ipcService.sendV2<void>("bs-download.import-version", { args: pathToImport }))
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
}
|
||||
|
||||
// ### Downloading
|
||||
|
||||
private handleInfoEvents(events$: Observable<DepotDownloaderEvent>): Subscription[] {
|
||||
const subs: Subscription[] = [];
|
||||
|
||||
subs.push(events$.pipe(
|
||||
filter(event => event.subType === DepotDownloaderInfoEvent.Start),
|
||||
take(1),
|
||||
map(event => event.data as string),
|
||||
).subscribe(startData => {
|
||||
const downloadVersion = JSON.parse(startData) as BSVersion;
|
||||
if(typeof downloadVersion === "object" && downloadVersion?.BSVersion){
|
||||
this.currentBsVersionDownload$.next(downloadVersion);
|
||||
}
|
||||
}));
|
||||
|
||||
subs.push(events$.pipe(
|
||||
filter(event => event.subType === DepotDownloaderInfoEvent.Progress || event.subType === DepotDownloaderInfoEvent.Validated),
|
||||
map(event => event.data as string),
|
||||
).subscribe(progress => {
|
||||
this.downloadProgress$.next(parseFloat(progress.replaceAll(",", ".")));
|
||||
}));
|
||||
|
||||
subs.push(events$.pipe(
|
||||
filter(event => event.subType === DepotDownloaderInfoEvent.MobileApp),
|
||||
take(1),
|
||||
).subscribe(async () => {
|
||||
const logged$ = events$.pipe(filter(event => event.subType === DepotDownloaderInfoEvent.SteamID), take(1));
|
||||
const res = await this.modalService.openModal(SteamMobileApproveModal, { logged$ });
|
||||
if(res.exitCode !== ModalExitCode.COMPLETED){
|
||||
return this.stopDownload();
|
||||
}
|
||||
}));
|
||||
|
||||
subs.push(events$.pipe(
|
||||
filter(event => event.subType === DepotDownloaderInfoEvent.TwoFA || event.subType === DepotDownloaderInfoEvent.Guard),
|
||||
take(1),
|
||||
map(event => event.subType),
|
||||
).subscribe(async () => {
|
||||
const res = await this.modalService.openModal(GuardModal);
|
||||
if(res.exitCode !== ModalExitCode.COMPLETED){
|
||||
return this.stopDownload();
|
||||
}
|
||||
this.sendInput(res.data);
|
||||
}));
|
||||
|
||||
subs.push(events$.pipe(
|
||||
filter(event => event.subType === DepotDownloaderInfoEvent.Finished),
|
||||
take(1),
|
||||
).subscribe(() => {
|
||||
if(this.isVerification){
|
||||
return this.notificationService.notifySuccess({title: "notifications.bs-download.success.titles.verification-finished"});
|
||||
}
|
||||
return this.notificationService.notifySuccess({title: "notifications.bs-download.success.titles.download-success"});
|
||||
}));
|
||||
|
||||
return subs;
|
||||
}
|
||||
|
||||
private handleWarningEvents(events$: Observable<DepotDownloaderEvent>): Subscription[] {
|
||||
const subs: Subscription[] = [];
|
||||
|
||||
const handledWarnings = Object.values(DepotDownloaderWarningEvent);
|
||||
|
||||
subs.push(events$.pipe(
|
||||
filter(event => handledWarnings.includes(event.subType as DepotDownloaderWarningEvent)),
|
||||
).subscribe(event => {
|
||||
this.notificationService.notifyWarning({title: "notifications.types.warning", desc: `notifications.bs-download.warnings.msg.${event.subType}`});
|
||||
}));
|
||||
|
||||
return subs;
|
||||
}
|
||||
|
||||
private hanndleErrorEvent(errorEvent: DepotDownloaderEvent) {
|
||||
const handledErrors = Object.values(DepotDownloaderErrorEvent);
|
||||
|
||||
if(handledErrors.includes(errorEvent?.subType as DepotDownloaderErrorEvent)){
|
||||
return this.notificationService.notifyError({title: "notifications.types.error", desc: `notifications.bs-download.errors.msg.${errorEvent.subType}`, duration: 10_000});
|
||||
}
|
||||
|
||||
return this.notificationService.notifyError({title: "notifications.types.error", desc: `notifications.bs-download.errors.msg.${DepotDownloaderErrorEvent.Unknown}`, duration: 10_000});
|
||||
}
|
||||
|
||||
private wrapDownload(download$: Observable<DepotDownloaderEvent>, silent?: boolean): Observable<DepotDownloaderEvent> {
|
||||
|
||||
return new Observable<DepotDownloaderEvent>(sub => {
|
||||
|
||||
const downloadSub = download$.subscribe({next: n => sub.next(n), error: e => sub.error(e), complete: () => sub.complete()});
|
||||
|
||||
const subs = [
|
||||
...this.handleInfoEvents(download$.pipe(filter(event => event.type === DepotDownloaderEventType.Info))),
|
||||
...this.handleWarningEvents(download$.pipe(filter(event => event.type === DepotDownloaderEventType.Warning), throttleTime(10_000))),
|
||||
];
|
||||
|
||||
return () => {
|
||||
downloadSub.unsubscribe();
|
||||
subs.forEach(sub => sub.unsubscribe());
|
||||
}
|
||||
|
||||
}).pipe(
|
||||
tap({
|
||||
error: (e) => {
|
||||
this.authService.deleteSteamSession();
|
||||
!silent && this.hanndleErrorEvent(e)
|
||||
}
|
||||
}),
|
||||
share({connector: () => new ReplaySubject(1)})
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
private tryAutoDownloadBsVersion(downloadInfo: DownloadInfo){
|
||||
|
||||
if(!this.authService.sessionExist()){
|
||||
return throwError(() => new Error("No session"));
|
||||
}
|
||||
|
||||
const infos: DownloadInfo = {...downloadInfo, username: this.authService.getSteamUsername()}
|
||||
return this.wrapDownload(
|
||||
this.ipcService.sendV2<DepotDownloaderEvent>("auto-download-bs-version", { args: infos }),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
private startDownload(downloadInfo: DownloadInfo){
|
||||
return this.wrapDownload(
|
||||
this.ipcService.sendV2<DepotDownloaderEvent>("download-bs-version", { args: downloadInfo })
|
||||
);
|
||||
}
|
||||
|
||||
private startQrCodeDownload(downloadInfo: DownloadInfo){
|
||||
return this.wrapDownload(
|
||||
this.ipcService.sendV2<DepotDownloaderEvent>("download-bs-version-qr", { args: downloadInfo })
|
||||
);
|
||||
}
|
||||
|
||||
private doDownloadBsVersion(bsVersion: BSVersion, isVerification?: boolean): Promise<void>{
|
||||
|
||||
if(!this.progressBarService.require()){
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
this.progressBarService.show(this.downloadProgress$, true);
|
||||
this.isVerification$.next(isVerification);
|
||||
|
||||
const downloadPromise = (async () => {
|
||||
|
||||
const haveDotNet = await this.isDotNet6Installed().catch(() => false);
|
||||
if(!haveDotNet){
|
||||
this.showDotNetNotInstalledError();
|
||||
return Promise.reject(new Error("DotNet not installed"));
|
||||
}
|
||||
|
||||
const downloadInfo: DownloadInfo = {bsVersion, isVerification}
|
||||
|
||||
const autoDownload = await lastValueFrom(this.tryAutoDownloadBsVersion(downloadInfo)).then(() => true).catch(() => false);
|
||||
|
||||
if(autoDownload){ return Promise.resolve(); }
|
||||
|
||||
const qrCodeDownload$ = this.startQrCodeDownload(downloadInfo);
|
||||
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$ });
|
||||
|
||||
if(loginRes.exitCode !== ModalExitCode.COMPLETED){
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if(loginRes.data.stay){
|
||||
this.authService.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});
|
||||
|
||||
return lastValueFrom(download$);
|
||||
|
||||
})();
|
||||
|
||||
// *** TEST EXPIRATION MOBILE APP APROVAL ***
|
||||
|
||||
return downloadPromise.then(() => {}).finally(() => {
|
||||
this.downloadProgress$.next(0);
|
||||
this.currentBsVersionDownload$.next(null);
|
||||
this.progressBarService.hide(true);
|
||||
this.isVerification$.next(false);
|
||||
this.bsVersionManager.askAvailableVersions();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private sendInput(input: string){
|
||||
return lastValueFrom(this.ipcService.sendV2<void>("send-input-bs-download", { args: input }));
|
||||
}
|
||||
|
||||
public downloadBsVersion(version: BSVersion): Promise<BSVersion> {
|
||||
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);
|
||||
}
|
||||
|
||||
public stopDownload(): Promise<void>{
|
||||
return lastValueFrom(this.ipcService.sendV2<void>("stop-download-bs-version"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,18 +64,10 @@ export class BSVersionManagerService {
|
||||
});
|
||||
}
|
||||
|
||||
public getAvailableYears(): string[] {
|
||||
return [...new Set(this.availableVersions$.value.map(v => v.year))].sort((a, b) => b.localeCompare(a));
|
||||
}
|
||||
|
||||
public isVersionInstalled(version: BSVersion): boolean {
|
||||
return !!this.getInstalledVersions().find(v => v.BSVersion === version.BSVersion && v.steam === version.steam && v.oculus === version.oculus);
|
||||
}
|
||||
|
||||
public getAvaibleVersionsOfYear(year: string): BSVersion[] {
|
||||
return this.availableVersions$.value.filter(v => v.year === year).sort((a, b) => +b.ReleaseDate - +a.ReleaseDate);
|
||||
}
|
||||
|
||||
public async editVersion(version: BSVersion): Promise<BSVersion> {
|
||||
const modalRes = await this.modalService.openModal(EditVersionModal, { version, clone: false });
|
||||
if (modalRes.exitCode !== ModalExitCode.COMPLETED) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { defaultIfEmpty, shareReplay } from "rxjs/operators";
|
||||
import { Observable, identity } from "rxjs";
|
||||
import { defaultIfEmpty, share } from "rxjs/operators";
|
||||
import { Observable, ReplaySubject, identity } from "rxjs";
|
||||
import { IpcRequest, IpcResponse } from "shared/models/ipc";
|
||||
import { IpcCompleteChannel, IpcErrorChannel, IpcTearDownChannel } from "shared/models/ipc/ipc-response.interface";
|
||||
|
||||
export class IpcService {
|
||||
private static instance: IpcService;
|
||||
@@ -65,26 +66,29 @@ export class IpcService {
|
||||
if (!request) {
|
||||
request = { args: null, responceChannel: null };
|
||||
}
|
||||
|
||||
if (!request.responceChannel) {
|
||||
request.responceChannel = `${channel}_responce_${crypto.randomUUID()}`;
|
||||
}
|
||||
|
||||
const completeChannel = `${request.responceChannel}_complete`;
|
||||
const errorChannel = `${request.responceChannel}_error`;
|
||||
const completeChannel: IpcCompleteChannel = `${request.responceChannel}_complete`;
|
||||
const errorChannel: IpcErrorChannel = `${request.responceChannel}_error`;
|
||||
const teardownChannel: IpcTearDownChannel = `${request.responceChannel}_teardown`;
|
||||
|
||||
const obs = new Observable<T>(observer => {
|
||||
window.electron.ipcRenderer.on(request.responceChannel, (res: T) => observer.next(res));
|
||||
window.electron.ipcRenderer.on(errorChannel, (err: Error) => observer.error(err));
|
||||
window.electron.ipcRenderer.on(completeChannel, () => observer.complete());
|
||||
}).pipe(defaultValue ? defaultIfEmpty(defaultValue) : identity, shareReplay(1));
|
||||
|
||||
window.electron.ipcRenderer.once(completeChannel, () => {
|
||||
window.electron.ipcRenderer.removeAllListeners(request.responceChannel);
|
||||
window.electron.ipcRenderer.removeAllListeners(errorChannel);
|
||||
window.electron.ipcRenderer.removeAllListeners(completeChannel);
|
||||
});
|
||||
window.electron.ipcRenderer.sendMessage(channel, request);
|
||||
|
||||
window.electron.ipcRenderer.sendMessage(channel, request);
|
||||
return () => {
|
||||
window.electron.ipcRenderer.removeAllListeners(request.responceChannel);
|
||||
window.electron.ipcRenderer.removeAllListeners(errorChannel);
|
||||
window.electron.ipcRenderer.removeAllListeners(completeChannel);
|
||||
window.electron.ipcRenderer.sendMessage(teardownChannel, null);
|
||||
};
|
||||
}).pipe(defaultValue ? defaultIfEmpty(defaultValue) : identity, share({connector: () => new ReplaySubject(1)}));
|
||||
|
||||
return obs;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user