install mods & uninstall mod & uninstall all mods & ligth theme & some modals

This commit is contained in:
MathieuG-P
2022-10-03 00:56:20 +02:00
parent 337697fafb
commit 9d2ec1166e
26 changed files with 459 additions and 108 deletions
+34
View File
@@ -3,6 +3,7 @@ import { BsModsManagerService } from "../services/mods/bs-mods-manager.service";
import { UtilsService } from "../services/utils.service";
import { BSVersion } from "shared/bs-version.interface";
import { IpcRequest } from "shared/models/ipc";
import { Mod } from "shared/models/mods/mod.interface";
ipcMain.on("get-available-mods", (event, request: IpcRequest<BSVersion>) => {
const utils = UtilsService.getInstance();
@@ -20,4 +21,37 @@ ipcMain.on("get-installed-mods", (event, request: IpcRequest<BSVersion>) => {
modsManager.getInstalledMods(request.args).then(mods => {
utils.ipcSend(request.responceChannel, {success: true, data: mods});
}).catch(() => utils.ipcSend(request.responceChannel, {success: false}));
});
ipcMain.on("install-mods", (event, request: IpcRequest<{mods: Mod[], version: BSVersion}>) => {
const utils = UtilsService.getInstance();
const modsManager = BsModsManagerService.getInstance();
modsManager.installMods(request.args.mods, request.args.version).then(nbInstalled => {
utils.ipcSend(request.responceChannel, {success: true, data: nbInstalled})
}).catch(err => {
utils.ipcSend(request.responceChannel, {success: false, error: err});
})
});
ipcMain.on("uninstall-mods", (event, request: IpcRequest<{mods: Mod[], version: BSVersion}>) => {
const utils = UtilsService.getInstance();
const modsManager = BsModsManagerService.getInstance();
modsManager.uninstallMods(request.args.mods, request.args.version).then(nbInstalled => {
utils.ipcSend(request.responceChannel, {success: true, data: nbInstalled})
}).catch(err => {
utils.ipcSend(request.responceChannel, {success: false, error: err});
})
});
ipcMain.on("uninstall-all-mods", (event, request: IpcRequest<BSVersion>) => {
const utils = UtilsService.getInstance();
const modsManager = BsModsManagerService.getInstance();
modsManager.uninstallAllMods(request.args).then(nbInstalled => {
utils.ipcSend(request.responceChannel, {success: true, data: nbInstalled})
}).catch(err => {
utils.ipcSend(request.responceChannel, {success: false, error: err});
})
});
-1
View File
@@ -14,7 +14,6 @@ import log from 'electron-log';
import './ipcs';
import { UtilsService } from './services/utils.service';
import { WindowManagerService } from './services/window-manager.service';
import { BsModsManagerService } from './services/mods/bs-mods-manager.service';
export const PRELOAD_PATH = app.isPackaged ? path.join(__dirname, 'preload.js') : path.join(__dirname, '../../.erb/dll/preload.js')
@@ -55,7 +55,7 @@ export class BeatModsApiService {
}
private asignDependencies(mod: Mod, mods: Mod[]): Mod{
mod.dependencies.map(dep => mods.find(mod => mod.name === dep.name));
mod.dependencies = mod.dependencies.map(dep => mods.find(mod => mod.name === dep.name));
return mod;
}
@@ -65,7 +65,7 @@ export class BeatModsApiService {
const alias = await this.getAliasOfVersion(version);
return this.requestService.get<Mod[]>(this.getVersionModsUrl(alias)).then(mods => {
mods.map(mod => this.asignDependencies(mod, mods));
mods = mods.map(mod => this.asignDependencies(mod, mods));
this.versionModsCache.set(version.BSVersion, mods);
return mods;
});
+129 -25
View File
@@ -1,5 +1,5 @@
import { BSVersion } from "shared/bs-version.interface";
import { Mod } from "shared/models/mods/mod.interface";import { BeatModsApiService } from "./beat-mods-api.service";
import { DownloadLink, Mod, ModInstallProgression } from "shared/models/mods/mod.interface";import { BeatModsApiService } from "./beat-mods-api.service";
import { BSLocalVersionService } from "../bs-local-version.service"
import path from "path";
import { UtilsService } from "../utils.service";
@@ -21,6 +21,12 @@ export class BsModsManagerService {
private manifestMatches: Mod[];
private nbModsToInstall = 0;
private nbInstalledMods = 0;
private nbModsToUninstall = 0;
private nbUninstalledMods = 0;
public static getInstance(): BsModsManagerService{
if(!BsModsManagerService.instance){ BsModsManagerService.instance = new BsModsManagerService(); }
return BsModsManagerService.instance;
@@ -55,7 +61,6 @@ export class BsModsManagerService {
if(!this.utilsService.pathExist(modsPath)){ return []; }
const files = fs.readdirSync(modsPath);
const promises = files.map(f => {
console.log()
return (async() => {
const filePath = path.join(modsPath, f)
const ext = path.extname(f);
@@ -96,26 +101,36 @@ export class BsModsManagerService {
return this.requestService.downloadFile(zipUrl, dest).then(zipPath => new StreamZip.async({file : zipPath}));
}
private async executeBSIPA(version: BSVersion): Promise<boolean>{
private async executeBSIPA(version: BSVersion, args: string[]): Promise<boolean>{
const versionPath = await this.bsLocalService.getVersionPath(version);
const ipaPath = path.join(versionPath, "IPA.exe");
const bsExePath = path.join(versionPath, BS_EXECUTABLE);
if(!this.utilsService.pathExist(ipaPath) || !this.utilsService.pathExist(bsExePath)){ return false; }
return new Promise<boolean>(resolve => {
const processIPA = spawn(`start /wait /min "" "${ipaPath}" -n`, {cwd: versionPath, detached: true, shell: true});
const processIPA = spawn(`start /wait /min "" "${ipaPath}" ${args.join(" ")}`, {cwd: versionPath, detached: true, shell: true});
processIPA.once("exit", code => {
if(code === 0){ return resolve(true); }
resolve(false);
});
setTimeout(() => resolve(false), (1 * 60) * 1000); //timeout 1min
});
}
private getModDownload(mod: Mod, version: BSVersion): DownloadLink{
return mod.downloads.find(download => {
const type = download.type.toLowerCase()
return type === "universal" || type === this.bsLocalService.getVersionType(version);
});
}
private async installMod(mod: Mod, version: BSVersion): Promise<boolean>{
const download = mod.downloads.find(download => {
const type = download.type.toLowerCase()
return type === "universal" || type === this.bsLocalService.getVersionType(version);
});
this.nbInstalledMods++;
this.utilsService.ipcSend<ModInstallProgression>("mod-installed", {success: true, data: {name: mod.name, progression: (this.nbInstalledMods / this.nbModsToInstall) * 100}})
const download = this.getModDownload(mod, version);
if(!download){ return false; }
@@ -139,14 +154,16 @@ export class BsModsManagerService {
const isBSIPA = mod.name.toLowerCase() === "bsipa";
const destDir = isBSIPA ? verionPath : path.join(verionPath, ModsInstallFolder.PENDING);
await zip.extract(null, destDir);
await zip.extract(null, destDir).catch(err => console.log(err));
return isBSIPA ? await this.executeBSIPA(version) : true;
const res = isBSIPA ? await this.executeBSIPA(version, ["-n"]) : true;
return res;
}
private isDependency(mod: Mod, selectedMods: Mod[], availableMods: Mod[]){
return selectedMods.some(m => {
const deps = m.dependencies.map(dep => Array.from(availableMods.values()).flat().find(m => dep.name === m.name));
const deps = m.dependencies.map(dep => Array.from(availableMods.values()).find(m => dep.name === m.name));
if(deps.some(depMod => depMod.name === mod.name)){ return true; }
return deps.some(depMod => depMod.dependencies.some(depModDep => depModDep.name === mod.name));
});
@@ -154,7 +171,50 @@ export class BsModsManagerService {
private async resolveDependencies(mods: Mod[], version: BSVersion): Promise<Mod[]>{
const availableMods = await this.beatModsApi.getVersionMods(version);
return availableMods.filter(mod => this.isDependency(mod, mods, availableMods));
return Array.from(new Map<string, Mod>(availableMods.reduce((res, mod) => {
if(this.isDependency(mod, mods, availableMods)){
res.push([mod.name, mod]);
}
return res;
}, [])).values());
}
private async uninstallBSIPA(mod: Mod, version: BSVersion): Promise<void>{
const download = this.getModDownload(mod, version);
const verionPath = await this.bsLocalService.getVersionPath(version);
const hasIPAExe = this.utilsService.pathExist(path.join(verionPath, "IPA.exe"));
const hasIPADir = this.utilsService.pathExist(path.join(verionPath, "IPA"));
if(!hasIPADir || !hasIPAExe){ return; }
await this.executeBSIPA(version, ["--revert", "-n"]).then(res => console.log("BSIPA", res));
const promises = download.hashMd5.map(files => {
const file = files.file.replaceAll("IPA/", "").replaceAll("Data", "Beat Saber_Data");
console.log("DELETE", file);
return this.utilsService.unlinkIfExist(path.join(verionPath, file));
})
await Promise.all(promises);
}
private async uninstallMod(mod: Mod, version: BSVersion): Promise<void>{
this.nbUninstalledMods++;
this.utilsService.ipcSend<ModInstallProgression>("mod-uninstalled", {success: true, data: {name: mod.name, progression: (this.nbUninstalledMods / this.nbModsToUninstall) * 100}})
if(mod.name.toLowerCase() === "bsipa"){ return this.uninstallBSIPA(mod, version); }
const download = this.getModDownload(mod, version);
const versionPath = await this.bsLocalService.getVersionPath(version);
const promises = download.hashMd5.map(async files => {
this.utilsService.unlinkIfExist(path.join(versionPath, files.file));
this.utilsService.unlinkIfExist(path.join(versionPath, "IPA", "Pending", files.file));
});
await Promise.all(promises);
}
public getAvailableMods(version: BSVersion): Promise<Mod[]>{
@@ -171,27 +231,70 @@ export class BsModsManagerService {
this.getModsInDir(version, ModsInstallFolder.PLUGINS),
this.getModsInDir(version, ModsInstallFolder.LIBS)
]).then(dirMods => {
return [((!!bsipa) && bsipa), ...Array.from(new Map<string, Mod>(dirMods.flat().map(m => [m.name, m])).values())];
})
const res = [];
if(!!bsipa){ res.push(bsipa); }
const installedMods = Array.from(new Map<string, Mod>(dirMods.flat().map(m => [m.name, m])).values());
res.push(...installedMods);
return res;
});
}
public async installMods(mods: Mod[], version: BSVersion){
if(!mods || !mods.length){ throw "no mods to install"; }
mods = [...mods];
const bsipaIndex = mods.findIndex(mod => mod.name.toLowerCase() === "bsipa");
const bsipa = mods[bsipaIndex];
mods.splice(bsipaIndex, 1);
public async installMods(mods: Mod[], version: BSVersion): Promise<number>{
if(!mods){ throw "no-mod"; }
// TODO
const deps = await this.resolveDependencies(mods, version);
mods.push(...deps);
const bsipa = mods.find(mod => mod.name.toLowerCase() === "bsipa");
if(bsipa){ mods = mods.filter(mod => mod.name.toLowerCase() !== "bsipa"); }
this.nbModsToInstall = mods.length + (bsipa && 1);
this.nbInstalledMods = 0;
if(bsipa){
const installed = await this.installMod(bsipa, version).catch(() => false)
if(!installed){ throw "cannot-install-bsipa"; }
}
let nbInstalledMods = bsipa ? 1 : 0;
for(const mod of mods){
await this.installMod(mod, version).then(installed => installed && nbInstalledMods++)
}
return nbInstalledMods;
}
public uninstallMods(mods: Mod[], version: BSVersion){
throw "NOT IMPLEMENTED YET";
public async uninstallMods(mods: Mod[], version: BSVersion): Promise<number>{
if(!mods){ throw "no-mod"; }
this.nbModsToUninstall = mods.length;
this.nbUninstalledMods = 0;
for(const mod of mods){
await this.uninstallMod(mod, version);
}
return this.nbUninstalledMods;
}
public isModsAvailable(version: BSVersion): Promise<boolean>{
throw "NOT IMPLEMENTED YET";
public async uninstallAllMods(version: BSVersion): Promise<number>{
const mods = await this.getInstalledMods(version);
this.nbModsToUninstall = mods.length;
this.nbUninstalledMods = 0;
for(const mod of mods){
await this.uninstallMod(mod, version);
}
const versionPath = await this.bsLocalService.getVersionPath(version);
this.utilsService.rmDirIfExist(path.join(versionPath, ModsInstallFolder.PLUGINS));
this.utilsService.rmDirIfExist(path.join(versionPath, ModsInstallFolder.LIBS));
this.utilsService.rmDirIfExist(path.join(versionPath, ModsInstallFolder.IPA));
return this.nbUninstalledMods;
}
}
@@ -199,6 +302,7 @@ export class BsModsManagerService {
const enum ModsInstallFolder {
PLUGINS = "Plugins",
LIBS = "Libs",
IPA = "IPA",
PENDING = "IPA/Pending",
PLUGINS_PENDING = "IPA/Pending/Plugins",
LIBS_PENDING = "IPA/Pending/Libs"
+13 -3
View File
@@ -1,9 +1,9 @@
import { existsSync, mkdirSync, readdirSync, readFile } from "fs";
import { existsSync, mkdirSync, readdirSync, readFile, unlinkSync } from "fs";
import { spawnSync } from "child_process";
import { homedir } from "os";
import path from "path";
import { app, BrowserWindow } from "electron";
import { rm } from "fs/promises";
import { rm, unlink } from "fs/promises";
import { IpcResponse } from "shared/models/ipc";
import log from "electron-log";
@@ -38,6 +38,16 @@ export class UtilsService{
if(!this.pathExist(path)){ mkdirSync(path, {recursive: true}); }
}
public async unlinkIfExist(pathToFile: string): Promise<void>{
if(!this.pathExist(pathToFile)){ return }
return unlink(pathToFile);
}
public rmDirIfExist(path: string): Promise<void>{
if(!this.pathExist(path)){ return; }
return rm(path, {recursive: true, force: true});
}
public taskRunning(task: string): boolean{
const tasks = spawnSync('tasklist').stdout.toString();
return tasks.includes(task);
@@ -68,7 +78,7 @@ export class UtilsService{
return rm(folderPath, {recursive: true});
}
public ipcSend(channel: string, response: IpcResponse<any>): void{
public ipcSend<T = any>(channel: string, response: IpcResponse<T>): void{
try {
this.mainWindow.webContents.send(channel, response);
} catch (error) {
+14 -8
View File
@@ -1,6 +1,6 @@
import { NavBar } from "./components/nav-bar/nav-bar.component";
import TitleBar from "./components/title-bar/title-bar.component";
import { Routes, Route } from "react-router-dom";
import { Routes, Route, useLocation } from "react-router-dom";
import { AvailableVersionsList } from "./pages/available-versions-list.components";
import { VersionViewer } from "./pages/version-viewer.component";
import { Modal } from "./components/modal/modal.component";
@@ -9,17 +9,23 @@ import { BsmProgressBar } from "./components/progress-bar/bsm-progress-bar.compo
import { useEffect } from "react";
import { ThemeService } from "./services/theme.service";
import { NotificationOverlay } from "./components/notification/notification-overlay.component";
import { PageStateService } from "./services/page-state.service";
export default function App() {
const themeService = ThemeService.getInstance();
const themeService = ThemeService.getInstance();
const pageState = PageStateService.getInstance();
useEffect(() => {
themeService.theme$.subscribe(() => {
if(themeService.isDark || (themeService.isOS && window.matchMedia('(prefers-color-scheme: dark)').matches)){ document.documentElement.classList.add('dark'); }
else { document.documentElement.classList.remove('dark'); }
});
}, []);
const location = useLocation();
pageState.setState(location.state);
useEffect(() => {
themeService.theme$.subscribe(() => {
if(themeService.isDark || (themeService.isOS && window.matchMedia('(prefers-color-scheme: dark)').matches)){ return document.documentElement.classList.add('dark'); }
document.documentElement.classList.remove('dark');
});
}, []);
return (
@@ -0,0 +1,27 @@
import { ModalExitCode, ModalResponse, ModalService } from "../../../services/modale.service";
import BeatConflict from "../../../../../assets/images/apngs/beat-conflict.png"
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { BsmImage } from "renderer/components/shared/bsm-image.component";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { Mod } from "shared/models/mods/mod.interface";
export function UninstallModModal({resolver}: {resolver: (x: ModalResponse) => void}) {
const mod = ModalService.getInsance().getModalData<Mod>();
const t = useTranslation();
const desc = mod.name.toLowerCase() === "bsipa" ? "modals.uninstall-mod.description-bsipa" : "modals.uninstall-mod.description";
return (
<form onSubmit={(e) => {e.preventDefault(); resolver({exitCode: ModalExitCode.COMPLETED})}}>
<h1 className="text-3xl uppercase tracking-wide w-full text-center text-gray-800 dark:text-gray-200">{t("modals.uninstall-mod.title")}</h1>
<BsmImage className="mx-auto h-24" image={BeatConflict}/>
<p className="max-w-sm text-gray-800 dark:text-gray-200">{t(desc, {mod: mod.name})}</p>
<div className="grid grid-flow-col grid-cols-2 gap-4 mt-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.bs-uninstall.buttons.submit"/>
</div>
</form>
)
}
@@ -7,6 +7,7 @@ import { InstallationFolderModal } from "./modal-types/installation-folder-modal
import { EditVersionModal } from "./modal-types/edit-version-modal.component";
import { useObservable } from "renderer/hooks/use-observable.hook";
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
import { UninstallModModal } from "./modal-types/uninstall-mod-modal.component";
export function Modal() {
@@ -29,6 +30,7 @@ export function Modal() {
{modalType === ModalType.INSTALLATION_FOLDER && <InstallationFolderModal resolver={modalSevice.getResolver()}/>}
{modalType === ModalType.EDIT_VERSION && <EditVersionModal resolver={modalSevice.getResolver()}/>}
{modalType === ModalType.CLONE_VERSION && <EditVersionModal resolver={modalSevice.getResolver()} clone/>}
{modalType === ModalType.UNINSTALL_MOD && <UninstallModModal resolver={modalSevice.getResolver()}/>}
</div>
</motion.div>
</div>
@@ -11,11 +11,13 @@ export function BsmProgressBar() {
const progress = useObservable(progressBarService.progression$);
const visible = useObservable(progressBarService.visible$);
const style = useObservable(progressBarService.style$);
const {firstColor, secondColor} = useThemeColor();
return (
<AnimatePresence> { visible &&
<motion.div initial={{y: "120%"}} animate={{y:"0%"}} exit={{y:"120%"}} className="w-full absolute h-14 flex justify-center items-center bottom-2">
<motion.div initial={{y: "120%"}} animate={{y:"0%"}} exit={{y:"120%"}} className="w-full absolute h-14 flex justify-center items-center bottom-2 pointer-events-none" style={style}>
<div className={`flex items-center content-center justify-center bottom-9 z-10 rounded-lg bg-light-main-color-2 dark:bg-main-color-2 shadow-center shadow-black cursor-pointer transition-all duration-300 ${!progress && "h-14 w-14 !rounded-full"} ${!!progress && "h-5 w-3/4 !rounded-full p-[6px]"}`}>
{ !!progress && (
<div className="relative flex items-center h-full w-full rounded-full bg-black">
@@ -1,6 +1,6 @@
import { BsmIcon, BsmIconType } from "../svgs/bsm-icon.component"
import OutsideClickHandler from 'react-outside-click-handler';
import React from "react";
import React, { MutableRefObject, Ref } from "react";
import { BsmImage } from "./bsm-image.component";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
@@ -46,7 +46,7 @@ export function BsmButton({className, style, imgClassName, iconClassName, icon,
return (
<OutsideClickHandler onOutsideClick={e => onClickOutside && onClickOutside(e)}>
<div onClick={e => onClick && onClick(e)} className={`${className} overflow-hidden cursor-pointer group ${(!withBar && !disabled && (!!typeColor || !!color)) && "hover:brightness-[1.15]"} ${disabled && "brightness-75 cursor-not-allowed"} ${renderTypeColor}`} style={{...style, ...((!!primaryColor || !!color) && {backgroundColor: primaryColor ?? color})}}>
<div onClick={onClick} className={`${className} overflow-hidden cursor-pointer group ${(!withBar && !disabled && (!!typeColor || !!color)) && "hover:brightness-[1.15]"} ${disabled && "brightness-75 cursor-not-allowed"} ${renderTypeColor}`} style={{...style, ...((!!primaryColor || !!color) && {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"}/> }
{text && (type === "submit" ? <button type="submit" className="w-full h-full" style={{...(!!textColor && {color: textColor})}}>{t(text)}</button> : <span style={{...(!!textColor && {color: textColor})}} >{t(text)}</span>)}
@@ -20,7 +20,7 @@ export function BsmCheckbox({className, checked, onChange, disabled} : Props) {
<div className={`group ${className}`}>
{!disabled && <div className="glow-on-hover !w-[calc(100%+6px)] !h-[calc(100%+6px)] !-top-[3px] !-left-[3px] group-hover:opacity-100"/>}
<div className={`w-full h-full bg-inherit border-2 rounded-md overflow-hidden flex items-center justify-center ${disabled ? "brightness-50 cursor-not-allowed" : "cursor-pointer"}`} onClickCapture={e => {e.stopPropagation(); handleClick()}}>
<div className={`w-full h-full bg-inherit border-2 border-current rounded-md overflow-hidden flex items-center justify-center ${disabled ? "brightness-50 cursor-not-allowed" : "cursor-pointer"}`} onClickCapture={e => {e.stopPropagation(); handleClick()}}>
<motion.span className="w-full h-full flex items-center justify-center" style={{backgroundColor: checkedColor}} initial={checked ? {opacity: 1} : {opacity: 0}} animate={checked ? {opacity: 1} : {opacity: 0}} transition={{duration: .15}}>
<BsmIcon icon="check" style={{color: iconColor}}/>
</motion.span>
@@ -6,17 +6,21 @@ import { useTranslation } from "renderer/hooks/use-translation.hook"
export interface DropDownItem {text: string, icon?: BsmIconType, onClick?: () => void}
export function BsmDropdownButton({className, items, align}: {className?: string, items?: DropDownItem[], align?: "left"|"right"}) {
type Props = {className?: string, items?: DropDownItem[], align?: "left"|"right", withBar?: boolean, icon?: BsmIconType, buttonClassName?: string, menuTranslationY?: string|number}
export function BsmDropdownButton({className, items, align, withBar = true, icon = "settings", buttonClassName, menuTranslationY}: Props) {
const [expanded, setExpanded] = useState(false)
const t = useTranslation()
const t = useTranslation();
const defaultButtonClassName = "relative z-[1] p-1 rounded-md text-inherit w-full h-full shadow-md shadow-black"
return (
<div className={`${className}`}>
<BsmButton onClick={() => setExpanded(!expanded)} className='relative z-[1] p-1 rounded-md text-inherit w-full h-full shadow-md shadow-black' icon="settings" active={expanded} onClickOutside={() => {setExpanded(false)}}/>
<div className={`pt-1 pb-1 w-fit absolute cursor-pointer top-[calc(100%-4px)] rounded-md overflow-hidden bg-light-main-color-2 dark:bg-main-color-2 text-sm text-gray-800 dark:text-gray-200 shadow-md shadow-black transition-transform ease-in-out ${align === "left" ? "left-0 origin-top-left" : "right-0 origin-top-right"}`} style={{transform: expanded ? "scale(1)" : "scale(0)"}}>
<BsmButton onClick={() => setExpanded(!expanded)} className={buttonClassName ?? defaultButtonClassName} icon={icon} active={expanded} onClickOutside={() => {setExpanded(false)}} withBar={withBar}/>
<div className={`pt-1 pb-1 w-fit absolute cursor-pointer top-[calc(100%-4px)] rounded-md overflow-hidden bg-inherit text-sm text-gray-800 dark:text-gray-200 shadow-md shadow-black transition-[scale] ease-in-out ${align === "left" ? "left-0 origin-top-left" : "right-0 origin-top-right"}`} style={{scale: expanded ? "1" : "0", translate: `0 ${menuTranslationY}`}}>
{ items?.map((i, index) => !!i &&(
<div key={index} onClick={!!i.onClick ? i.onClick : undefined} className="flex justify-start items-center w-full pr-3 pl-3 pt-2 pb-2 hover:bg-light-main-color-3 dark:hover:bg-main-color-3">
<div key={index} onClick={!!i.onClick ? i.onClick : undefined} className="flex justify-start items-center w-full pr-3 pl-3 pt-2 pb-2 hover:backdrop-brightness-150">
{i.icon && <BsmIcon icon={i.icon} className="h-5 w-5 mr-1 text-gray-800 dark:text-white"></BsmIcon>}
<span className="w-max">{t(i.text)}</span>
</div>
@@ -21,9 +21,10 @@ import { ExportIcon } from "./icons/export-icon.component";
import PatreonIcon from "./icons/patreon-icon.component";
import { SearchIcon } from "./icons/search-icon.component";
import { CheckIcon } from "./icons/check-icon.component";
import { ThreeDotsIcon } from "./icons/three-dots-icon.component";
export type BsmIconType = (
"settings"|"trash"|"favorite"|"folder"|"bsNote"|"check"|
"settings"|"trash"|"favorite"|"folder"|"bsNote"|"check"|"three-dots"|
"terminal"|"desktop"|"oculus"|"add"|"cross"|"task"|
"copy"|"steam"|"edit"|"export"|"patreon"|"search"|
"fr-FR-flag"|"es-ES-flag"|"en-US-flag"|"en-EN-flag"
@@ -54,6 +55,7 @@ export const BsmIcon = memo(({className, icon, style}: {className?: string, icon
if(icon === "patreon"){ return <PatreonIcon className={className} style={style}/> }
if(icon === "search"){ return <SearchIcon className={className} style={style}/> }
if(icon === "check"){ return <CheckIcon className={className} style={style}/> }
if(icon === "three-dots"){ return <ThreeDotsIcon className={className} style={style}/> }
return <TrashIcon className={className} style={style}/>
}
@@ -0,0 +1,9 @@
import { CSSProperties } from "react";
export function ThreeDotsIcon(props: {className?: string, style?: CSSProperties}) {
return (
<svg className={props.className} style={props.style} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24">
<path fill="currentColor" d="M12.025 21.15q-1 0-1.7-.7t-.7-1.675q0-1 .7-1.7t1.675-.7q1 0 1.688.7.687.7.687 1.7 0 .975-.687 1.675-.688.7-1.663.7Zm0-6.775q-1 0-1.7-.7T9.625 12q0-1 .7-1.688.7-.687 1.675-.687 1 0 1.688.687.687.688.687 1.663 0 1-.687 1.7-.688.7-1.663.7Zm0-6.75q-1 0-1.7-.713-.7-.712-.7-1.687 0-1 .7-1.688.7-.687 1.675-.687 1 0 1.688.687.687.688.687 1.688 0 .975-.687 1.687-.688.713-1.663.713Z"/>
</svg>
)
}
@@ -3,30 +3,39 @@ import { BsmCheckbox } from "renderer/components/shared/bsm-checkbox.component";
import { Mod } from "shared/models/mods/mod.interface";
import { CSSProperties } from "react"
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
import { BsModsManagerService } from "renderer/services/bs-mods-manager.service";
import { useObservable } from "renderer/hooks/use-observable.hook";
import { PageStateService } from "renderer/services/page-state.service";
type Props = {mod: Mod, installedVersion: string, isDependency?: boolean, isSelected?: boolean, onChange?: (val: boolean) => void, wantInfo?: boolean}
export function ModItem({mod, installedVersion, isDependency, isSelected, onChange, wantInfo}: Props) {
const modsManager = BsModsManagerService.getInstance();
const pageState = PageStateService.getInstance();
const themeColor = useThemeColor("second-color");
const uninstalling = useObservable(modsManager.isUninstalling$);
const wantInfoStyle: CSSProperties = wantInfo ? {borderColor: themeColor} : {borderColor: "transparent"};
const isOutDated = (() => {
return installedVersion < mod.version;
})()
const isOutDated = installedVersion < mod.version;
const uninstall = () => {
modsManager.uninstallMod(mod, pageState.getState());
}
return (
<>
<div className="h-full aspect-square flex items-center justify-center p-[7px] rounded-l-md bg-main-color-1 ml-3 border-t-2 border-b-2 border-l-2" style={wantInfoStyle}>
<BsmCheckbox className="h-full aspect-square z-[1] relative bg-main-color-1" onChange={onChange} disabled={mod.required || isDependency} checked={isDependency || isSelected || mod.required}/>
<div className="contents bg-light-main-color-3 dark:bg-main-color-1 text-main-color-1 dark:text-light-main-color-1">
<div className="h-full aspect-square flex items-center justify-center p-[7px] rounded-l-md bg-inherit ml-3 border-t-2 border-b-2 border-l-2" style={wantInfoStyle}>
<BsmCheckbox className="h-full aspect-square z-[1] relative bg-inherit" onChange={onChange} disabled={mod.required || isDependency} checked={isDependency || isSelected || mod.required}/>
</div>
<span className="bg-main-color-1 py-2 pl-3 font-bold text-sm whitespace-nowrap border-t-2 border-b-2" style={wantInfoStyle}>{mod.name}</span>
<span className={`min-w-0 text-center bg-main-color-1 py-2 px-1 text-sm border-t-2 border-b-2 ${(installedVersion && isOutDated) && "text-red-400 line-through"} ${installedVersion && !isOutDated && "text-green-400"}`} style={wantInfoStyle}>{installedVersion || "-"}</span>
<span className="min-w-0 text-center bg-main-color-1 py-2 px-1 text-sm border-t-2 border-b-2" style={wantInfoStyle}>{mod.version}</span>
<span title={mod.description} className="px-3 bg-main-color-1 whitespace-nowrap text-ellipsis overflow-hidden py-2 text-sm border-t-2 border-b-2" style={wantInfoStyle}>{mod.description}</span>
<div className="h-full bg-main-color-1 flex items-center justify-center mr-3 rounded-r-md pr-2 border-t-2 border-b-2 border-r-2" style={wantInfoStyle}>
{(installedVersion && !mod.required) && <BsmButton className="h-7 w-7 p-[5px] rounded-full" icon="trash" withBar={false}/>}
<span className="bg-inherit py-2 pl-3 font-bold text-sm whitespace-nowrap border-t-2 border-b-2" style={wantInfoStyle}>{mod.name}</span>
<span className={`min-w-0 text-center bg-inherit py-2 px-1 text-sm border-t-2 border-b-2 ${(installedVersion && isOutDated) && "text-red-400 line-through"} ${installedVersion && !isOutDated && "text-green-400"}`} style={wantInfoStyle}>{installedVersion || "-"}</span>
<span className="min-w-0 text-center bg-inherit py-2 px-1 text-sm border-t-2 border-b-2" style={wantInfoStyle}>{mod.version}</span>
<span title={mod.description} className="px-3 bg-inherit whitespace-nowrap text-ellipsis overflow-hidden py-2 text-sm border-t-2 border-b-2" style={wantInfoStyle}>{mod.description}</span>
<div className="h-full bg-inherit flex items-center justify-center mr-3 rounded-r-md pr-2 border-t-2 border-b-2 border-r-2" style={wantInfoStyle}>
{installedVersion && <BsmButton className="z-[1] h-7 w-7 p-[5px] rounded-full" icon="trash" disabled={uninstalling} withBar={false} onClick={e => {e.stopPropagation(); uninstall()}}/>}
</div>
</>
</div>
)
}
@@ -1,6 +1,9 @@
import { motion } from "framer-motion";
import { useState } from "react";
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { BsmDropdownButton } from "renderer/components/shared/bsm-dropdown-button.component";
import { BsModsManagerService } from "renderer/services/bs-mods-manager.service";
import { PageStateService } from "renderer/services/page-state.service";
import { Mod } from "shared/models/mods/mod.interface"
import { ModItem } from "./mod-item.component"
@@ -8,6 +11,9 @@ type Props = {modsMap: Map<string, Mod[]>, installed: Map<string, Mod[]>, modsSe
export function ModsGrid({modsMap, installed, modsSelected, onModChange, moreInfoMod, onWantInfos}: Props) {
const pageState = PageStateService.getInstance();
const modsManager = BsModsManagerService.getInstance();
const [filter, setFilter] = useState("");
const [filterEnabled, setFilterEnabled] = useState(false);
@@ -35,15 +41,17 @@ export function ModsGrid({modsMap, installed, modsSelected, onModChange, moreInf
setFilterEnabled(b => !b);
}
return modsMap && (
<div className="grid gap-y-1 grid-cols-[40px_min-content_min-content_min-content_1fr_min-content]">
<span className="absolute z-10 top-0 w-full h-8 bg-main-color-2"/>
<span className="z-10 sticky flex items-center justify-center top-0 bg-main-color-2 border-b-2 border-main-color-1">
<div className="pl-4">
<BsmButton className="rounded-full h-6 p-[2px]" withBar={false} icon="search" onClick={handleToogleFilter}/>
</div>
const handleUninstallAll = () => {
modsManager.uninstallAllMods(pageState.getState())
}
return modsMap && (
<div className="grid gap-y-1 grid-cols-[40px_min-content_min-content_min-content_1fr_min-content] bg-light-main-color-2 dark:bg-main-color-2 text-main-color-1 dark:text-light-main-color-1">
<span className="absolute z-10 top-0 w-full h-8 bg-inherit"/>
<span className="z-10 sticky flex items-center justify-end top-0 bg-inherit border-b-2 border-main-color-1">
<BsmButton className="rounded-full h-6 w-6 p-[2px]" withBar={false} icon="search" onClick={handleToogleFilter}/>
</span>
<span className="z-10 sticky top-0 flex items-center bg-main-color-2 border-main-color-1 border-b-2 h-8 px-1">
<span className="z-10 sticky top-0 flex items-center bg-inherit border-main-color-1 border-b-2 h-8 px-1">
{(filterEnabled ? (
<motion.input autoFocus className="bg-main-color-1 rounded-md h-6 px-2" initial={{width: 0}} animate={{width: "250px"}} transition={{ease:"easeInOut", duration:.15}} onChange={e => handleInput(e.target.value)}/>
):(
@@ -51,10 +59,14 @@ export function ModsGrid({modsMap, installed, modsSelected, onModChange, moreInf
))}
</span>
<span className="z-10 sticky flex items-center justify-center top-0 bg-main-color-2 border-b-2 border-main-color-1 h-8 px-2">Installé</span>
<span className="z-10 sticky flex items-center justify-center top-0 bg-main-color-2 border-b-2 border-main-color-1 h-8 px-2">Récent</span>
<span className="z-10 sticky flex items-center justify-center top-0 bg-main-color-2 border-b-2 border-main-color-1 h-8">Description</span>
<span className="z-10 sticky top-0 bg-main-color-2 border-b-2 border-main-color-1 h-8"></span>
<span className="z-10 sticky flex items-center justify-center top-0 bg-inherit border-b-2 border-main-color-1 h-8 px-2">Installé</span>
<span className="z-10 sticky flex items-center justify-center top-0 bg-inherit border-b-2 border-main-color-1 h-8 px-2">Récent</span>
<span className="z-10 sticky flex items-center justify-center top-0 bg-inherit border-b-2 border-main-color-1 h-8">Description</span>
<span className="z-10 sticky top-0 bg-inherit border-b-2 border-main-color-1 h-8 flex justify-start items-center py-1 pl-[3px] min-w-[50px]">
<BsmDropdownButton className="h-full aspect-square relative rounded-full bg-light-main-color-1 dark:bg-main-color-3" withBar={false} icon="three-dots" buttonClassName="!rounded-full !p-[2px] !bg-light-main-color-2 dark:!bg-main-color-2 hover:!bg-light-main-color-1 dark:hover:!bg-main-color-3" menuTranslationY="5px" items={[
({text: "Tout désintaller", icon: "trash", onClick: handleUninstallAll}),
]}/>
</span>
{
Array.from(modsMap.keys()).map(key => modsMap.get(key).some(mod => mod.name.toLowerCase().includes(filter)) && (
@@ -64,11 +76,10 @@ export function ModsGrid({modsMap, installed, modsSelected, onModChange, moreInf
<div className="contents cursor-pointer" onClick={() => onWantInfos(mod)} key={mod.name}>
<ModItem mod={mod} installedVersion={installedModVersion(key, mod)} isDependency={isDependency(mod)} isSelected={isSelected(mod)} onChange={(val) => onModChange(val, mod)} wantInfo={mod.name === moreInfoMod?.name}/>
</div>
))}
</div>
))
}
</div>
)
)
}
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { useEffect, useLayoutEffect, useRef, useState } from "react";
import { BsModsManagerService } from "renderer/services/bs-mods-manager.service";
import { BSVersion } from "shared/bs-version.interface";
import VisibilitySensor from 'react-visibility-sensor';
@@ -11,6 +11,9 @@ import { IpcService } from "renderer/services/ipc.service";
import BeatWaitingImg from "../../../../../../assets/images/apngs/beat-waiting.png"
import { SpoilerClick } from "renderer/components/shared/UwU/spoiler-click.component";
import YuruYuriDance from "../../../../../../assets/images/gifs/yuruyuri-dance.gif"
import { useObservable } from "renderer/hooks/use-observable.hook";
import { skip, filter } from "rxjs/operators";
import { Subscription } from "rxjs";
export function ModsSlide({version}: {version: BSVersion}) {
@@ -22,7 +25,11 @@ export function ModsSlide({version}: {version: BSVersion}) {
const [modsAvailable, setModsAvailable] = useState(null as Map<string, Mod[]>);
const [modsInstalled, setModsInstalled] = useState(null as Map<string, Mod[]>);
const [modsSelected, setModsSelected] = useState([] as Mod[]);
const [moreInfoMod, setMoreInfoMod] = useState(null as Mod);
const [moreInfoMod, setMoreInfoMod] = useState(null as Mod);
const installing = useObservable(modsManager.isInstalling$);
const downloadRef = useRef(null);
const [downloadWith, setDownloadWidth] = useState(0);
const modsToCategoryMap = (mods: Mod[]): Map<string, Mod[]> => {
if(!mods){ return new Map<string, Mod[]>(); }
@@ -49,34 +56,52 @@ export function ModsSlide({version}: {version: BSVersion}) {
}
const installMods = () => {
// NEXT THING TO DO
if(installing){ return; }
modsManager.installMods(modsSelected, version).then(() => {
loadMods();
});
}
console.log(modsSelected);
const loadMods = () => {
Promise.all([
modsManager.getAvailableMods(version),
modsManager.getInstalledMods(version)
]).then(([available, installed]) => {
const defaultMods = configService.get<string[]>("default_mods" as DefaultConfigKey);
setModsAvailable(modsToCategoryMap(available));
setModsSelected(available.filter(m => m.required || defaultMods.some(d => m.name.toLowerCase() === d.toLowerCase()) || installed.some(i => m.name === i.name)));
setModsInstalled(modsToCategoryMap(installed))
});
}
useEffect(() => {
const subs: Subscription[] = [];
if(isVisible){
Promise.all([
modsManager.getAvailableMods(version),
modsManager.getInstalledMods(version)
]).then(([available, installed]) => {
const defaultMods = configService.get<string[]>("default_mods" as DefaultConfigKey);
setModsAvailable(modsToCategoryMap(available));
setModsSelected(available.filter(m => m.required || defaultMods.some(d => m.name === d)));
setModsInstalled(modsToCategoryMap(installed))
});
loadMods();
subs.push(modsManager.isUninstalling$.pipe(skip(1), filter(uninstalling => !uninstalling)).subscribe(() => {
loadMods();
}))
}
return () => {
setMoreInfoMod(null);
setModsAvailable(null);
setModsInstalled(null);
subs.forEach(s => s.unsubscribe());
}
}, [isVisible, version]);
useLayoutEffect(() => {
if(modsAvailable){
setDownloadWidth(downloadRef?.current?.offsetWidth)
}
}, [modsAvailable])
return (
<VisibilitySensor onChange={setIsVisible}>
<div className='shrink-0 w-full h-full px-8 pb-7 flex justify-center'>
@@ -87,8 +112,10 @@ export function ModsSlide({version}: {version: BSVersion}) {
<ModsGrid modsMap={modsAvailable} installed={modsInstalled} modsSelected={modsSelected} onModChange={handleModChange} moreInfoMod={moreInfoMod} onWantInfos={handleMoreInfo}/>
</div>
<div className="h-10 shrink-0 flex items-center justify-between px-3">
<BsmButton className="rounded-md px-2 py-[2px]" text="Plus d'infos" typeColor="cancel" withBar={false} disabled={!moreInfoMod} onClick={handleOpenMoreInfo}/>
<BsmButton className="rounded-md px-2 py-[2px]" text="Installer ou mettre à jour" withBar={false} typeColor="primary" onClick={installMods}/>
<BsmButton className="text-center rounded-md px-2 py-[2px]" text="Plus d'infos" typeColor="cancel" withBar={false} disabled={!moreInfoMod} onClick={handleOpenMoreInfo} style={{width: downloadWith}}/>
<div ref={downloadRef}>
<BsmButton className="text-center rounded-md px-2 py-[2px]" text="Installer ou mettre à jour" withBar={false} disabled={installing} typeColor="primary" onClick={installMods}/>
</div>
</div>
</>
) : (
@@ -78,7 +78,7 @@ export function VersionViewer() {
<ModsSlide version={state}/>
</div>
</div>
<BsmDropdownButton className='absolute top-5 right-5 h-9 w-9' items={[
<BsmDropdownButton className='absolute top-5 right-5 h-9 w-9 bg-light-main-color-2 dark:bg-main-color-2 rounded-md' items={[
{text: "pages.version-viewer.dropdown.open-folder", icon: "folder", onClick: openFolder},
(!state.steam && {text: "pages.version-viewer.dropdown.verify-files", icon: "task", onClick: verifyFiles}),
(!state.steam && {text: "Exporter les misc.maps", icon: "export", onClick: exportMaps}),
@@ -1,12 +1,23 @@
import { BehaviorSubject } from "rxjs";
import { map } from "rxjs/operators";
import { BSVersion } from "shared/bs-version.interface";
import { Mod } from "shared/models/mods/mod.interface";
import { Mod, ModInstallProgression } from "shared/models/mods/mod.interface";
import { IpcService } from "./ipc.service";
import { ModalExitCode, ModalService, ModalType } from "./modale.service";
import { NotificationService } from "./notification.service";
import { ProgressBarService } from "./progress-bar.service";
export class BsModsManagerService {
private static instance: BsModsManagerService;
private readonly ipcService: IpcService;
private readonly progressBar: ProgressBarService;
private readonly modals: ModalService;
private readonly notifications: NotificationService;
public readonly isInstalling$: BehaviorSubject<boolean> = new BehaviorSubject(false);
public readonly isUninstalling$: BehaviorSubject<boolean> = new BehaviorSubject(false);
public static getInstance(): BsModsManagerService{
if(!BsModsManagerService.instance){ BsModsManagerService.instance = new BsModsManagerService(); }
@@ -15,6 +26,9 @@ export class BsModsManagerService {
private constructor(){
this.ipcService = IpcService.getInstance();
this.progressBar = ProgressBarService.getInstance();
this.modals = ModalService.getInsance();
this.notifications = NotificationService.getInstance();
}
public getAvailableMods(version: BSVersion): Promise<Mod[]>{
@@ -26,11 +40,46 @@ export class BsModsManagerService {
}
public installMods(mods: Mod[], version: BSVersion){
throw "NOT IMPLEMENTED YET";
if(!this.progressBar.require()){ return; }
const progress$ = this.ipcService.watch<ModInstallProgression>("mod-installed").pipe(map(res => res.data.progression));
this.progressBar.show(progress$, true, {paddingLeft: "190px", paddingRight: "190px", bottom: "20px"});
this.isInstalling$.next(true);
return this.ipcService.send<number, {mods: Mod[], version: BSVersion}>("install-mods", {args: {mods, version}}).then(res => {
this.isInstalling$.next(false);
this.progressBar.hide();
});
}
public async uninstallMod(mod: Mod, version: BSVersion){
const modalRes = await this.modals.openModal(ModalType.UNINSTALL_MOD, mod);
if(modalRes.exitCode !== ModalExitCode.COMPLETED || !this.progressBar.require()){ return; }
const progress$ = this.ipcService.watch<ModInstallProgression>("mod-uninstalled").pipe(map(res => res.data.progression));
this.progressBar.show(progress$, true, {paddingLeft: "190px", paddingRight: "190px", bottom: "20px"});
this.isUninstalling$.next(true);
return this.ipcService.send("uninstall-mods", {args: {mods: [mod], version}}).then(res => {
this.isUninstalling$.next(false);
this.progressBar.hide();
});
}
public uninstallMods(mods: Mod[], version: BSVersion){
throw "NOT IMPLEMENTED YET";
public async uninstallAllMods(version: BSVersion){
//TODO MODAL
const progress$ = this.ipcService.watch<ModInstallProgression>("mod-uninstalled").pipe(map(res => res.data.progression));
this.progressBar.show(progress$, true, {paddingLeft: "190px", paddingRight: "190px", bottom: "20px"});
this.isUninstalling$.next(true);
return this.ipcService.send("uninstall-all-mods", {args: version}).then(res => {
this.isUninstalling$.next(false);
this.progressBar.hide();
});
}
public isModsAvailable(version: BSVersion): Promise<boolean>{
+5 -4
View File
@@ -50,23 +50,24 @@ export class ModalService{
}
export enum ModalType {
export const enum ModalType {
STEAM_LOGIN = "STEAM_LOGIN",
GUARD_CODE = "GUARD_CODE",
UNINSTALL = "UNINSTALL",
INSTALLATION_FOLDER = "INSTALLATION_FOLDER",
EDIT_VERSION = "EDIT_VERSION",
CLONE_VERSION = "CLONE_VERSION"
CLONE_VERSION = "CLONE_VERSION",
UNINSTALL_MOD = "UNINSTALL_MOD"
}
export enum ModalExitCode {
export const enum ModalExitCode {
NO_CHOICE = -1,
COMPLETED = 0,
CLOSED = 1,
CANCELED = 2,
}
export interface ModalResponse<T> {
export interface ModalResponse<T = unknown> {
exitCode: ModalExitCode,
data?: T
}
@@ -0,0 +1,30 @@
import { BehaviorSubject } from "rxjs";
export class PageStateService {
private static instance: PageStateService;
private readonly _state$: BehaviorSubject<any> = new BehaviorSubject(undefined);
public static getInstance(): PageStateService{
if(!PageStateService.instance){ PageStateService.instance = new PageStateService(); }
return PageStateService.instance;
}
private constructor(){
this.state$.subscribe(a => console.log(a))
}
public setState(state: unknown){
this._state$.next(state);
}
public getState<T = unknown>(): T{
return this._state$.value;
}
public get state$(){
return this._state$.asObservable();
}
}
@@ -2,6 +2,7 @@ import { distinctUntilChanged, map } from "rxjs/operators";
import { BehaviorSubject, Observable, Subscription, timer } from "rxjs";
import { IpcService } from "./ipc.service";
import { NotificationService } from "./notification.service";
import { CSSProperties } from "react";
export class ProgressBarService{
@@ -12,6 +13,7 @@ export class ProgressBarService{
private readonly _progression$: BehaviorSubject<number>;
private readonly _visible$: BehaviorSubject<boolean>;
private readonly _style$: BehaviorSubject<CSSProperties>;
private subscription: Subscription;
@@ -26,6 +28,7 @@ export class ProgressBarService{
this._progression$ = new BehaviorSubject<number>(0);
this._visible$ = new BehaviorSubject<boolean>(false);
this._style$ = new BehaviorSubject<CSSProperties>(undefined);
this.progression$.subscribe(progression => this.setSystemProgression(progression));
}
@@ -47,13 +50,14 @@ export class ProgressBarService{
this.subscription = null;
}
public show(obs?: Observable<number>, unsubscribe? : boolean){
public show(obs?: Observable<number>, unsubscribe? : boolean, style?: CSSProperties){
if(unsubscribe){ this.unsubscribe(); }
if(obs){ this.subscribreTo(obs); }
this.visible$.next(true);
this.visible$.next(true);
this._style$.next(style);
}
public showFake(speed: number): void{
public showFake(speed: number, style?: CSSProperties): void{
const obs = timer(1000, 100).pipe(map(val => {
if(this.progression$.value >= 100){ return 100; }
const currentProgress = speed * (val + 1);
@@ -66,7 +70,7 @@ export class ProgressBarService{
this.progression$.next(100);
}
public hide(unsubscribe: boolean){
public hide(unsubscribe: boolean = true){
if(unsubscribe){ this.unsubscribe(); }
this.visible$.next(false);
}
@@ -82,6 +86,7 @@ export class ProgressBarService{
public get progression$(): BehaviorSubject<number>{ return this._progression$; }
public get visible$(): BehaviorSubject<boolean>{ return this._visible$; }
public get isVisible(): boolean{ return this._visible$.value; }
public get style$(): BehaviorSubject<CSSProperties>{ return this._style$; }
+5
View File
@@ -34,4 +34,9 @@ export type DownloadLinkType = "universal"|"steam"|"oculus";
export interface FileHashes {
hash: string,
file: string
}
export interface ModInstallProgression{
name: string,
progression: number
}