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:
MathieuG-P
2023-08-06 16:16:07 +02:00
committed by GitHub
41 changed files with 1095 additions and 678 deletions
@@ -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 },
]}
/>
+12 -4
View File
@@ -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);
+230 -130
View File
@@ -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) {
+15 -11
View File
@@ -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;
}