can now export maps of a bs version

This commit is contained in:
MathieuG-P
2022-08-06 20:11:27 +02:00
parent dae85a1c34
commit b2c58e47ca
21 changed files with 420 additions and 1305 deletions
+1
View File
@@ -3,3 +3,4 @@ import './os-controls-ipcs';
import './bs-launcher-ipcs';
import './bs-version-ipcs';
import './bs-uninstall-ipcs';
import './map-ipcs';
+13
View File
@@ -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: ""}}));
})
+8
View File
@@ -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});
})
});
+49
View File
@@ -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>
+23 -16
View File
@@ -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; }
+1 -1
View File
@@ -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()}`; }
+51
View File
@@ -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; }
+2 -1
View File
@@ -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};
+4
View File
@@ -0,0 +1,4 @@
export interface OpenSaveDialogOption {
filename?: string,
filters?: Electron.FileFilter[]
}