mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
install mods & uninstall mod & uninstall all mods & ligth theme & some modals
This commit is contained in:
@@ -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});
|
||||
})
|
||||
});
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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>{
|
||||
|
||||
@@ -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$; }
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -34,4 +34,9 @@ export type DownloadLinkType = "universal"|"steam"|"oculus";
|
||||
export interface FileHashes {
|
||||
hash: string,
|
||||
file: string
|
||||
}
|
||||
|
||||
export interface ModInstallProgression{
|
||||
name: string,
|
||||
progression: number
|
||||
}
|
||||
Reference in New Issue
Block a user