mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
can now export maps of a bs version
This commit is contained in:
@@ -3,3 +3,4 @@ import './os-controls-ipcs';
|
||||
import './bs-launcher-ipcs';
|
||||
import './bs-version-ipcs';
|
||||
import './bs-uninstall-ipcs';
|
||||
import './map-ipcs';
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ipcMain} from 'electron';
|
||||
import { MapService } from '../services/map.service';
|
||||
import { UtilsService } from '../services/utils.service';
|
||||
import { IpcRequest } from 'shared/models/ipc';
|
||||
import { ExportVersionMapsOption } from 'shared/models/maps/export-version-maps.model';
|
||||
|
||||
ipcMain.on("map.export-version", (event, request: IpcRequest<ExportVersionMapsOption>) => {
|
||||
const utils = UtilsService.getInstance();
|
||||
const mapService = MapService.getInstance();
|
||||
mapService.exportVersionMaps(request.args.version, request.args.path).then(() => {
|
||||
utils.ipcSend(request.responceChannel, {success: true});
|
||||
}).catch(() => utils.ipcSend(request.responceChannel, {success: false, error: {title: "", msg: ""}}));
|
||||
})
|
||||
@@ -32,3 +32,11 @@ ipcMain.on('choose-folder', async (event, request: IpcRequest<void>) => {
|
||||
ipcMain.on("window.progression", async (event, request: IpcRequest<number>) => {
|
||||
getMainWindow().setProgressBar(request.args / 100);
|
||||
});
|
||||
|
||||
ipcMain.on('save-file', async (event, request: IpcRequest<{filename?: string, filters?: Electron.FileFilter[]}>) => {
|
||||
dialog.showSaveDialog({properties: ['showOverwriteConfirmation'], defaultPath: request.args.filename, filters: request.args.filters}).then(res => {
|
||||
const utils = UtilsService.getInstance();
|
||||
if(res.canceled || !res.filePath){ utils.ipcSend(request.responceChannel, {success: false}); }
|
||||
UtilsService.getInstance().ipcSend(request.responceChannel, {success: true, data: res.filePath});
|
||||
})
|
||||
});
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import path from "path";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { BSLocalVersionService } from "./bs-local-version.service";
|
||||
import archiver from "archiver";
|
||||
import { createWriteStream } from "fs";
|
||||
import log from "electron-log";
|
||||
|
||||
export class MapService{
|
||||
|
||||
private readonly MAP_PATH = path.join("Beat Saber_Data", "CustomLevels");
|
||||
|
||||
private static instance: MapService;
|
||||
|
||||
private readonly localVersionService: BSLocalVersionService;
|
||||
|
||||
public static getInstance(): MapService{
|
||||
if(!MapService.instance){ MapService.instance = new MapService(); }
|
||||
return MapService.instance;
|
||||
}
|
||||
|
||||
private constructor(){
|
||||
this.localVersionService = BSLocalVersionService.getInstance();
|
||||
}
|
||||
|
||||
private getMapsPath(versionPath: string): string{
|
||||
return path.join(versionPath, this.MAP_PATH);
|
||||
}
|
||||
|
||||
public async exportVersionMaps(version: BSVersion, outputZip: string): Promise<void>{
|
||||
const versionPath = await this.localVersionService.getVersionPath(version);
|
||||
const mapsPath = this.getMapsPath(versionPath);
|
||||
|
||||
console.log(outputZip);
|
||||
|
||||
const output = createWriteStream(outputZip);
|
||||
const archive = archiver("zip", {zlib: {level: 9}});
|
||||
|
||||
const promise = new Promise<void>((resolve, reject) => {
|
||||
archive.pipe(output);
|
||||
archive.directory(mapsPath, false);
|
||||
archive.on("error", reject);
|
||||
output.on("close", resolve);
|
||||
archive.finalize()
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { memo } from "react"
|
||||
|
||||
export const BsManagerIcon = memo(function ({className, color1, color2}: {className: string, color1: string, color2: string}) {
|
||||
export const BsManagerIcon = memo(({className, color1, color2}: {className: string, color1: string, color2: string}) => {
|
||||
return (
|
||||
<svg className={className} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000">
|
||||
<g fill={color1}>
|
||||
|
||||
@@ -17,15 +17,16 @@ import { CopyIcon } from "./icons/copy-icon.component";
|
||||
import { SteamIcon } from "./icons/steam-icon.component";
|
||||
import { CSSProperties, memo } from "react";
|
||||
import EditIcon from "./icons/edit-icon.component";
|
||||
import { ExportIcon } from "./icons/export-icon.component";
|
||||
|
||||
export type BsmIconType = (
|
||||
"settings"|"trash"|"favorite"|"folder"|"bsNote"|
|
||||
"terminal"|"desktop"|"oculus"|"add"|"cross"|"task"|
|
||||
"copy"|"steam"|"edit"|
|
||||
"copy"|"steam"|"edit"|"export"|
|
||||
"fr-FR-flag"|"es-ES-flag"|"en-US-flag"|"en-EN-flag"
|
||||
);
|
||||
|
||||
export const BsmIcon = memo(function({className, icon, style}: {className?: string, icon: BsmIconType, style?: CSSProperties}) {
|
||||
export const BsmIcon = memo(({className, icon, style}: {className?: string, icon: BsmIconType, style?: CSSProperties}) => {
|
||||
|
||||
const renderIcon = () => {
|
||||
if(icon === "settings"){ return <SettingIcon className={className} style={style}/> }
|
||||
@@ -46,6 +47,7 @@ export const BsmIcon = memo(function({className, icon, style}: {className?: stri
|
||||
if(icon === "copy"){ return <CopyIcon className={className} style={style}/> }
|
||||
if(icon === "steam"){ return <SteamIcon className={className} style={style}/> }
|
||||
if(icon === "edit"){ return <EditIcon className={className} style={style}/> }
|
||||
if(icon === "export"){ return <ExportIcon className={className} style={style}/> }
|
||||
return <TrashIcon className={className} style={style}/>
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { CSSProperties } from "react";
|
||||
|
||||
export function ExportIcon(props: {className?: string, style?: CSSProperties}) {
|
||||
return (
|
||||
<svg className={props.className} style={props.style} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" height="40" width="40" fill="currentColor">
|
||||
<path d="M20 26.625q-.667 0-1.125-.458-.458-.459-.458-1.125V11.958l-3.584 3.625q-.5.459-1.125.438-.625-.021-1.083-.479-.5-.5-.479-1.146.021-.646.479-1.104L18.875 7q.25-.25.542-.354.291-.104.583-.104t.583.104q.292.104.542.354l6.292 6.333q.458.459.458 1.105 0 .645-.458 1.104-.459.458-1.105.458-.645 0-1.145-.458l-3.584-3.584v13.084q0 .666-.458 1.125-.458.458-1.125.458Zm-10.708 7.25q-1.292 0-2.23-.937-.937-.938-.937-2.23v-4.583q0-.667.458-1.125.459-.458 1.125-.458.667 0 1.125.458.459.458.459 1.125v4.583h21.416v-4.583q0-.667.459-1.125.458-.458 1.125-.458.666 0 1.125.458.458.458.458 1.125v4.583q0 1.292-.937 2.23-.938.937-2.23.937Z"/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -74,10 +74,7 @@ export function SettingsPage() {
|
||||
}
|
||||
|
||||
const setDefaultInstallationFolder = () => {
|
||||
if(progressBarService.isVisible){
|
||||
notificationService.notifyError({title: "notifications.shared.errors.titles.operation-running", desc: "notifications.shared.errors.msg.operation-running", duration: 3000});
|
||||
return;
|
||||
}
|
||||
if(!progressBarService.require()){ return; }
|
||||
|
||||
modalService.openModal(ModalType.INSTALLATION_FOLDER).then(async res => {
|
||||
if(res.exitCode !== ModalExitCode.COMPLETED){ return; }
|
||||
@@ -110,12 +107,12 @@ export function SettingsPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex justify-center overflow-y-scroll pb-12 scrollbar-thin scrollbar-thumb-neutral-900 text-gray-800 dark:text-gray-200">
|
||||
<div className="w-full h-full flex justify-center overflow-y-scroll scrollbar-thin scrollbar-thumb-neutral-900 text-gray-800 dark:text-gray-200">
|
||||
|
||||
<div className="max-w-2xl w-full mt-10">
|
||||
|
||||
<SettingContainer title="pages.settings.steam.title" description="pages.settings.steam.description">
|
||||
<BsmButton onClick={deleteSteamSession} className={`w-fit px-3 py-[2px] text-white rounded-md`} withBar={false} text="pages.settings.steam.logout" typeColor="error" disabled={!sessionExist}/>
|
||||
<BsmButton onClick={deleteSteamSession} className="w-fit px-3 py-[2px] text-white rounded-md" withBar={false} text="pages.settings.steam.logout" typeColor="error" disabled={!sessionExist}/>
|
||||
</SettingContainer>
|
||||
|
||||
<SettingContainer title="pages.settings.appearance.title" description="pages.settings.appearance.description">
|
||||
@@ -142,7 +139,7 @@ export function SettingsPage() {
|
||||
<SettingRadioArray items={languagesItems} selectedItem={languageSelected} onItemSelected={handleChangeLanguage}/>
|
||||
</SettingContainer>
|
||||
|
||||
<div className="h-10"></div>
|
||||
<div className="h-10"/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -18,6 +18,7 @@ import { BsDownloaderService } from 'renderer/services/bs-downloader.service';
|
||||
import { useTranslation } from 'renderer/hooks/use-translation.hook';
|
||||
import { IpcService } from 'renderer/services/ipc.service';
|
||||
import { useObservable } from 'renderer/hooks/use-observable.hook';
|
||||
import { MapService } from 'renderer/services/maps.service';
|
||||
|
||||
export function VersionViewer() {
|
||||
|
||||
@@ -27,6 +28,7 @@ export function VersionViewer() {
|
||||
const modalService = ModalService.getInsance();
|
||||
const bsDownloaderService = BsDownloaderService.getInstance();
|
||||
const ipcService = IpcService.getInstance();
|
||||
const mapService = MapService.getInstance();
|
||||
|
||||
const {state} = useLocation() as {state: BSVersion};
|
||||
const navigate = useNavigate();
|
||||
@@ -95,12 +97,16 @@ export function VersionViewer() {
|
||||
});
|
||||
}
|
||||
|
||||
const clone = () => {
|
||||
bsVersionManagerService.cloneVersion(state).then(newVersion => {
|
||||
if(!newVersion){ return; }
|
||||
navigateToVersion(newVersion);
|
||||
});
|
||||
}
|
||||
const clone = () => {
|
||||
bsVersionManagerService.cloneVersion(state).then(newVersion => {
|
||||
if(!newVersion){ return; }
|
||||
navigateToVersion(newVersion);
|
||||
});
|
||||
}
|
||||
|
||||
const exportMaps = () => {
|
||||
mapService.exportVersionMaps(state);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -137,6 +143,7 @@ export function VersionViewer() {
|
||||
<BsmDropdownButton className='absolute top-5 right-5 h-9 w-9' 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}),
|
||||
(!state.steam && {text: "pages.version-viewer.dropdown.edit", icon: "edit", onClick: edit}),
|
||||
{text: "pages.version-viewer.dropdown.clone", icon: "copy", onClick: clone},
|
||||
(!state.steam && {text: "pages.version-viewer.dropdown.uninstall", icon:"trash", onClick: uninstall})
|
||||
@@ -145,17 +152,17 @@ export function VersionViewer() {
|
||||
)
|
||||
}
|
||||
|
||||
function ToogleLunchMod(props: {onClick: Function, active: boolean, text: string, icon: BsmIconType}) {
|
||||
function ToogleLunchMod(props: {onClick: () => void, active: boolean, text: string, icon: BsmIconType}) {
|
||||
|
||||
const t = useTranslation();
|
||||
|
||||
return (
|
||||
<div className={`relative rounded-full cursor-pointer group active:scale-95 transition-transform ${!props.active && "shadow-md shadow-black"}`} onClick={() => props.onClick()}>
|
||||
<div className={`absolute glow-on-hover rounded-full ${props.active && "opacity-100 blur-[2px]"}`}></div>
|
||||
<div className='w-full h-full pl-6 pr-6 flex justify-center items-center bg-light-main-color-2 dark:bg-main-color-2 p-3 rounded-full text-gray-800 dark:text-white group-hover:bg-light-main-color-1 dark:group-hover:bg-main-color-1'>
|
||||
<BsmIcon icon={props.icon} className='mr-1 h-7 text-gray-800 dark:text-white'/>
|
||||
<span className='w-fit min-w-fit h-full text-lg font-bold uppercase tracking-wide italic'>{t(props.text)}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className={`relative rounded-full cursor-pointer group active:scale-95 transition-transform ${!props.active && "shadow-md shadow-black"}`} onClick={() => props.onClick()}>
|
||||
<div className={`absolute glow-on-hover rounded-full ${props.active && "opacity-100 blur-[2px]"}`}/>
|
||||
<div className='w-full h-full pl-6 pr-6 flex justify-center items-center bg-light-main-color-2 dark:bg-main-color-2 p-3 rounded-full text-gray-800 dark:text-white group-hover:bg-light-main-color-1 dark:group-hover:bg-main-color-1'>
|
||||
<BsmIcon icon={props.icon} className='mr-1 h-7 text-gray-800 dark:text-white'/>
|
||||
<span className='w-fit min-w-fit h-full text-lg font-bold uppercase tracking-wide italic'>{t(props.text)}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -82,11 +82,8 @@ export class BsDownloaderService{
|
||||
return this.ipcService.send<boolean>("bs-download.kill");
|
||||
}
|
||||
|
||||
public async download(bsVersion: BSVersion, isVerification?: boolean, isFirstCall: boolean = true): Promise<IpcResponse<DownloadEvent>>{
|
||||
if(isFirstCall && this.progressBarService.isVisible){
|
||||
this.notificationService.notifyError({title: "notifications.shared.errors.titles.operation-running", desc: "notifications.shared.errors.msg.operation-running", duration: 3000});
|
||||
return {success: false};
|
||||
}
|
||||
public async download(bsVersion: BSVersion, isVerification?: boolean, isFirstCall = true): Promise<IpcResponse<DownloadEvent>>{
|
||||
if(isFirstCall && !this.progressBarService.require()){ return {success: false}; }
|
||||
|
||||
this.progressBarService.show(this.downloadProgress$);
|
||||
this._isVerification = !!isVerification;
|
||||
|
||||
@@ -88,10 +88,7 @@ export class BSVersionManagerService {
|
||||
}
|
||||
|
||||
public async cloneVersion(version: BSVersion): Promise<BSVersion>{
|
||||
if(this.progressBarService.isVisible){
|
||||
this.notificationService.notifyError({title: "notifications.shared.errors.titles.operation-running", desc: "notifications.shared.errors.msg.operation-running", duration: 3000});
|
||||
return null;
|
||||
}
|
||||
if(!this.progressBarService.require()){ return null; }
|
||||
const modalRes = await this.modalService.openModal<{name: string, color: string}>(ModalType.CLONE_VERSION, version);
|
||||
if(modalRes.exitCode !== ModalExitCode.COMPLETED){ return null; }
|
||||
if(modalRes.data.name?.length < 2){ return null; }
|
||||
|
||||
@@ -16,7 +16,7 @@ export class IpcService {
|
||||
this.channelObservables = new Map<string, Observable<IpcResponse<unknown>>>();
|
||||
}
|
||||
|
||||
public send<T>(channel: string, request?: IpcRequest<any>): Promise<IpcResponse<T>>{
|
||||
public send<T, U = unknown>(channel: string, request?: IpcRequest<U>): Promise<IpcResponse<T>>{
|
||||
if(!request){ request = {args: null, responceChannel: null}; }
|
||||
if(!request.responceChannel){ request.responceChannel = `${channel}_responce_${new Date().getTime()}`; }
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { OpenSaveDialogOption } from "shared/models/ipc";
|
||||
import { ExportVersionMapsOption } from "shared/models/maps/export-version-maps.model";
|
||||
import { IpcService } from "./ipc.service";
|
||||
import { NotificationService } from "./notification.service";
|
||||
import { ProgressBarService } from "./progress-bar.service";
|
||||
|
||||
export class MapService {
|
||||
|
||||
private static instance: MapService;
|
||||
|
||||
private readonly ipcService: IpcService;
|
||||
private readonly progressService: ProgressBarService;
|
||||
private readonly notificationService: NotificationService;
|
||||
|
||||
public static getInstance(): MapService{
|
||||
if(!MapService.instance){ MapService.instance = new MapService(); }
|
||||
return MapService.instance;
|
||||
}
|
||||
|
||||
private constructor(){
|
||||
this.ipcService = IpcService.getInstance();
|
||||
this.notificationService = NotificationService.getInstance();
|
||||
this.progressService = ProgressBarService.getInstance();
|
||||
}
|
||||
|
||||
public async exportVersionMaps(version: BSVersion): Promise<void>{
|
||||
if(!this.progressService.require()){ return; }
|
||||
|
||||
const resFile = await this.ipcService.send<string, OpenSaveDialogOption>("save-file", {args: {
|
||||
filename: `${version.BSVersion} Maps`,
|
||||
filters: [{name: "zip", extensions: ["zip"]}]
|
||||
}});
|
||||
|
||||
if(!resFile.success){ return; }
|
||||
|
||||
this.progressService.showFake(.008);
|
||||
|
||||
const resExport = await this.ipcService.send<string, ExportVersionMapsOption>("map.export-version", {args: {version, path: resFile.data}});
|
||||
if(!resExport.success && resExport.error.title){
|
||||
const {title, msg} = resExport.error;
|
||||
this.notificationService.notifyError({title, desc: msg});
|
||||
}
|
||||
else {
|
||||
this.notificationService.notifySuccess({title: "Export terminé 🎉", duration: 3000});
|
||||
}
|
||||
this.progressService.complete();
|
||||
this.progressService.hide(true);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,7 +17,7 @@ export class NotificationService{
|
||||
}
|
||||
|
||||
public notify(notification: Notification): Promise<NotificationResult|string>{
|
||||
let resovableNotification: ResolvableNotification = {id: uuidv4(), notification: notification, resolver: null };
|
||||
const resovableNotification: ResolvableNotification = {id: uuidv4(), notification, resolver: null };
|
||||
const promise = new Promise<NotificationResult|string>(resolve => {
|
||||
resovableNotification.resolver = resolve;
|
||||
setTimeout(() => resolve(NotificationResult.NO_CHOICE), notification.duration || 7000);
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { distinctUntilChanged, map } from "rxjs/operators";
|
||||
import { BehaviorSubject, Observable, Subscription, timer } from "rxjs";
|
||||
import { IpcService } from "./ipc.service";
|
||||
import { NotificationService } from "./notification.service";
|
||||
|
||||
export class ProgressBarService{
|
||||
|
||||
private static instance: ProgressBarService;
|
||||
|
||||
private readonly ipcService: IpcService;
|
||||
private notificationService: NotificationService;
|
||||
|
||||
private readonly _progression$: BehaviorSubject<number>;
|
||||
private readonly _visible$: BehaviorSubject<boolean>;
|
||||
@@ -20,6 +22,7 @@ export class ProgressBarService{
|
||||
|
||||
private constructor(){
|
||||
this.ipcService = IpcService.getInstance();
|
||||
this.notificationService = NotificationService.getInstance();
|
||||
|
||||
this._progression$ = new BehaviorSubject<number>(0);
|
||||
this._visible$ = new BehaviorSubject<boolean>(false);
|
||||
@@ -68,6 +71,14 @@ export class ProgressBarService{
|
||||
this.visible$.next(false);
|
||||
}
|
||||
|
||||
public require(): boolean{
|
||||
if(this.isVisible){
|
||||
this.notificationService.notifyError({title: "notifications.shared.errors.titles.operation-running", desc: "notifications.shared.errors.msg.operation-running", duration: 3000});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public get progression$(): BehaviorSubject<number>{ return this._progression$; }
|
||||
public get visible$(): BehaviorSubject<boolean>{ return this._visible$; }
|
||||
public get isVisible(): boolean{ return this._visible$.value; }
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { IpcRequest } from "./ipc-request.interface"
|
||||
export { IpcResponse } from "./ipc-response.interface"
|
||||
export { IpcResponse } from "./ipc-response.interface"
|
||||
export { OpenSaveDialogOption } from "../os/dialog.model"
|
||||
@@ -0,0 +1,3 @@
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
|
||||
export interface ExportVersionMapsOption {version: BSVersion, path: string};
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface OpenSaveDialogOption {
|
||||
filename?: string,
|
||||
filters?: Electron.FileFilter[]
|
||||
}
|
||||
Reference in New Issue
Block a user