mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
Merge pull request #176 from Zagrios/feature/share-custom-folders/155
feature/share-custom-folders/155
This commit is contained in:
@@ -84,7 +84,8 @@
|
||||
"verify-files": "Verify files",
|
||||
"clone": "Clone",
|
||||
"edit": "Edit",
|
||||
"uninstall": "Uninstall"
|
||||
"uninstall": "Uninstall",
|
||||
"shared-folders": "Shared Folders"
|
||||
}
|
||||
},
|
||||
"available-versions": {
|
||||
@@ -414,6 +415,14 @@
|
||||
"success":"Model installation complete",
|
||||
"error":"An error occurred while installing the model"
|
||||
}
|
||||
},
|
||||
"shared-folder": {
|
||||
"success": {
|
||||
"yeet-mods-disabled": {
|
||||
"title": "YeetMods disabled",
|
||||
"msg": "To prevent mod removal, the value of the 'YeetMods' variable in the 'Beat Saber IPA.json' file has been disabled"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
@@ -563,6 +572,15 @@
|
||||
"li-2": "Mods are subject to being broken by updates and that's normal - be patient and respectful when this happens, as modders are volunteers with real lives.",
|
||||
"li-3": "Beat Games aren't purposefully trying to break mods. They wish to work on the codebase and sometimes this breaks mods, but they are not out to kill mods.",
|
||||
"p-2": "Do not attack the game developers for issues related to mods, and vice versa - modders and game developers are two separate groups. Just don't be a jerk ok."
|
||||
},
|
||||
"shared-folders":{
|
||||
"title": "Shared Folders",
|
||||
"description": "Link BeatSaber folders to synchronize their contents with shared folders in other versions. Note that deleting content will also be shared.",
|
||||
"buttons": {
|
||||
"add-folder": "Add Folder",
|
||||
"link-folder": "Link Folder",
|
||||
"unlink-folder": "Unlink Folder"
|
||||
}
|
||||
}
|
||||
},
|
||||
"maps": {
|
||||
|
||||
@@ -83,7 +83,8 @@
|
||||
"verify-files": "Verificar los archivos",
|
||||
"clone": "Clonar",
|
||||
"edit": "Editar",
|
||||
"uninstall": "Desinstalar"
|
||||
"uninstall": "Desinstalar",
|
||||
"shared-folders": "Carpetas Compartidas"
|
||||
}
|
||||
},
|
||||
"available-versions": {
|
||||
@@ -413,6 +414,14 @@
|
||||
"success":"Instalación del modelo completada",
|
||||
"error":"Se produjo un error durante la instalación del modelo"
|
||||
}
|
||||
},
|
||||
"shared-folder": {
|
||||
"success": {
|
||||
"yeet-mods-disabled": {
|
||||
"title": "YeetMods desactivado",
|
||||
"msg": "Para evitar la eliminación de mods, se ha desactivado el valor de la variable 'YeetMods' en el archivo 'Beat Saber IPA.json'"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
@@ -562,6 +571,15 @@
|
||||
"li-2": "Es probable que los mods dejen de funcionar debido a las actualizaciones y eso es normal. Sé paciente y respetuoso cuando suceda esto, ya que los modders son voluntarios y tienen una vida.",
|
||||
"li-3": "Beat Games no está intencionalmente tratando de romper los mods. Quieren mejorar el juego base y a veces esto rompe los mods, pero no es su objetivo matar a los mods.",
|
||||
"p-2": "No ataques a los desarrolladores por problemas relacionados con los mods y viceversa. Los modders y los desarrolladores son dos grupos distintos. No seas tonto, ¿de acuerdo?"
|
||||
},
|
||||
"shared-folders":{
|
||||
"title": "Carpetas Compartidas",
|
||||
"description": "Enlace las carpetas de BeatSaber para sincronizar su contenido con las carpetas compartidas en otras versiones. Tenga en cuenta que la eliminación de contenido también se compartirá.",
|
||||
"buttons": {
|
||||
"add-folder": "Añadir Carpeta",
|
||||
"link-folder": "Enlazar Carpeta",
|
||||
"unlink-folder": "Desenlazar Carpeta"
|
||||
}
|
||||
}
|
||||
},
|
||||
"maps": {
|
||||
|
||||
@@ -83,7 +83,8 @@
|
||||
"verify-files": "Vérifier les fichiers",
|
||||
"clone": "Cloner",
|
||||
"edit": "Editer",
|
||||
"uninstall": "Désinstaller"
|
||||
"uninstall": "Désinstaller",
|
||||
"shared-folders": "Dossiers Partagés"
|
||||
}
|
||||
},
|
||||
"available-versions": {
|
||||
@@ -413,6 +414,14 @@
|
||||
"success":"Installation du modèle terminée",
|
||||
"error":"Une erreur s'est produite lors de l'installation du modèle"
|
||||
}
|
||||
},
|
||||
"shared-folder":{
|
||||
"success": {
|
||||
"yeet-mods-disabled":{
|
||||
"title": "YeetMods désactivé",
|
||||
"msg": "Pour éviter la suppression des mods, la valeur de la variable 'YeetMods' dans le fichier 'Beat Saber IPA.json' a été désactivée"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
@@ -562,6 +571,15 @@
|
||||
"li-2": "Les mods sont susceptibles de ne plus fonctionner à cause des mises à jour et c'est normal - soyez patient et respectueux lorsque cela se produit, parce que les moddeurs sont bénévoles et ont une vie.",
|
||||
"li-3": "Beat Games ne cherche pas volontairement à casser les mods. Ils souhaitent améliorer la base du jeu et parfois cela casse les mods, mais ce n'est pas dans leur objectif de tuer les mods.",
|
||||
"p-2": "N'attaquez pas les développeurs pour des problèmes relatifs aux mods, et inversement - les moddeurs et les développeurs sont deux groupes distincts. Ne soyez pas idiots, d'accord ?"
|
||||
},
|
||||
"shared-folders":{
|
||||
"title": "Dossiers Partagés",
|
||||
"description": "Liez les dossiers de BeatSaber pour synchroniser leurs contenus avec les dossiers partagés des autres versions. Notez que la suppression de contenu sera également partagée.",
|
||||
"buttons": {
|
||||
"add-folder": "Ajouter un dossier",
|
||||
"link-folder": "Lier le dossier",
|
||||
"unlink-folder": "Délier le dossier"
|
||||
}
|
||||
}
|
||||
},
|
||||
"maps": {
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { move } from "fs-extra";
|
||||
import { access, mkdir, rm, readdir, unlink, lstat, readlink } from "fs/promises";
|
||||
import path from "path";
|
||||
import { Observable } from "rxjs";
|
||||
import log from "electron-log"
|
||||
|
||||
export async function pathExist(path: string): Promise<boolean> {
|
||||
try{
|
||||
await access(path);
|
||||
return true;
|
||||
}catch(e){
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export async function ensureFolderExist(path: string): Promise<void> {
|
||||
if(await pathExist(path)){ return Promise.resolve(); }
|
||||
return mkdir(path, {recursive: true}).catch(log.error).then(() => {});
|
||||
}
|
||||
|
||||
export async function deleteFolder(folderPath: string): Promise<void> {
|
||||
if(!(await pathExist(folderPath))){ return; }
|
||||
return rm(folderPath, {recursive: true, force: true});
|
||||
}
|
||||
|
||||
export async function unlinkPath(path: string): Promise<void>{
|
||||
if(!(await pathExist(path))){ return; }
|
||||
return unlink(path);
|
||||
}
|
||||
|
||||
export async function getFoldersInFolder(folderPath: string): Promise<string[]> {
|
||||
if(!(await pathExist(folderPath))){ return []; }
|
||||
|
||||
const files = await readdir(folderPath, {withFileTypes: true});
|
||||
|
||||
const promises = files.map(async file => {
|
||||
if(file.isDirectory()){ return path.join(folderPath, file.name); }
|
||||
if(!file.isSymbolicLink()){ return undefined; }
|
||||
try{
|
||||
const targetPath = await readlink(path.join(folderPath, file.name));
|
||||
return (await lstat(targetPath)).isDirectory() ? path.join(folderPath, file.name) : undefined;
|
||||
}catch(e){
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
return (await Promise.all(promises)).filter(folder => folder);
|
||||
}
|
||||
|
||||
export function moveFolderContent(src: string, dest: string): Observable<Progression>{
|
||||
const progress: Progression = { current: 0, total: 0 };
|
||||
return new Observable<Progression>(subscriber => {
|
||||
subscriber.next(progress);
|
||||
(async () => {
|
||||
|
||||
const srcExist = await pathExist(src);
|
||||
|
||||
if(!srcExist){ return subscriber.complete(); }
|
||||
|
||||
ensureFolderExist(dest);
|
||||
|
||||
const files = await readdir(src, {encoding: "utf-8"});
|
||||
progress.total = files.length;
|
||||
|
||||
const promises = files.map(async file => {
|
||||
const srcFullPath = path.join(src, file);
|
||||
const destFullPath = path.join(dest, file);
|
||||
if(await pathExist(destFullPath)){
|
||||
progress.current++;
|
||||
return subscriber.next(progress);
|
||||
}
|
||||
await move(srcFullPath, destFullPath);
|
||||
progress.current++;
|
||||
subscriber.next(progress);
|
||||
});
|
||||
|
||||
Promise.allSettled(promises).then(() => subscriber.complete());
|
||||
|
||||
})();
|
||||
});
|
||||
}
|
||||
|
||||
export interface Progression<T = unknown>{
|
||||
total: number;
|
||||
current: number;
|
||||
extra?: T;
|
||||
}
|
||||
|
||||
@@ -116,4 +116,9 @@ ipc.on("is-map-deep-links-enabled", async (request: IpcRequest<void>) => {
|
||||
utils.ipcSend(request.responceChannel, {success: false});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
ipc.on("get-version-maps-path", async (req: IpcRequest<BSVersion>, reply) => {
|
||||
const maps = LocalMapsManagerService.getInstance();
|
||||
reply(from(maps.getMapsFolderPath(req.args)));
|
||||
});
|
||||
@@ -6,6 +6,16 @@ import { exec } from 'child_process';
|
||||
import { IpcRequest } from 'shared/models/ipc';
|
||||
import { BSLocalVersionService } from '../services/bs-local-version.service';
|
||||
import { BsmException } from 'shared/models/bsm-exception.model';
|
||||
import { IpcService } from '../services/ipc.service';
|
||||
import { from } from 'rxjs';
|
||||
import path from 'path';
|
||||
import { pathExist } from '../helpers/fs.helpers';
|
||||
import { FolderLinkerService } from '../services/folder-linker.service';
|
||||
import { LocalMapsManagerService } from '../services/additional-content/local-maps-manager.service';
|
||||
import { readJSON, writeJSON } from 'fs-extra';
|
||||
import log from "electron-log"
|
||||
|
||||
const ipc = IpcService.getInstance();
|
||||
|
||||
ipcMain.on('bs-version.get-version-dict', (event, req: IpcRequest<void>) => {
|
||||
BSVersionLibService.getInstance().getAvailableVersions().then(versions => {
|
||||
@@ -26,7 +36,7 @@ ipcMain.on('bs-version.installed-versions', async (event, req: IpcRequest<void>)
|
||||
ipcMain.on("bs-version.open-folder", async (event, req: IpcRequest<BSVersion>) => {
|
||||
const localVersionService = BSLocalVersionService.getInstance();
|
||||
const versionFolder = await localVersionService.getVersionPath(req.args);
|
||||
UtilsService.getInstance().pathExist(versionFolder) && exec(`start "" "${versionFolder}"`);
|
||||
(await pathExist(versionFolder)) && exec(`start "" "${versionFolder}"`);
|
||||
});
|
||||
|
||||
ipcMain.on("bs-version.edit", async (event, req: IpcRequest<{version: BSVersion, name: string, color: string}>) => {
|
||||
@@ -44,3 +54,77 @@ ipcMain.on("bs-version.clone", async (event, req: IpcRequest<{version: BSVersion
|
||||
UtilsService.getInstance().ipcSend(req.responceChannel, {success: false, error});
|
||||
});
|
||||
});
|
||||
|
||||
ipc.on("get-version-full-path", async (req: IpcRequest<BSVersion>, reply) => {
|
||||
const localVersions = BSLocalVersionService.getInstance();
|
||||
reply(from(
|
||||
localVersions.getVersionPath(req.args)
|
||||
));
|
||||
});
|
||||
|
||||
ipc.on("relative-version-path-to-full", async (req: IpcRequest<{version: BSVersion, relative: string}>, reply) => {
|
||||
path.isAbsolute(req.args.relative) && reply(from(Promise.resolve(req.args.relative)));
|
||||
|
||||
const localVersions = BSLocalVersionService.getInstance();
|
||||
const promise = localVersions.getVersionPath(req.args.version).catch(() => null).then(versionPath => {
|
||||
return path.join(versionPath, req.args.relative);
|
||||
});
|
||||
reply(from(promise));
|
||||
});
|
||||
|
||||
ipc.on("full-version-path-to-relative", async (req: IpcRequest<{version: BSVersion, fullPath: string}>, reply) => {
|
||||
const localVersions = BSLocalVersionService.getInstance();
|
||||
const promise = localVersions.getVersionPath(req.args.version).catch(() => null).then(versionPath => {
|
||||
return path.relative(versionPath, req.args.fullPath);
|
||||
});
|
||||
reply(from(promise));
|
||||
});
|
||||
|
||||
ipc.on("get-linked-folders", async (req: IpcRequest<BSVersion>, reply) => {
|
||||
const localVersions = BSLocalVersionService.getInstance();
|
||||
reply(from(localVersions.getLinkedFolders(req.args)));
|
||||
});
|
||||
|
||||
ipc.on("link-folder", async (req: IpcRequest<{ folder: string, keepContents?: boolean}>, reply) => {
|
||||
const linker = FolderLinkerService.getInstance();
|
||||
|
||||
const relativeMapsFolder = path.join(LocalMapsManagerService.LEVELS_ROOT_FOLDER, LocalMapsManagerService.CUSTOM_LEVELS_FOLDER)
|
||||
|
||||
if(req.args.folder.includes(relativeMapsFolder)){
|
||||
return reply(from(linker.linkFolder(req.args.folder, {keepContents: req.args.keepContents, intermediateFolder: LocalMapsManagerService.SHARED_MAPS_FOLDER})));
|
||||
}
|
||||
|
||||
const res = from(linker.linkFolder(req.args.folder, {keepContents: true}));
|
||||
|
||||
const jsonIPAPath = path.join(req.args.folder, "Beat Saber IPA.json");
|
||||
|
||||
if(!(await pathExist(jsonIPAPath))){ return reply(res); }
|
||||
|
||||
await res.toPromise();
|
||||
|
||||
try{
|
||||
const ipaData = (await readJSON(jsonIPAPath)) ?? {} as any;
|
||||
ipaData["YeetMods"] = false;
|
||||
await writeJSON(jsonIPAPath, ipaData, {spaces: 4});
|
||||
}catch(e){
|
||||
log.error("Disable YeetMods", e);
|
||||
}
|
||||
|
||||
reply(res);
|
||||
|
||||
});
|
||||
|
||||
ipc.on("unlink-folder", async (req: IpcRequest<{folder: string, keepContents?: boolean}>, reply) => {
|
||||
const linker = FolderLinkerService.getInstance();
|
||||
|
||||
const relativeMapsFolder = path.join(LocalMapsManagerService.LEVELS_ROOT_FOLDER, LocalMapsManagerService.CUSTOM_LEVELS_FOLDER)
|
||||
|
||||
if(req.args.folder.includes(relativeMapsFolder)){
|
||||
return reply(from(linker.unlinkFolder(req.args.folder, {keepContents: req.args.keepContents, intermediateFolder: LocalMapsManagerService.SHARED_MAPS_FOLDER})));
|
||||
}
|
||||
|
||||
reply(from(linker.unlinkFolder(req.args.folder, {keepContents: req.args.keepContents})));
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -4,10 +4,16 @@ import { IpcRequest } from 'shared/models/ipc';
|
||||
import { SystemNotificationOptions } from 'shared/models/notification/system-notification.model';
|
||||
import { NotificationService } from '../services/notification.service';
|
||||
import { SteamService } from '../services/steam.service';
|
||||
import { IpcService } from '../services/ipc.service';
|
||||
import { from } from 'rxjs';
|
||||
import { fstat, lstat } from 'fs';
|
||||
import { pathExist } from '../helpers/fs.helpers';
|
||||
|
||||
|
||||
// TODO IMPROVE WINDOW CONTROL BY USING WINDOW SERVICE
|
||||
|
||||
const ipc = IpcService.getInstance();
|
||||
|
||||
ipcMain.on('window.close', async () => {
|
||||
const utils = UtilsService.getInstance();
|
||||
utils.getMainWindows("index.html")?.close();
|
||||
@@ -32,10 +38,10 @@ ipcMain.on('new-window', async (event, request: IpcRequest<string>) => {
|
||||
shell.openExternal(request.args);
|
||||
});
|
||||
|
||||
ipcMain.on('choose-folder', async (event, request: IpcRequest<void>) => {
|
||||
dialog.showOpenDialog({properties: ['openDirectory'],}).then(res => {
|
||||
UtilsService.getInstance().ipcSend(request.responceChannel, {success: true, data: res});
|
||||
});
|
||||
ipc.on('choose-folder', async (req: IpcRequest<string>, reply) => {
|
||||
reply(
|
||||
from(dialog.showOpenDialog({properties: ['openDirectory'], defaultPath: req.args ?? ""}))
|
||||
)
|
||||
});
|
||||
|
||||
ipcMain.on("window.progression", async (event, request: IpcRequest<number>) => {
|
||||
@@ -71,4 +77,15 @@ ipcMain.on("open-steam", async (event, request: IpcRequest<void>) => {
|
||||
}).catch((e) => {
|
||||
utils.ipcSend(request.responceChannel, {success: false, error: e});
|
||||
});
|
||||
});
|
||||
|
||||
ipc.on("is-folder-symlink", async (req: IpcRequest<string>, reply) => {
|
||||
const promise = new Promise<boolean>(async resolve => {
|
||||
if(!(await pathExist(req.args))){ return resolve(false); }
|
||||
lstat(req.args, (err, stats) => {
|
||||
if(err){ return resolve(false); }
|
||||
resolve(stats.isSymbolicLink());
|
||||
});
|
||||
});
|
||||
reply(from(promise));
|
||||
});
|
||||
@@ -6,8 +6,8 @@ import { BSLocalVersionService } from "../bs-local-version.service";
|
||||
import { InstallationLocationService } from "../installation-location.service";
|
||||
import { UtilsService } from "../utils.service";
|
||||
import crypto from "crypto";
|
||||
import { lstatSync, symlinkSync, unlinkSync } from "fs";
|
||||
import { copy, copySync } from "fs-extra";
|
||||
import { lstatSync, unlinkSync } from "fs";
|
||||
import { copySync } from "fs-extra";
|
||||
import StreamZip from "node-stream-zip";
|
||||
import { RequestService } from "../request.service";
|
||||
import sanitize from "sanitize-filename";
|
||||
@@ -18,6 +18,9 @@ import { ipcMain } from "electron";
|
||||
import { IpcRequest } from 'shared/models/ipc';
|
||||
import { Observable } from "rxjs";
|
||||
import { Archive } from "../../models/archive.class";
|
||||
import { deleteFolder, ensureFolderExist, getFoldersInFolder, pathExist } from "../../helpers/fs.helpers";
|
||||
import { readFile } from "fs/promises";
|
||||
import { FolderLinkerService } from "../folder-linker.service";
|
||||
|
||||
export class LocalMapsManagerService {
|
||||
|
||||
@@ -28,8 +31,9 @@ export class LocalMapsManagerService {
|
||||
return LocalMapsManagerService.instance;
|
||||
}
|
||||
|
||||
private readonly LEVELS_ROOT_FOLDER = "Beat Saber_Data";
|
||||
private readonly CUSTOM_LEVELS_FOLDER = "CustomLevels";
|
||||
public static readonly LEVELS_ROOT_FOLDER = "Beat Saber_Data";
|
||||
public static readonly CUSTOM_LEVELS_FOLDER = "CustomLevels";
|
||||
public static readonly SHARED_MAPS_FOLDER = "SharedMaps";
|
||||
|
||||
private readonly DEEP_LINKS = {
|
||||
BeatSaver: "beatsaver",
|
||||
@@ -42,6 +46,7 @@ export class LocalMapsManagerService {
|
||||
private readonly reqService: RequestService;
|
||||
private readonly deepLink: DeepLinkService;
|
||||
private readonly windows: WindowManagerService;
|
||||
private readonly linker = FolderLinkerService.getInstance();
|
||||
|
||||
private constructor(){
|
||||
this.localVersion = BSLocalVersionService.getInstance();
|
||||
@@ -50,6 +55,7 @@ export class LocalMapsManagerService {
|
||||
this.reqService = RequestService.getInstance();
|
||||
this.deepLink = DeepLinkService.getInstance();
|
||||
this.windows = WindowManagerService.getInstance();
|
||||
this.linker = FolderLinkerService.getInstance();
|
||||
|
||||
this.deepLink.addLinkOpenedListener(this.DEEP_LINKS.BeatSaver, link => {
|
||||
log.info("DEEP-LINK RECEIVED FROM", this.DEEP_LINKS.BeatSaver, link);
|
||||
@@ -64,10 +70,10 @@ export class LocalMapsManagerService {
|
||||
|
||||
public async getMapsFolderPath(version?: BSVersion): Promise<string>{
|
||||
|
||||
if(version){ return path.join(await this.localVersion.getVersionPath(version), this.LEVELS_ROOT_FOLDER, this.CUSTOM_LEVELS_FOLDER); }
|
||||
const sharedMapsPath = path.join(this.installLocation.sharedMapsPath, this.CUSTOM_LEVELS_FOLDER);
|
||||
if(!(await this.utils.pathExist(sharedMapsPath))){
|
||||
this.utils.createFolderIfNotExist(sharedMapsPath);
|
||||
if(version){ return path.join(await this.localVersion.getVersionPath(version), LocalMapsManagerService.LEVELS_ROOT_FOLDER, LocalMapsManagerService.CUSTOM_LEVELS_FOLDER); }
|
||||
const sharedMapsPath = path.join(this.installLocation.sharedContentPath, LocalMapsManagerService.SHARED_MAPS_FOLDER, LocalMapsManagerService.CUSTOM_LEVELS_FOLDER);
|
||||
if(!(await pathExist(sharedMapsPath))){
|
||||
await ensureFolderExist(sharedMapsPath);
|
||||
}
|
||||
return sharedMapsPath;
|
||||
}
|
||||
@@ -79,7 +85,7 @@ export class LocalMapsManagerService {
|
||||
for(const set of mapRawInfo._difficultyBeatmapSets){
|
||||
for(const diff of set._difficultyBeatmaps){
|
||||
const diffFilePath = path.join(mapPath, diff._beatmapFilename);
|
||||
const diffContent = await this.utils.readFileAsync(diffFilePath).catch(() => null);
|
||||
const diffContent = await readFile(diffFilePath, {encoding: "utf-8"}).catch(() => null);
|
||||
diffContent && shasum.update(diffContent);
|
||||
}
|
||||
}
|
||||
@@ -90,9 +96,9 @@ export class LocalMapsManagerService {
|
||||
private async loadMapInfoFromPath(mapPath: string): Promise<BsmLocalMap>{
|
||||
const infoFilePath = path.join(mapPath, "Info.dat");
|
||||
|
||||
if(!(await this.utils.pathExist(infoFilePath))){ return null; }
|
||||
if(!(await pathExist(infoFilePath))){ return null; }
|
||||
|
||||
const rawInfoString = await this.utils.readFileAsync(infoFilePath);
|
||||
const rawInfoString = await readFile(infoFilePath, {encoding: "utf-8"});
|
||||
|
||||
const rawInfo: RawMapInfoData = JSON.parse(rawInfoString);
|
||||
const coverUrl = new URL(`file:///${path.join(mapPath, rawInfo._coverImageFilename)}`).href;
|
||||
@@ -106,7 +112,7 @@ export class LocalMapsManagerService {
|
||||
private async downloadMapZip(zipUrl: string): Promise<{zip: StreamZip.StreamZipAsync, zipPath: string}>{
|
||||
const fileName = path.basename(zipUrl);
|
||||
const tempPath = this.utils.getTempPath();
|
||||
this.utils.createFolderIfNotExist(this.utils.getTempPath());
|
||||
await ensureFolderExist(this.utils.getTempPath());
|
||||
const dest = path.join(tempPath, fileName);
|
||||
|
||||
const zipPath = await this.reqService.downloadFile(zipUrl, dest);
|
||||
@@ -137,7 +143,7 @@ export class LocalMapsManagerService {
|
||||
(async () => {
|
||||
const levelsFolder = await this.getMapsFolderPath(version);
|
||||
|
||||
const levelsPaths = (await this.utils.pathExist(levelsFolder)) ? this.utils.listDirsInDir(levelsFolder, true) : [];
|
||||
const levelsPaths = (await pathExist(levelsFolder)) ? await getFoldersInFolder(levelsFolder) : [];
|
||||
|
||||
progression.total = levelsPaths.length;
|
||||
|
||||
@@ -169,7 +175,7 @@ export class LocalMapsManagerService {
|
||||
|
||||
const levelsPath = await this.getMapsFolderPath(version);
|
||||
|
||||
const isPathExist = await this.utils.pathExist(levelsPath);
|
||||
const isPathExist = await pathExist(levelsPath);
|
||||
|
||||
if(!isPathExist){ return false; }
|
||||
|
||||
@@ -177,36 +183,13 @@ export class LocalMapsManagerService {
|
||||
}
|
||||
|
||||
public async linkVersionMaps(version: BSVersion, keepMaps: boolean): Promise<void>{
|
||||
|
||||
if(await this.versionIsLinked(version)){ return; }
|
||||
|
||||
const sharedMapsPath = await this.getMapsFolderPath();
|
||||
const versionMapsPath = await this.getMapsFolderPath(version);
|
||||
|
||||
if(keepMaps){
|
||||
await this.utils.moveDirContent(versionMapsPath, sharedMapsPath);
|
||||
}
|
||||
|
||||
await this.utils.deleteFolder(versionMapsPath);
|
||||
|
||||
symlinkSync(sharedMapsPath, versionMapsPath, "junction");
|
||||
return this.linker.linkFolder(versionMapsPath, {keepContents: keepMaps, intermediateFolder: LocalMapsManagerService.SHARED_MAPS_FOLDER});
|
||||
}
|
||||
|
||||
public async unlinkVersionMaps(version: BSVersion, keepMaps: boolean): Promise<void>{
|
||||
|
||||
const sharedMapsPath = await this.getMapsFolderPath();
|
||||
const versionMapsPath = await this.getMapsFolderPath(version);
|
||||
|
||||
if(await this.versionIsLinked(version)){
|
||||
unlinkSync(versionMapsPath);
|
||||
}
|
||||
|
||||
this.utils.createFolderIfNotExist(versionMapsPath);
|
||||
|
||||
if(keepMaps){
|
||||
await copy(sharedMapsPath, versionMapsPath);
|
||||
}
|
||||
|
||||
return this.linker.unlinkFolder(versionMapsPath, {keepContents: keepMaps, intermediateFolder: LocalMapsManagerService.SHARED_MAPS_FOLDER});
|
||||
}
|
||||
|
||||
public deleteMaps(maps: BsmLocalMap[]): Observable<DeleteMapsProgress>{
|
||||
@@ -221,7 +204,7 @@ export class LocalMapsManagerService {
|
||||
for(const folder of mapsFolders){
|
||||
const detail = await this.loadMapInfoFromPath(folder);
|
||||
if(!mapsHashsToDelete.includes(detail?.hash)){ continue; }
|
||||
await this.utils.deleteFolder(folder);
|
||||
await deleteFolder(folder);
|
||||
progress.deleted++;
|
||||
observer.next(progress);
|
||||
}
|
||||
@@ -249,7 +232,7 @@ export class LocalMapsManagerService {
|
||||
|
||||
if(!zip){ throw `Cannot download ${zipUrl}`; }
|
||||
|
||||
this.utils.createFolderIfNotExist(mapPath);
|
||||
await ensureFolderExist(mapPath);
|
||||
|
||||
await zip.extract(null, mapPath);
|
||||
await zip.close();
|
||||
@@ -291,7 +274,7 @@ export class LocalMapsManagerService {
|
||||
|
||||
const versionMapsPath = await this.getMapsFolderPath(version);
|
||||
|
||||
this.utils.createFolderIfNotExist(versionMapsPath);
|
||||
await ensureFolderExist(versionMapsPath);
|
||||
|
||||
copySync(downloadedMap.path, path.join(versionMapsPath, path.basename(downloadedMap.path)), {overwrite: true});
|
||||
|
||||
@@ -311,6 +294,4 @@ export class LocalMapsManagerService {
|
||||
return Array.from(Object.values(this.DEEP_LINKS)).every(link => this.deepLink.isDeepLinkRegistred(link));
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import path from "path";
|
||||
import { RequestService } from "../request.service";
|
||||
import { copyFileSync } from "fs-extra";
|
||||
import sanitize from "sanitize-filename";
|
||||
import { ensureFolderExist } from "../../helpers/fs.helpers";
|
||||
|
||||
export class LocalModelsManagerService {
|
||||
|
||||
@@ -74,7 +75,7 @@ export class LocalModelsManagerService {
|
||||
const versionPath = await this.localVersion.getVersionPath(version);
|
||||
const modelFolderPath = path.join(versionPath, this.MODEL_TYPE_FOLDER[type]);
|
||||
|
||||
this.utils.createFolderIfNotExist(modelFolderPath);
|
||||
await ensureFolderExist(modelFolderPath);
|
||||
|
||||
return modelFolderPath;
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import { BPList, DownloadPlaylistProgression } from "shared/models/playlists/pla
|
||||
import { copyFileSync, readFileSync } from "fs";
|
||||
import { BeatSaverService } from "../thrid-party/beat-saver/beat-saver.service";
|
||||
import { copySync } from "fs-extra";
|
||||
import { ensureFolderExist, pathExist } from "../../helpers/fs.helpers";
|
||||
|
||||
export class LocalPlaylistsManagerService {
|
||||
|
||||
@@ -80,7 +81,7 @@ export class LocalPlaylistsManagerService {
|
||||
|
||||
const folder = path.join(versionFolder, this.PLAYLISTS_FOLDER);
|
||||
|
||||
await this.utils.createFolderIfNotExist(folder)
|
||||
await ensureFolderExist(folder)
|
||||
|
||||
return folder;
|
||||
|
||||
@@ -92,7 +93,7 @@ export class LocalPlaylistsManagerService {
|
||||
|
||||
const bpListDest = path.join(playlistFolder, path.basename(bpListUrlOrPath));
|
||||
|
||||
if(this.utils.pathExist(bpListUrlOrPath)){
|
||||
if(await pathExist(bpListUrlOrPath)){
|
||||
copyFileSync(bpListUrlOrPath, bpListDest);
|
||||
}
|
||||
else{
|
||||
@@ -105,7 +106,7 @@ export class LocalPlaylistsManagerService {
|
||||
|
||||
private async readPlaylistFile(path: string): Promise<BPList>{
|
||||
|
||||
if(!this.utils.pathExist(path)){ throw `bplist file not exist at ${path}`; }
|
||||
if(!(await pathExist(path))){ throw `bplist file not exist at ${path}`; }
|
||||
|
||||
const rawContent = readFileSync(path).toString();
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import isOnline from 'is-online';
|
||||
import { WindowManagerService } from "./window-manager.service";
|
||||
import { copy, copySync } from "fs-extra";
|
||||
import { clean, satisfies } from "semver";
|
||||
import { ensureFolderExist, pathExist } from "../helpers/fs.helpers";
|
||||
|
||||
export class BSInstallerService{
|
||||
|
||||
@@ -94,11 +95,11 @@ export class BSInstallerService{
|
||||
if(!bsVersion){ return {type: "[Error]"}; }
|
||||
if(!(await isOnline({timeout: 1500}))){ throw "no-internet"; }
|
||||
|
||||
this.utils.createFolderIfNotExist(this.installLocationService.versionsDirectory);
|
||||
await ensureFolderExist(this.installLocationService.versionsDirectory);
|
||||
|
||||
const versionPath = await this.localVersionService.getVersionPath(bsVersion)
|
||||
|
||||
const dest = !downloadInfos.isVerification ? this.getPathNotAleardyExist(versionPath) : versionPath;
|
||||
const dest = !downloadInfos.isVerification ? await this.getPathNotAleardyExist(versionPath) : versionPath;
|
||||
|
||||
const downloadVersion: BSVersion = {...downloadInfos.bsVersion, ...(path.basename(dest) !== downloadInfos.bsVersion.BSVersion && {name: path.basename(dest)})}
|
||||
|
||||
@@ -186,15 +187,15 @@ export class BSInstallerService{
|
||||
})
|
||||
}
|
||||
|
||||
private getPathNotAleardyExist(path: string): string{
|
||||
private async getPathNotAleardyExist(path: string): Promise<string>{
|
||||
let destPath = path;
|
||||
let folderExist = this.utils.pathExist(destPath);
|
||||
let folderExist = await pathExist(destPath);
|
||||
let i = 0;
|
||||
|
||||
while(folderExist){
|
||||
i++;
|
||||
destPath = `${path} (${i})`;
|
||||
folderExist = this.utils.pathExist(destPath);
|
||||
folderExist = await pathExist(destPath);
|
||||
}
|
||||
|
||||
return destPath
|
||||
@@ -206,7 +207,7 @@ export class BSInstallerService{
|
||||
|
||||
if(!rawBsVersion){ throw new Error("NOT_BS_FOLDER"); }
|
||||
|
||||
const destPath = this.getPathNotAleardyExist(await this.localVersionService.getVersionPath(rawBsVersion));
|
||||
const destPath = await this.getPathNotAleardyExist(await this.localVersionService.getVersionPath(rawBsVersion));
|
||||
|
||||
await copy(path, destPath, {dereference: true});
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ChildProcessWithoutNullStreams, spawn } from "child_process";
|
||||
import { SteamService } from "./steam.service";
|
||||
import { BSLocalVersionService } from "./bs-local-version.service";
|
||||
import { OculusService } from "./oculus.service";
|
||||
import { pathExist } from "../helpers/fs.helpers";
|
||||
|
||||
export class BSLauncherService{
|
||||
|
||||
@@ -42,10 +43,11 @@ export class BSLauncherService{
|
||||
const cwd = await this.localVersionService.getVersionPath(launchOptions.version);
|
||||
const exePath = path.join(cwd, BS_EXECUTABLE);
|
||||
|
||||
if(!this.utilsService.pathExist(exePath)){ return "EXE_NOT_FINDED"; }
|
||||
if(!(await pathExist(exePath))){ return "EXE_NOT_FINDED"; }
|
||||
|
||||
const launchMods = [];
|
||||
|
||||
if(!launchOptions.version.steam && !launchOptions.version.oculus){ launchMods.push("--no-yeet"); }
|
||||
if(launchOptions.oculus){ launchMods.push("-vrmode oculus"); }
|
||||
if(launchOptions.desktop){ launchMods.push("fpfc"); }
|
||||
if(launchOptions.debug){ launchMods.push("--verbose"); }
|
||||
|
||||
@@ -2,7 +2,6 @@ import { BSVersionLibService } from "./bs-version-lib.service";
|
||||
import { BSVersion, PartialBSVersion } from 'shared/bs-version.interface';
|
||||
import { InstallationLocationService } from "./installation-location.service";
|
||||
import { SteamService } from "./steam.service";
|
||||
import { UtilsService } from "./utils.service";
|
||||
import { BS_APP_ID, OCULUS_BS_DIR } from "../constants";
|
||||
import path from "path";
|
||||
import { createReadStream } from "fs";
|
||||
@@ -14,6 +13,8 @@ import log from "electron-log";
|
||||
import { OculusService } from "./oculus.service";
|
||||
import { DownloadLinkType } from "shared/models/mods";
|
||||
import sanitize from "sanitize-filename";
|
||||
import { deleteFolder, getFoldersInFolder, pathExist } from "../helpers/fs.helpers";
|
||||
import { FolderLinkerService } from "./folder-linker.service";
|
||||
|
||||
export class BSLocalVersionService{
|
||||
|
||||
@@ -22,11 +23,11 @@ export class BSLocalVersionService{
|
||||
private readonly CUSTOM_VERSIONS_KEY = "custom-versions";
|
||||
|
||||
private readonly installLocationService: InstallationLocationService;
|
||||
private readonly utilsService: UtilsService;
|
||||
private readonly steamService: SteamService;
|
||||
private readonly oculusService: OculusService;
|
||||
private readonly remoteVersionService: BSVersionLibService;
|
||||
private readonly configService: ConfigurationService;
|
||||
private readonly linker: FolderLinkerService;
|
||||
|
||||
public static getInstance(): BSLocalVersionService{
|
||||
if(!BSLocalVersionService.instance){ BSLocalVersionService.instance = new BSLocalVersionService(); }
|
||||
@@ -35,18 +36,18 @@ export class BSLocalVersionService{
|
||||
|
||||
private constructor(){
|
||||
this.installLocationService = InstallationLocationService.getInstance();
|
||||
this.utilsService = UtilsService.getInstance();
|
||||
this.steamService = SteamService.getInstance();
|
||||
this.oculusService = OculusService.getInstance();
|
||||
this.remoteVersionService = BSVersionLibService.getInstance();
|
||||
this.configService = ConfigurationService.getInstance();
|
||||
this.linker = FolderLinkerService.getInstance();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public async getVersionOfBSFolder(bsPath: string): Promise<PartialBSVersion>{
|
||||
const versionFilePath = path.join(bsPath, 'Beat Saber_Data', 'globalgamemanagers');
|
||||
if(!this.utilsService.pathExist(versionFilePath)){ return null; }
|
||||
if(!(await pathExist(versionFilePath))){ return null; }
|
||||
const versionsAvailable = await this.remoteVersionService.getAvailableVersions();
|
||||
return new Promise<PartialBSVersion>(resolve => {
|
||||
const stream = createReadStream(versionFilePath);
|
||||
@@ -118,8 +119,7 @@ export class BSLocalVersionService{
|
||||
|
||||
private async getSteamVersion(): Promise<BSVersion>{
|
||||
const steamBsFolder = await this.steamService.getGameFolder(BS_APP_ID, "Beat Saber");
|
||||
|
||||
if(!steamBsFolder || !this.utilsService.pathExist(steamBsFolder)){ return null; }
|
||||
if(!steamBsFolder || !(await pathExist(steamBsFolder))){ return null; }
|
||||
const steamBsVersion = await this.getVersionOfBSFolder(steamBsFolder);
|
||||
|
||||
if(!steamBsVersion){ return null; }
|
||||
@@ -146,11 +146,11 @@ export class BSLocalVersionService{
|
||||
if(steamVersion){ versions.push(steamVersion); }
|
||||
|
||||
const oculusVersion = await this.getOculusVersion();
|
||||
if(oculusVersion){ versions.push(oculusVersion); }
|
||||
if(oculusVersion){ versions.push(oculusVersion); }
|
||||
|
||||
if(!this.utilsService.pathExist(this.installLocationService.versionsDirectory)){ return versions }
|
||||
if(!(await pathExist(this.installLocationService.versionsDirectory))){ return versions }
|
||||
|
||||
const folderInInstallation = this.utilsService.listDirsInDir(this.installLocationService.versionsDirectory, true);
|
||||
const folderInInstallation = await getFoldersInFolder(this.installLocationService.versionsDirectory);
|
||||
|
||||
for(const f of folderInInstallation){
|
||||
log.info("try get version from folder", f);
|
||||
@@ -176,9 +176,9 @@ export class BSLocalVersionService{
|
||||
public async deleteVersion(version: BSVersion): Promise<boolean>{
|
||||
if(version.steam || version.oculus){ return false; }
|
||||
const versionFolder = await this.getVersionPath(version);
|
||||
if(!this.utilsService.pathExist(versionFolder)){ return true; }
|
||||
if(!(await pathExist(versionFolder))){ return true; }
|
||||
|
||||
return this.utilsService.deleteFolder(versionFolder)
|
||||
return deleteFolder(versionFolder)
|
||||
.then(() => { return true; })
|
||||
.catch(() => { return false; })
|
||||
}
|
||||
@@ -197,7 +197,7 @@ export class BSLocalVersionService{
|
||||
return editedVersion;
|
||||
}
|
||||
|
||||
if(this.utilsService.pathExist(newPath) && newPath === oldPath){ throw {title: "VersionAlreadExist"} as BsmException; }
|
||||
if((await pathExist(newPath)) && newPath === oldPath){ throw {title: "VersionAlreadExist"} as BsmException; }
|
||||
|
||||
return rename(oldPath, newPath).then(() => {
|
||||
this.deleteCustomVersion(version);
|
||||
@@ -221,7 +221,7 @@ export class BSLocalVersionService{
|
||||
this.addCustomVersion(cloneVersion);
|
||||
}
|
||||
|
||||
if(this.utilsService.pathExist(newPath)){ throw {title: "VersionAlreadExist"} as BsmException; }
|
||||
if(await pathExist(newPath)){ throw {title: "VersionAlreadExist"} as BsmException; }
|
||||
|
||||
return fs.copy(originPath, newPath, {dereference: true}).then(() => {
|
||||
this.addCustomVersion(cloneVersion);
|
||||
@@ -232,4 +232,18 @@ export class BSLocalVersionService{
|
||||
})
|
||||
}
|
||||
|
||||
public async getLinkedFolders(version: BSVersion): Promise<string[]>{
|
||||
const versionPath = await this.getVersionPath(version);
|
||||
const [rootFolders, beatSaberDataFolders] = await Promise.all([
|
||||
getFoldersInFolder(versionPath),
|
||||
getFoldersInFolder(path.join(versionPath, "Beat Saber_Data"))
|
||||
]);
|
||||
|
||||
const linkedFolder = Promise.all([...rootFolders, ...beatSaberDataFolders].map(async folder => {
|
||||
if(!(await this.linker.isFolderSymlink(folder))){ return null; }
|
||||
return folder;
|
||||
}));
|
||||
|
||||
return (await linkedFolder).filter(folder => folder);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { get } from 'https'
|
||||
import { UtilsService } from './utils.service';
|
||||
import path from 'path';
|
||||
import { writeFileSync } from 'fs';
|
||||
import { BSVersion } from 'shared/bs-version.interface';
|
||||
import { RequestService } from "./request.service"
|
||||
import isOnline from 'is-online';
|
||||
import { readJSON } from 'fs-extra';
|
||||
|
||||
export class BSVersionLibService{
|
||||
|
||||
@@ -35,8 +35,7 @@ export class BSVersionLibService{
|
||||
|
||||
private async getLocalVersions(): Promise<BSVersion[]>{
|
||||
const localVersionsPath = path.join(this.utilsService.getAssestsJsonsPath(), this.VERSIONS_FILE);
|
||||
const rawVersion = await this.utilsService.readFileAsync(localVersionsPath);
|
||||
return JSON.parse(rawVersion);
|
||||
return readJSON(localVersionsPath);
|
||||
}
|
||||
|
||||
private async updateLocalVersions(versions: BSVersion[]): Promise<void>{
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { InstallationLocationService } from "./installation-location.service";
|
||||
import log from "electron-log";
|
||||
import { deleteFolder, ensureFolderExist, moveFolderContent, pathExist, unlinkPath } from "../helpers/fs.helpers";
|
||||
import { lstat, symlink } from "fs/promises";
|
||||
import path from "path";
|
||||
import { copy } from "fs-extra";
|
||||
|
||||
export class FolderLinkerService {
|
||||
|
||||
private static instance: FolderLinkerService;
|
||||
|
||||
public static getInstance(): FolderLinkerService {
|
||||
if (!FolderLinkerService.instance) {
|
||||
FolderLinkerService.instance = new FolderLinkerService();
|
||||
}
|
||||
return FolderLinkerService.instance;
|
||||
}
|
||||
|
||||
private readonly installLocationService = InstallationLocationService.getInstance();
|
||||
|
||||
private readonly sharedFolder: string;
|
||||
|
||||
private constructor(){
|
||||
this.installLocationService = InstallationLocationService.getInstance();
|
||||
|
||||
this.sharedFolder = this.installLocationService.sharedContentPath;
|
||||
}
|
||||
|
||||
private getSharedFolder(folderPath: string, intermediateFolder?: string): string {
|
||||
return path.join(this.sharedFolder, intermediateFolder ?? "", path.basename(folderPath));
|
||||
}
|
||||
|
||||
public async linkFolder(folderPath: string, options?: LinkOptions): Promise<void> {
|
||||
|
||||
if(await this.isFolderSymlink(folderPath)){ return; }
|
||||
|
||||
const sharedPath = this.getSharedFolder(folderPath, options?.intermediateFolder);
|
||||
|
||||
await ensureFolderExist(folderPath);
|
||||
await ensureFolderExist(sharedPath);
|
||||
|
||||
if(options?.keepContents !== false){
|
||||
await moveFolderContent(folderPath, sharedPath).toPromise();
|
||||
}
|
||||
|
||||
await deleteFolder(folderPath);
|
||||
|
||||
return symlink(sharedPath, folderPath, "junction");
|
||||
}
|
||||
|
||||
public async unlinkFolder(folderPath: string, options?: LinkOptions): Promise<void> {
|
||||
|
||||
if(!(await this.isFolderSymlink(folderPath))){ return; }
|
||||
await unlinkPath(folderPath);
|
||||
|
||||
const sharedPath = this.getSharedFolder(folderPath, options?.intermediateFolder);
|
||||
|
||||
await ensureFolderExist(folderPath);
|
||||
|
||||
if(options?.keepContents === false){ return; }
|
||||
|
||||
await ensureFolderExist(sharedPath);
|
||||
|
||||
return copy(sharedPath, folderPath, { errorOnExist: false });
|
||||
}
|
||||
|
||||
public async isFolderSymlink(folder: string): Promise<boolean> {
|
||||
try{
|
||||
if(!(await pathExist(folder))){ return false; }
|
||||
return lstat(folder).then(stat => stat.isSymbolicLink());
|
||||
}
|
||||
catch(e){
|
||||
log.error(e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export interface LinkOptions {
|
||||
keepContents?: boolean,
|
||||
intermediateFolder?: string,
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import path from "path";
|
||||
import { UtilsService } from "./utils.service";
|
||||
import fs from 'fs-extra';
|
||||
import log from "electron-log";
|
||||
import { app } from "electron";
|
||||
import { BsmException } from "shared/models/bsm-exception.model";
|
||||
import ElectronStore from "electron-store";
|
||||
import { ensureFolderExist, pathExist } from "../helpers/fs.helpers";
|
||||
|
||||
export class InstallationLocationService {
|
||||
|
||||
@@ -19,10 +19,8 @@ export class InstallationLocationService {
|
||||
private readonly VERSIONS_FOLDER = "BSInstances";
|
||||
|
||||
private readonly SHARED_CONTENT_FOLDER = "SharedContent";
|
||||
private readonly SHARED_MAPS_FOLDER = "SharedMaps";
|
||||
|
||||
private readonly STORE_INSTALLATION_PATH_KEY = "installation-folder";
|
||||
private readonly utilsService: UtilsService;
|
||||
|
||||
private readonly installPathConfig: ElectronStore;
|
||||
private readonly updateListeners: Set<Listener> = new Set();
|
||||
@@ -31,7 +29,6 @@ export class InstallationLocationService {
|
||||
|
||||
private constructor(){
|
||||
this.installPathConfig = new ElectronStore({watch: true});
|
||||
this.utilsService = UtilsService.getInstance();
|
||||
this.initInstallationLocation();
|
||||
|
||||
this.installPathConfig.onDidChange(this.STORE_INSTALLATION_PATH_KEY, () => {
|
||||
@@ -50,8 +47,8 @@ export class InstallationLocationService {
|
||||
public setInstallationDirectory(newDir: string): Promise<string>{
|
||||
const oldDir = this.installationDirectory;
|
||||
const newDest = path.join(newDir, this.INSTALLATION_FOLDER);
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
if(!this.utilsService.pathExist(oldDir)){ this.utilsService.createFolderIfNotExist(oldDir); }
|
||||
return new Promise<string>(async (resolve, reject) => {
|
||||
if(!(await pathExist(oldDir))){ ensureFolderExist(oldDir); }
|
||||
fs.move(oldDir, newDest, { overwrite: true }).then(() => {
|
||||
this._installationDirectory = newDir;
|
||||
this.installPathConfig.set(this.STORE_INSTALLATION_PATH_KEY, newDir);
|
||||
@@ -72,7 +69,6 @@ export class InstallationLocationService {
|
||||
public get versionsDirectory(): string { return path.join(this.installationDirectory, this.VERSIONS_FOLDER); }
|
||||
|
||||
public get sharedContentPath(): string { return path.join(this.installationDirectory, this.SHARED_CONTENT_FOLDER); }
|
||||
public get sharedMapsPath(): string { return path.join(this.sharedContentPath, this.SHARED_MAPS_FOLDER); }
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ export class IpcService {
|
||||
}
|
||||
|
||||
public send<T>(channel: IpcChannel, window: AppWindow, response?: T|Error): void{
|
||||
this.windows.getWindow(window).webContents.send(channel, response);
|
||||
this.windows.getWindow(window)?.webContents?.send(channel, response);
|
||||
}
|
||||
|
||||
public connectStream(channel: IpcChannel, window: AppWindow, observable: Observable<unknown>): void{
|
||||
|
||||
@@ -11,6 +11,7 @@ import { RequestService } from "../request.service";
|
||||
import { spawn } from "child_process";
|
||||
import { BS_EXECUTABLE } from "../../constants";
|
||||
import log from "electron-log";
|
||||
import { deleteFolder, ensureFolderExist, pathExist, unlinkPath } from "../../helpers/fs.helpers";
|
||||
|
||||
export class BsModsManagerService {
|
||||
|
||||
@@ -60,7 +61,7 @@ export class BsModsManagerService {
|
||||
private async getModsInDir(version: BSVersion, modsDir: ModsInstallFolder): Promise<Mod[]>{
|
||||
const bsPath = await this.bsLocalService.getVersionPath(version);
|
||||
const modsPath = path.join(bsPath, modsDir);
|
||||
if(!this.utilsService.pathExist(modsPath)){ return []; }
|
||||
if(!(await pathExist(modsPath))){ return []; }
|
||||
const files = fs.readdirSync(modsPath);
|
||||
const promises = files.map(f => {
|
||||
return (async() => {
|
||||
@@ -89,7 +90,7 @@ export class BsModsManagerService {
|
||||
private async getBsipaInstalled(version: BSVersion): Promise<Mod>{
|
||||
const bsPath = await this.bsLocalService.getVersionPath(version);
|
||||
const injectorPath = path.join(bsPath, "Beat Saber_Data", "Managed", "IPA.Injector.dll");
|
||||
if(!this.utilsService.pathExist(injectorPath)){ return undefined; }
|
||||
if(!(await pathExist(injectorPath))){ return undefined; }
|
||||
const injectorMd5 = await md5File(injectorPath);
|
||||
return this.getIpaFromHash(injectorMd5);
|
||||
}
|
||||
@@ -98,7 +99,7 @@ export class BsModsManagerService {
|
||||
zipUrl = path.join(this.beatModsApi.BEAT_MODS_URL, zipUrl);
|
||||
const fileName = path.basename(zipUrl);
|
||||
const tempPath = this.utilsService.getTempPath();
|
||||
this.utilsService.createFolderIfNotExist(this.utilsService.getTempPath());
|
||||
await ensureFolderExist(this.utilsService.getTempPath());
|
||||
const dest = path.join(tempPath, fileName);
|
||||
|
||||
const zipPath = await this.requestService.downloadFile(zipUrl, dest);
|
||||
@@ -111,7 +112,7 @@ export class BsModsManagerService {
|
||||
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; }
|
||||
if(!(await pathExist(ipaPath)) || !(await pathExist(bsExePath))){ return false; }
|
||||
|
||||
return new Promise<boolean>(resolve => {
|
||||
const processIPA = spawn(`start /wait /min "" "${ipaPath}" ${args.join(" ")}`, {cwd: versionPath, detached: true, shell: true});
|
||||
@@ -163,7 +164,7 @@ export class BsModsManagerService {
|
||||
const extracted = await zip.extract(null, destDir).then(() => true).catch(err => {log.error(err); return false});
|
||||
|
||||
await zip.close();
|
||||
await this.utilsService.unlinkIfExist(zipPath);
|
||||
await unlinkPath(zipPath);
|
||||
|
||||
const res = isBSIPA ? (extracted && (await this.executeBSIPA(version, ["-n"]))) : extracted;
|
||||
|
||||
@@ -194,8 +195,8 @@ export class BsModsManagerService {
|
||||
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"));
|
||||
const hasIPAExe = await pathExist(path.join(verionPath, "IPA.exe"));
|
||||
const hasIPADir = await pathExist(path.join(verionPath, "IPA"));
|
||||
|
||||
if(!hasIPADir || !hasIPAExe){ return; }
|
||||
|
||||
@@ -203,7 +204,7 @@ export class BsModsManagerService {
|
||||
|
||||
const promises = download.hashMd5.map(files => {
|
||||
const file = files.file.replaceAll("IPA/", "").replaceAll("Data", "Beat Saber_Data");
|
||||
return this.utilsService.unlinkIfExist(path.join(verionPath, file));
|
||||
return unlinkPath(path.join(verionPath, file));
|
||||
})
|
||||
|
||||
await Promise.all(promises);
|
||||
@@ -220,8 +221,10 @@ export class BsModsManagerService {
|
||||
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));
|
||||
return Promise.all([
|
||||
unlinkPath(path.join(versionPath, files.file)),
|
||||
unlinkPath(path.join(versionPath, "IPA", "Pending", files.file))
|
||||
]);
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
@@ -306,9 +309,9 @@ export class BsModsManagerService {
|
||||
|
||||
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));
|
||||
await deleteFolder(path.join(versionPath, ModsInstallFolder.PLUGINS));
|
||||
await deleteFolder(path.join(versionPath, ModsInstallFolder.LIBS));
|
||||
await deleteFolder(path.join(versionPath, ModsInstallFolder.IPA));
|
||||
|
||||
path.resolve
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { UtilsService } from "./utils.service";
|
||||
import regedit from 'regedit'
|
||||
import path from "path";
|
||||
import { pathExist } from "../helpers/fs.helpers";
|
||||
|
||||
export class OculusService {
|
||||
|
||||
@@ -57,7 +58,7 @@ export class OculusService {
|
||||
|
||||
for(const lib of libsFolders){
|
||||
const gameFullPath = path.join(lib, rootLibDir, gameFolder);
|
||||
if(this.utils.pathExist(gameFullPath)){ return gameFullPath; }
|
||||
if(await pathExist(gameFullPath)){ return gameFullPath; }
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -4,6 +4,7 @@ import path from "path";
|
||||
import { parse } from "@node-steam/vdf";
|
||||
import { readFile } from "fs/promises";
|
||||
import { spawn } from "child_process";
|
||||
import { pathExist } from "../helpers/fs.helpers";
|
||||
|
||||
export class SteamService{
|
||||
|
||||
@@ -50,7 +51,7 @@ export class SteamService{
|
||||
|
||||
let libraryFolders: any = path.join(steamPath, 'steamapps', 'libraryfolders.vdf');
|
||||
|
||||
if(!this.utils.pathExist(libraryFolders)){ return null; }
|
||||
if(!(await pathExist(libraryFolders))){ return null; }
|
||||
libraryFolders = parse(await readFile(libraryFolders, {encoding: 'utf-8'}));
|
||||
|
||||
if(!libraryFolders.libraryfolders){ return null; }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { writeFileSync } from "fs";
|
||||
import { readJSON } from "fs-extra";
|
||||
import { get } from "https";
|
||||
import isOnline from "is-online";
|
||||
import path from "path";
|
||||
@@ -43,8 +44,7 @@ export class SupportersService {
|
||||
|
||||
private async getLocalSupporters(): Promise<Supporter[]>{
|
||||
const patreonsPath = path.join(this.utilsService.getAssestsJsonsPath(), this.PATREONS_FILE);
|
||||
const rawPatreons = await this.utilsService.readFileAsync(patreonsPath);
|
||||
return JSON.parse(rawPatreons);
|
||||
return readJSON(patreonsPath);
|
||||
}
|
||||
|
||||
private async loadSupporters(): Promise<Supporter[]>{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { splitIntoChunk } from "../../../helpers/array-tools";
|
||||
import { splitIntoChunk } from "../../../helpers/array.helpers";
|
||||
import { BsvMapDetail } from "shared/models/maps";
|
||||
import { BsvPlaylist, SearchParams } from "shared/models/maps/beat-saver.model";
|
||||
import { BeatSaverApiService } from "./beat-saver-api.service";
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { existsSync, mkdirSync, readdirSync, readFile, rmSync } from "fs";
|
||||
import { moveSync } from "fs-extra"
|
||||
import { spawnSync } from "child_process";
|
||||
import { homedir } from "os";
|
||||
import path from "path";
|
||||
import { app, BrowserWindow } from "electron";
|
||||
import { rm, unlink } from "fs/promises";
|
||||
import { IpcResponse } from "shared/models/ipc";
|
||||
import log from "electron-log";
|
||||
import { AppWindow } from "shared/models/window-manager/app-window.model";
|
||||
@@ -37,72 +33,14 @@ export class UtilsService{
|
||||
public setMainWindows(windows: Map<AppWindow, BrowserWindow>){ this.windows = windows; }
|
||||
public getMainWindows(win: AppWindow){ return this.windows.get(win); }
|
||||
|
||||
public pathExist(path: string): boolean{ return existsSync(path); }
|
||||
|
||||
public createFolderIfNotExist(path: string): void{
|
||||
if(!this.pathExist(path)){ mkdirSync(path, {recursive: true}); }
|
||||
}
|
||||
|
||||
public async unlinkIfExist(pathToFile: string): Promise<void>{
|
||||
if(!this.pathExist(pathToFile)){ return }
|
||||
return unlink(pathToFile);
|
||||
}
|
||||
|
||||
public async 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);
|
||||
}
|
||||
|
||||
public getUserFolder(){ return homedir(); }
|
||||
|
||||
public getUserDocumentsFolder(): string{
|
||||
return path.join(this.getUserFolder(), 'Documents');
|
||||
}
|
||||
|
||||
public readFileAsync(path: string){
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
readFile(path, {encoding: "utf-8", flag: "r"}, (err, data) => {
|
||||
if(err){ reject(err); }
|
||||
else{ resolve(data); }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public listDirsInDir(dirPath: string, fullPath = false): string[]{
|
||||
let files = readdirSync(dirPath, { withFileTypes:true });
|
||||
return files.reduce((acc, f) => {
|
||||
if(!f.isDirectory()){ return acc; }
|
||||
acc.push(fullPath ? path.join(dirPath, f.name) : f.name);
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
public async deleteFolder(folderPath: string): Promise<void>{
|
||||
const folderExist = this.pathExist(folderPath);
|
||||
if(!folderExist){ return; }
|
||||
return rmSync(folderPath, {recursive: true});
|
||||
}
|
||||
|
||||
public async moveDirContent(src: string, dest: string, overwrite = false): Promise<void>{
|
||||
const [srcExist, destExist] = await Promise.all([this.pathExist(src), this.pathExist(dest)]);
|
||||
if(!srcExist){ return; }
|
||||
if(!destExist){ await this.createFolderIfNotExist(dest); }
|
||||
readdirSync(src, {encoding: "utf-8"}).forEach(file => {
|
||||
const srcFullPath = path.join(src, file);
|
||||
const destFullPath = path.join(dest, file);
|
||||
if(!overwrite && this.pathExist(destFullPath)){ return; }
|
||||
moveSync(srcFullPath, destFullPath, {overwrite});
|
||||
});
|
||||
}
|
||||
|
||||
public ipcSend<T = any>(channel: string, response: IpcResponse<T>): void{
|
||||
try {
|
||||
Array.from(this.windows.values()).forEach(window => window.webContents.send(channel, response));
|
||||
Array.from(this.windows.values()).forEach(window => window?.webContents?.send(channel, response));
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { motion } from "framer-motion"
|
||||
import { forwardRef } from "react"
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook"
|
||||
import { BsmIcon } from "../svgs/bsm-icon.component"
|
||||
|
||||
type Props = {
|
||||
className?: string,
|
||||
title?: string,
|
||||
linked?: boolean
|
||||
disabled?: boolean,
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
export const LinkButton = motion(forwardRef((props: Props, ref) => {
|
||||
|
||||
const color = useThemeColor("first-color");
|
||||
|
||||
const linkedColor = (() => {
|
||||
if(props.disabled){ return "orange"; }
|
||||
if(props.linked){ return color; }
|
||||
return "red";
|
||||
})()
|
||||
|
||||
return (
|
||||
<div ref={ref} className={props.className} title={props.title} onClick={e => {e.preventDefault(); !props.disabled && props.onClick?.()}} style={{pointerEvents: props.disabled ? "none" : "auto"}}>
|
||||
<span className="absolute top-0 left-0 h-full w-full rounded-full brightness-50 opacity-75 dark:opacity-20 dark:filter-none" style={{backgroundColor: linkedColor}}/>
|
||||
<BsmIcon className="p-1 absolute top-0 left-0 h-full w-full !bg-transparent -rotate-45 brightness-150" icon={props.linked ? "link" : "unlink"} style={{color: linkedColor}} />
|
||||
</div>
|
||||
)
|
||||
}))
|
||||
+4
-5
@@ -24,9 +24,10 @@ type Props = {
|
||||
className?: string,
|
||||
filter?: MapFilter
|
||||
search?: string,
|
||||
linked?: boolean
|
||||
}
|
||||
|
||||
export const LocalMapsListPanel = forwardRef(({version, className, filter, search} : Props, forwardRef) => {
|
||||
export const LocalMapsListPanel = forwardRef(({version, className, filter, search, linked} : Props, forwardRef) => {
|
||||
|
||||
const mapsManager = MapsManagerService.getInstance();
|
||||
const mapsDownloader = MapsDownloaderService.getInstance();
|
||||
@@ -35,7 +36,7 @@ export const LocalMapsListPanel = forwardRef(({version, className, filter, searc
|
||||
|
||||
const t = useTranslation();
|
||||
const ref = useRef(null)
|
||||
const isVisible = useInView(ref, {once: true});
|
||||
const isVisible = useInView(ref, {once: true, amount: .5});
|
||||
const [maps, setMaps] = useState<BsmLocalMap[]>(null);
|
||||
const [subs] = useState<Subscription[]>([]);
|
||||
const [selectedMaps$] = useState(new BehaviorSubject<BsmLocalMap[]>([]));
|
||||
@@ -65,8 +66,6 @@ export const LocalMapsListPanel = forwardRef(({version, className, filter, searc
|
||||
|
||||
if(isVisible){
|
||||
loadMaps();
|
||||
subs.push(mapsManager.versionLinked$.subscribe(loadMaps));
|
||||
subs.push(mapsManager.versionUnlinked$.subscribe(loadMaps));
|
||||
mapsDownloader.addOnMapDownloadedListener((map, targerVersion) => {
|
||||
if(targerVersion !== version){ return; }
|
||||
setMaps(maps => maps ? [map, ...maps] : [map]);
|
||||
@@ -79,7 +78,7 @@ export const LocalMapsListPanel = forwardRef(({version, className, filter, searc
|
||||
subs.forEach(s => s.unsubscribe());
|
||||
mapsDownloader.removeOnMapDownloadedListener(loadMaps);
|
||||
}
|
||||
}, [isVisible, version]);
|
||||
}, [isVisible, version, linked]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
|
||||
+36
-9
@@ -15,6 +15,9 @@ import { OsDiagnosticService } from "renderer/services/os-diagnostic.service"
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook"
|
||||
import { BsmIcon } from "../svgs/bsm-icon.component"
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook"
|
||||
import { LinkButton } from "./link-button.component"
|
||||
import { debounceTime } from "rxjs/operators"
|
||||
import { FolderLinkerService } from "renderer/services/folder-linker.service"
|
||||
|
||||
type Props = {
|
||||
version?: BSVersion
|
||||
@@ -25,12 +28,14 @@ export function MapsPlaylistsPanel({version}: Props) {
|
||||
const mapsService = MapsManagerService.getInstance();
|
||||
const mapsDownloader = MapsDownloaderService.getInstance();
|
||||
const osDiagnostic = OsDiagnosticService.getInstance();
|
||||
const linker = FolderLinkerService.getInstance();
|
||||
|
||||
const [tabIndex, setTabIndex] = useState(0);
|
||||
const [mapFilter, setMapFilter] = useState<MapFilter>({});
|
||||
const [mapSearch, setMapSearch] = useState("");
|
||||
const [playlistSearch, setPlaylistSearch] = useState("");
|
||||
const [mapsLinked, setMapsLinked] = useState(false);
|
||||
const [linkingPending, setLinkingPending] = useState(false);
|
||||
const isOnline = useObservable(osDiagnostic.isOnline$);
|
||||
const color = useThemeColor("first-color");
|
||||
const t = useTranslation();
|
||||
@@ -38,6 +43,23 @@ export function MapsPlaylistsPanel({version}: Props) {
|
||||
|
||||
useEffect(() => {
|
||||
loadMapIsLinked();
|
||||
|
||||
const sub = mapsService.$mapsLinkingPending(version).pipe(debounceTime(50)).subscribe(setLinkingPending);
|
||||
|
||||
const onMapsLinked = (folder: string) => {
|
||||
if(!folder.includes("CustomLevels")){ return; }
|
||||
loadMapIsLinked();
|
||||
}
|
||||
|
||||
linker.onFolderLinked(onMapsLinked);
|
||||
linker.onFolderUnlinked(onMapsLinked);
|
||||
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
linker.removeOnFolderLinked(onMapsLinked);
|
||||
linker.removeOnFolderUnlinked(onMapsLinked);
|
||||
}
|
||||
|
||||
}, [version]);
|
||||
|
||||
const loadMapIsLinked = () => {
|
||||
@@ -53,9 +75,9 @@ export function MapsPlaylistsPanel({version}: Props) {
|
||||
|
||||
const handleMapsLinkClick = () => {
|
||||
if(!mapsLinked){
|
||||
return mapsService.linkVersion(version).then(loadMapIsLinked);
|
||||
return mapsService.linkVersion(version);
|
||||
}
|
||||
return mapsService.unlinkVersion(version).then(loadMapIsLinked);
|
||||
return mapsService.unlinkVersion(version);
|
||||
}
|
||||
|
||||
const handleMapsAddClick = () => {
|
||||
@@ -64,8 +86,6 @@ export function MapsPlaylistsPanel({version}: Props) {
|
||||
|
||||
const renderTab = (props: DetailedHTMLProps<React.HTMLAttributes<HTMLLIElement>, HTMLLIElement>, text: string, index: number): JSX.Element => {
|
||||
|
||||
const linkedColor = mapsLinked ? color : "red";
|
||||
|
||||
const onClickLink = (index: number) => {
|
||||
if(index === 0){ handleMapsLinkClick(); }
|
||||
}
|
||||
@@ -91,10 +111,17 @@ export function MapsPlaylistsPanel({version}: Props) {
|
||||
</motion.div>
|
||||
)}
|
||||
{(!!version) && (
|
||||
<motion.div variants={variants} whileHover="hover" whileTap="tap" initial={{rotate: 0}} className="block p-0.5 h-[calc(100%-5px)] aspect-square blur-0 hover:brightness-75" title={t(mapsLinked ? "pages.version-viewer.maps.tabs.maps.actions.link-maps.tooltips.unlink" : "pages.version-viewer.maps.tabs.maps.actions.link-maps.tooltips.link")} onClick={e => {e.stopPropagation(); onClickLink(index)}}>
|
||||
<span className="absolute top-0 left-0 h-full w-full rounded-full brightness-50 opacity-75 dark:opacity-20 dark:filter-none" style={{backgroundColor: linkedColor}}/>
|
||||
<BsmIcon className="p-1 absolute top-0 left-0 h-full w-full !bg-transparent -rotate-45 brightness-150" icon={mapsLinked ? "link" : "unlink"} style={{color: linkedColor}} />
|
||||
</motion.div>
|
||||
<LinkButton
|
||||
variants={variants}
|
||||
disabled={linkingPending}
|
||||
whileHover="hover"
|
||||
whileTap="tap"
|
||||
initial={{rotate: 0}}
|
||||
className="block p-0.5 h-[calc(100%-5px)] aspect-square blur-0 hover:brightness-75"
|
||||
linked={mapsLinked}
|
||||
title={t(mapsLinked ? "pages.version-viewer.maps.tabs.maps.actions.link-maps.tooltips.unlink" : "pages.version-viewer.maps.tabs.maps.actions.link-maps.tooltips.link")}
|
||||
onClick={() => onClickLink(index)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -130,7 +157,7 @@ export function MapsPlaylistsPanel({version}: Props) {
|
||||
<div className="w-full h-full flex flex-col bg-light-main-color-3 dark:bg-main-color-2 rounded-md shadow-black shadow-md overflow-hidden">
|
||||
<TabNavBar className="!rounded-none shadow-sm" tabIndex={tabIndex} tabsText={["misc.maps", "misc.playlists"]} onTabChange={setTabIndex} renderTab={renderTab}/>
|
||||
<div className="w-full grow min-h-0 flex flex-row items-center transition-transform duration-300" style={{transform: `translate(${-(tabIndex * 100)}%, 0)`}}>
|
||||
<LocalMapsListPanel ref={mapsRef} className="w-full h-full shrink-0 flex flex-col" version={version} filter={mapFilter} search={mapSearch}/>
|
||||
<LocalMapsListPanel ref={mapsRef} className="w-full h-full shrink-0 flex flex-col" version={version} filter={mapFilter} search={mapSearch} linked={mapsLinked}/>
|
||||
<div className="w-full h-full shrink-0 flex flex-col justify-center items-center content-center gap-2 overflow-hidden text-gray-800 dark:text-gray-200">
|
||||
<BsmImage className="rounded-md" image={wipGif}/>
|
||||
<span>Coming soon</span>
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
import Tippy from "@tippyjs/react";
|
||||
import { Variants } from "framer-motion";
|
||||
import { useEffect, useState } from "react";
|
||||
import { LinkButton } from "renderer/components/maps-mangement-components/link-button.component";
|
||||
import { BsmBasicSpinner } from "renderer/components/shared/bsm-basic-spinner/bsm-basic-spinner.component";
|
||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import { BsmIcon } from "renderer/components/svgs/bsm-icon.component";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { ConfigurationService } from "renderer/services/configuration.service";
|
||||
import { FolderLinkerService } from "renderer/services/folder-linker.service";
|
||||
import { IpcService } from "renderer/services/ipc.service";
|
||||
import { ModalComponent } from "renderer/services/modale.service";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
|
||||
export const ShareFoldersModal: ModalComponent<void, BSVersion> = ({ data }) => {
|
||||
|
||||
const SHARED_FOLDERS_KEY = "default-shared-folders";
|
||||
|
||||
const config = ConfigurationService.getInstance();
|
||||
const ipc = IpcService.getInstance();
|
||||
|
||||
const t = useTranslation();
|
||||
|
||||
const [folders, setFolders] = useState<string[]>([]);
|
||||
const versionPath = useObservable(ipc.sendV2<string>("get-version-full-path", {args: data}));
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const defaultFolders = config.get<string[]>(SHARED_FOLDERS_KEY);
|
||||
|
||||
const promises = Promise.allSettled(defaultFolders.map(async (folder) => {
|
||||
return ipc.sendV2<string>("relative-version-path-to-full", {args: {version: data, relative: folder}}).toPromise();
|
||||
}));
|
||||
|
||||
promises.then((res) => {
|
||||
|
||||
const fullPathFolders = res.reduce((acc, curent) => {
|
||||
if(curent.status !== "fulfilled"){ return acc; }
|
||||
acc.push(curent.value);
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
setFolders(prev => Array.from(new Set([...prev, ...fullPathFolders]).values()));
|
||||
});
|
||||
|
||||
ipc.sendV2<string[]>("get-linked-folders", {args: data}).toPromise().then(linkedFolders => {
|
||||
setFolders(prev => Array.from(new Set([...prev, ...linkedFolders]).values()));
|
||||
});
|
||||
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if(!folders?.length){
|
||||
config.delete(SHARED_FOLDERS_KEY);
|
||||
return;
|
||||
}
|
||||
|
||||
const promises = Promise.allSettled(folders.map(async folder => {
|
||||
return ipc.sendV2<string>("full-version-path-to-relative", {args: {version: data, fullPath: folder}}).toPromise();
|
||||
}));
|
||||
|
||||
promises.then((res) => {
|
||||
const sharedFolders = res.reduce((acc, curent) => {
|
||||
if(curent.status !== "fulfilled"){ return acc; }
|
||||
acc.push(curent.value);
|
||||
return acc;
|
||||
}, [] as string[]);
|
||||
config.set(SHARED_FOLDERS_KEY, sharedFolders);
|
||||
});
|
||||
|
||||
}, [folders]);
|
||||
|
||||
const addFolder = async () => {
|
||||
const folder = await ipc.sendV2<{canceled: boolean, filePaths: string[]}, string>("choose-folder", {args: versionPath}).toPromise();
|
||||
|
||||
if(!folder || folder.canceled || !folder.filePaths?.length){ return; }
|
||||
const linkedFolder = folder.filePaths[0];
|
||||
|
||||
if(folders.includes(linkedFolder)){ return; }
|
||||
|
||||
setFolders(pre => [...pre, linkedFolder]);
|
||||
}
|
||||
|
||||
const removeFolder = (index: number) => {
|
||||
setFolders((prev) => prev.filter((_, i) => i !== index));
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="w-full max-w-md ">
|
||||
<h1 className="text-3xl uppercase tracking-wide w-full text-center text-gray-800 dark:text-gray-200">{t("modals.shared-folders.title")}</h1>
|
||||
<p className="my-3">{t("modals.shared-folders.description")}</p>
|
||||
<ul className="flex flex-col gap-1 mb-2 h-[300px] max-h-[300px] overflow-scroll scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900 px-1">
|
||||
{folders.map((folder, index) => (
|
||||
<FolderItem key={index} path={folder} onDelete={() => {removeFolder(index)}}/>
|
||||
))}
|
||||
</ul>
|
||||
<div className="w-full h-12 rounded-md flex justify-center items-center cursor-pointer bg-light-main-color-1 dark:bg-main-color-1 hover:bg-light-main-color-3 hover:dark:bg-main-color-3" onClick={addFolder}>
|
||||
<BsmIcon className="aspect-square h-8" icon="add"/>
|
||||
<span className="font-bold">{t("modals.shared-folders.buttons.add-folder")}</span>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
type FolderProps = {
|
||||
path: string;
|
||||
onDelete?: () => void;
|
||||
}
|
||||
|
||||
const FolderItem = ({path, onDelete}: FolderProps) => {
|
||||
|
||||
const linker = FolderLinkerService.getInstance();
|
||||
|
||||
const t = useTranslation();
|
||||
|
||||
const name = path.split("\\").at(-1);
|
||||
const color = useThemeColor("first-color");
|
||||
|
||||
const variants: Variants = {
|
||||
hover: {rotate: 22.5},
|
||||
tap: {rotate: 45}
|
||||
};
|
||||
|
||||
const pending = useObservable(linker.isPending(path));
|
||||
const processing = useObservable(linker.isProcessing(path));
|
||||
const linkDisabled = pending || processing;
|
||||
|
||||
const [linked, setLinked] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadFolderIsLinked();
|
||||
}, [path]);
|
||||
|
||||
const loadFolderIsLinked = () => {
|
||||
linker.isFolderLinked(path).toPromise().then(setLinked);
|
||||
}
|
||||
|
||||
const onClickLink = () => {
|
||||
if(linked){
|
||||
return linker.unlinkFolder(path).toPromise().then(loadFolderIsLinked);
|
||||
}
|
||||
return linker.linkFolder(path).toPromise().then(loadFolderIsLinked);
|
||||
}
|
||||
|
||||
const cancelLink = () => {
|
||||
linker.cancelFolder(path);
|
||||
loadFolderIsLinked();
|
||||
}
|
||||
|
||||
return (
|
||||
<li className="w-full h-12 rounded-md shrink-0 flex flex-row items-center justify-between px-2 font-bold bg-light-main-color-1 dark:bg-main-color-1">
|
||||
<span className="cursor-help" title={path}>{name}</span>
|
||||
<div className="flex flex-row gap-1.5">
|
||||
<Tippy placement="left" content={t(`modals.shared-folders.buttons.${linked ? "unlink-folder" : "link-folder"}`)} arrow={false}>
|
||||
<LinkButton variants={variants} linked={linked} disabled={linkDisabled} whileHover="hover" whileTap="tap" className="p-0.5 h-7 shrink-0 aspect-square blur-0 cursor-pointer hover:brightness-75" onClick={onClickLink}/>
|
||||
</Tippy>
|
||||
{!processing ? (
|
||||
!pending ? (
|
||||
<BsmButton className="aspect-square h-7 rounded-md p-1" icon={"trash"} withBar={false} onClick={e => {e.preventDefault(); onDelete?.()}}/>
|
||||
) : (
|
||||
<BsmButton className="aspect-square h-7 rounded-md p-1" icon={"cross"} withBar={false} onClick={e => {e.preventDefault(); cancelLink()}}/>
|
||||
)
|
||||
) : (
|
||||
<BsmBasicSpinner className="aspect-square h-7 rounded-md p-1 dark:bg-main-color-2" thikness="3.5px" style={{color}}/>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
|
||||
}
|
||||
@@ -64,7 +64,7 @@ export default function TitleBar({template = "index.html"} : {template: AppWindo
|
||||
<div className='shrink-0 w-0 overflow-hidden transition-all group-hover:w-16 group-hover:overflow-visible group-active:w-16 group-active:overflow-visible text-main-color-3'>
|
||||
<BsmRange min={0} max={1} step={.01} values={[volume.muted ? 0 : volume.volume]} colors={[color, "currentColor"]} onChange={val => audio.setVolume(val[0])} onFinalChange={val => audio.setFinalVolume(val[0])}/>
|
||||
</div>
|
||||
<BsmButton className='shrink-0 h-[23px] w-[23px] aspect-square !bg-transparent flex items-start' iconClassName={volumeIcon === "volume-down" && "-translate-x-[1.8px]"} icon={volumeIcon} withBar={false} onClick={() => audio.toggleMute()}/>
|
||||
<BsmButton className='shrink-0 h-[23px] w-[23px] aspect-square !bg-transparent flex items-start' iconClassName={volumeIcon === "volume-down" ? "-translate-x-[1.8px]" : null} icon={volumeIcon} withBar={false} onClick={() => audio.toggleMute()}/>
|
||||
</div>
|
||||
<div onClick={minimizeWindow} className="text-gray-800 dark:text-gray-200 hover:bg-gray-300 dark:hover:bg-[#4F545C] cursor-pointer w-11 h-full shrink-0 flex justify-center items-center" id="min-button" >
|
||||
<svg aria-hidden="false" width="12" height="12" viewBox="0 0 12 12"><rect fill="currentColor" width="10" height="1" x="1" y="6"> </rect></svg>
|
||||
|
||||
@@ -4,9 +4,14 @@ export const defaultConfiguration: {[key in DefaultConfigKey]: any} = {
|
||||
"theme": "os",
|
||||
"language": window.navigator.language.length <= 2 ? `${window.navigator.language}-${window.navigator.language.toLocaleUpperCase()}` : window.navigator.language,
|
||||
"supported_languages": ["en-US", "en-EN", "fr-FR", "es-ES"],
|
||||
"default_mods": ["SongCore", "WhyIsThereNoLeaderboard", "BeatSaverDownloader", "BeatSaverVoting", "PlaylistManager"]
|
||||
"default_mods": ["SongCore", "WhyIsThereNoLeaderboard", "BeatSaverDownloader", "BeatSaverVoting", "PlaylistManager"],
|
||||
"default-shared-folders": [
|
||||
"Beat Saber_Data\\CustomLevels",
|
||||
"Beat Saber_Data\\CustomWIPLevels",
|
||||
"DLC"
|
||||
]
|
||||
}
|
||||
|
||||
export type DefaultConfigKey = "first-color" | "second-color" | "theme" | "language" | "supported_languages" | "default_mods";
|
||||
export type DefaultConfigKey = "first-color" | "second-color" | "theme" | "language" | "supported_languages" | "default_mods" | "default-shared-folders";
|
||||
|
||||
export type ThemeConfig = "dark" | "light" | "os"
|
||||
|
||||
@@ -54,15 +54,15 @@ export function AvailableVersionsList() {
|
||||
|
||||
if(modalRes.exitCode !== ModalExitCode.COMPLETED){ return; }
|
||||
|
||||
const folderRes = await ipc.send<{canceled: boolean, filePaths: string[]}>("choose-folder");
|
||||
const folderRes = await ipc.sendV2<{canceled: boolean, filePaths: string[]}>("choose-folder").toPromise();
|
||||
|
||||
if(!folderRes.success || folderRes.data.canceled || !folderRes.data.filePaths?.length){ return; }
|
||||
if(!folderRes || folderRes.canceled || !folderRes.filePaths?.length){ return; }
|
||||
|
||||
notification.notifySuccess({title: "notifications.bs-import-version.success.start-import.title", desc: "notifications.bs-import-version.success.start-import.desc", duration: 6_000});
|
||||
|
||||
progressBar.showFake(.008);
|
||||
|
||||
const toImport = folderRes.data.filePaths.at(0);
|
||||
const toImport = folderRes.filePaths.at(0);
|
||||
|
||||
const imported = await installer.importVersion(toImport);
|
||||
|
||||
|
||||
@@ -110,11 +110,12 @@ export function SettingsPage() {
|
||||
|
||||
modalService.openModal(InstallationFolderModal).then(async res => {
|
||||
if(res.exitCode !== ModalExitCode.COMPLETED){ return; }
|
||||
const fileChooserRes = await ipcService.send<{canceled: boolean, filePaths: string[]}>("choose-folder");
|
||||
|
||||
if(fileChooserRes.success && !fileChooserRes.data.canceled && fileChooserRes.data.filePaths?.length){
|
||||
const fileChooserRes = await ipcService.sendV2<{canceled: boolean, filePaths: string[]}>("choose-folder").toPromise();
|
||||
|
||||
if(!fileChooserRes.canceled && fileChooserRes.filePaths?.length){
|
||||
progressBarService.showFake(.008);
|
||||
downloaderService.setInstallationFolder(fileChooserRes.data.filePaths[0]).then(res => {
|
||||
downloaderService.setInstallationFolder(fileChooserRes.filePaths[0]).then(res => {
|
||||
setTimeout(() => {
|
||||
progressBarService.complete();
|
||||
setTimeout(() => progressBarService.hide(true), 1000);
|
||||
|
||||
@@ -15,6 +15,7 @@ import { LaunchSlide } from 'renderer/components/version-viewer/slides/launch/la
|
||||
import { ModsSlide } from 'renderer/components/version-viewer/slides/mods/mods-slide.component';
|
||||
import { UninstallModal } from 'renderer/components/modal/modal-types/uninstall-modal.component';
|
||||
import { MapsPlaylistsPanel } from 'renderer/components/maps-mangement-components/maps-playlists-panel.component';
|
||||
import { ShareFoldersModal } from 'renderer/components/modal/modal-types/share-folders-modal.component';
|
||||
|
||||
export function VersionViewer() {
|
||||
|
||||
@@ -66,6 +67,10 @@ export function VersionViewer() {
|
||||
setCurrentTabIndex(() => 0);
|
||||
}
|
||||
|
||||
const openShareFolderModal = () => {
|
||||
modalService.openModal(ShareFoldersModal, state);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -87,6 +92,7 @@ export function VersionViewer() {
|
||||
((!state.steam && !state.oculus) && {text: "pages.version-viewer.dropdown.verify-files", icon: "task", onClick: verifyFiles}),
|
||||
((!state.steam && !state.oculus) && {text: "pages.version-viewer.dropdown.edit", icon: "edit", onClick: edit}),
|
||||
(!state.oculus && {text: "pages.version-viewer.dropdown.clone", icon: "copy", onClick: clone}),
|
||||
{text: "pages.version-viewer.dropdown.shared-folders", icon: "link", onClick: openShareFolderModal},
|
||||
((!state.steam && !state.oculus) && {text: "pages.version-viewer.dropdown.uninstall", icon:"trash", onClick: uninstall})
|
||||
]}/>
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
import { of, timer } from "rxjs";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { map, distinctUntilChanged, filter } from "rxjs/operators";
|
||||
import { from } from "rxjs";
|
||||
import { Observable } from "rxjs";
|
||||
import { IpcService } from "./ipc.service";
|
||||
import { ProgressBarService } from "./progress-bar.service";
|
||||
import { NotificationService } from "./notification.service";
|
||||
|
||||
export class FolderLinkerService {
|
||||
|
||||
private static instance: FolderLinkerService;
|
||||
|
||||
public static getInstance(): FolderLinkerService {
|
||||
if (!FolderLinkerService.instance) {
|
||||
FolderLinkerService.instance = new FolderLinkerService();
|
||||
}
|
||||
return FolderLinkerService.instance;
|
||||
}
|
||||
|
||||
private readonly ipc: IpcService;
|
||||
private readonly progress: ProgressBarService;
|
||||
private readonly notification: NotificationService;
|
||||
|
||||
private readonly onLinkedListeners = new Map<string, () => void>();
|
||||
private readonly onUnlinkedListeners = new Map<string, () => void>();
|
||||
private readonly onRemovedFromQueueListeners = new Map<string, () => void>();
|
||||
|
||||
private readonly publicOnLinkedListeners = new Set<(folder: string) => void>();
|
||||
private readonly publicOnUnlinkedListeners = new Set<(folder: string) => void>();
|
||||
|
||||
private readonly _currentAction$ = new BehaviorSubject<LinkAction>(null);
|
||||
private readonly _queue$ = new BehaviorSubject<LinkAction[]>([]);
|
||||
|
||||
private constructor() {
|
||||
this.ipc = IpcService.getInstance();
|
||||
this.progress = ProgressBarService.getInstance();
|
||||
this.notification = NotificationService.getInstance();
|
||||
|
||||
this.queue$.pipe(map(queue => !!queue.length), distinctUntilChanged(), filter(haveAction => haveAction)).subscribe(() => {
|
||||
this.startQueue();
|
||||
});
|
||||
}
|
||||
|
||||
private async startQueue(): Promise<void>{
|
||||
|
||||
while(this._queue$.value.at(0)){
|
||||
|
||||
const toDo = this._queue$.value.at(0);
|
||||
|
||||
let progressOpened = false;
|
||||
|
||||
if(!this.progress.isVisible){
|
||||
this.progress.showFake(.01, null, toDo.folder.split("\\").at(-1));
|
||||
progressOpened = true;
|
||||
}
|
||||
|
||||
this._currentAction$.next(toDo);
|
||||
await this.doAction(toDo).toPromise();
|
||||
|
||||
this.specialFolderNotification(toDo);
|
||||
|
||||
if(toDo.type === LinkActionType.Link){
|
||||
this.onLinkedListeners.get(toDo.folder)?.();
|
||||
this.publicOnLinkedListeners.forEach(listener => listener(toDo.folder));
|
||||
this.onLinkedListeners.delete(toDo.folder);
|
||||
}
|
||||
else{
|
||||
this.onUnlinkedListeners.get(toDo.folder)?.();
|
||||
this.publicOnUnlinkedListeners.forEach(listener => listener(toDo.folder));
|
||||
this.onUnlinkedListeners.delete(toDo.folder);
|
||||
}
|
||||
|
||||
const newArr = [...this._queue$.value];
|
||||
newArr.shift();
|
||||
this._queue$.next(newArr);
|
||||
|
||||
if(progressOpened){
|
||||
this.progress.hide(true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
this._currentAction$.next(null);
|
||||
}
|
||||
|
||||
private specialFolderNotification(action: LinkAction): void{
|
||||
if(action.type === LinkActionType.Link && action.folder.includes("UserData")){
|
||||
this.notification.notifySuccess({
|
||||
title: "notifications.shared-folder.success.yeet-mods-disabled.title",
|
||||
desc: "notifications.shared-folder.success.yeet-mods-disabled.msg"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private doAction(action: LinkAction): Observable<void>{
|
||||
return this.ipc.sendV2(action.type === LinkActionType.Link ? "link-folder" : "unlink-folder", { args: { folder: action.folder, keepContents: action.keepContents } });
|
||||
}
|
||||
|
||||
private addFolderToQueue(folder: string, type: LinkActionType, keepContents: boolean){
|
||||
if(this._queue$.value.some(action => action.folder === folder)){ return; }
|
||||
this._queue$.next([...this._queue$.value, {folder, type, keepContents}]);
|
||||
}
|
||||
|
||||
private removeFolderFromQueue(folder: string){
|
||||
const queue = this._queue$.value;
|
||||
const index = queue.findIndex(action => action.folder === folder);
|
||||
if(index === -1){ return; }
|
||||
this.onRemovedFromQueueListeners.get(folder)?.();
|
||||
queue.splice(index, 1);
|
||||
this._queue$.next(queue);
|
||||
}
|
||||
|
||||
private onLinked(folder: string, callback: () => void){ this.onLinkedListeners.set(folder, callback); }
|
||||
private onUnlinked(folder: string, callback: () => void){ this.onUnlinkedListeners.set(folder, callback); }
|
||||
private onRemovedFromQueue(folder: string, callback: () => void){ this.onRemovedFromQueueListeners.set(folder, callback); }
|
||||
|
||||
public isFolderLinked(path: string): Observable<boolean> {
|
||||
return this.ipc.sendV2<boolean>("is-folder-symlink", {args: path});
|
||||
}
|
||||
|
||||
public linkFolders(...folders: string[]): Map<string, Observable<void>> {
|
||||
return new Map(folders.map(folder => [folder, this.linkFolder(folder)]));
|
||||
}
|
||||
|
||||
public linkFolder(folder: string, keepContents = true): Observable<void> {
|
||||
const promise = new Promise<void>(resolve => {
|
||||
this.onLinked(folder, resolve);
|
||||
this.onRemovedFromQueue(folder, resolve);
|
||||
});
|
||||
|
||||
this.addFolderToQueue(folder, LinkActionType.Link, keepContents);
|
||||
return from(promise);
|
||||
}
|
||||
|
||||
public unlinkFolders(...folders: string[]): Map<string, Observable<void>> {
|
||||
return new Map(folders.map(folder => [folder, this.unlinkFolder(folder)]));
|
||||
}
|
||||
|
||||
public unlinkFolder(folder: string, keepContents = true): Observable<void> {
|
||||
const promise = new Promise<void>(resolve => {
|
||||
this.onUnlinked(folder, resolve);
|
||||
this.onRemovedFromQueue(folder, resolve);
|
||||
});
|
||||
|
||||
this.addFolderToQueue(folder, LinkActionType.Unlink, keepContents);
|
||||
return from(promise);
|
||||
}
|
||||
|
||||
public cancelFolder(folder: string): void {
|
||||
this.removeFolderFromQueue(folder);
|
||||
}
|
||||
|
||||
public get queue$(): Observable<LinkAction[]> { return this._queue$.asObservable(); }
|
||||
public get currentAction$(): Observable<LinkAction> { return this._currentAction$.asObservable(); }
|
||||
|
||||
public isPending(folder: string): Observable<boolean> {
|
||||
return this.queue$.pipe(map(queue => queue.some(action => action.folder === folder), distinctUntilChanged()));
|
||||
}
|
||||
|
||||
public isProcessing(folder: string): Observable<boolean> {
|
||||
return this.currentAction$.pipe(distinctUntilChanged(), map(action => action?.folder === folder));
|
||||
}
|
||||
|
||||
public onFolderLinked(callback: (folder: string) => void): void { this.publicOnLinkedListeners.add(callback); }
|
||||
public removeOnFolderLinked(callback: (folder: string) => void): void { this.publicOnLinkedListeners.delete(callback); }
|
||||
public onFolderUnlinked(callback: (folder: string) => void): void { this.publicOnUnlinkedListeners.add(callback); }
|
||||
public removeOnFolderUnlinked(callback: (folder: string) => void): void { this.publicOnUnlinkedListeners.delete(callback); }
|
||||
|
||||
}
|
||||
|
||||
export interface LinkAction {
|
||||
folder: string;
|
||||
type: LinkActionType;
|
||||
keepContents?: boolean;
|
||||
}
|
||||
|
||||
export enum LinkActionType {
|
||||
Link,
|
||||
Unlink
|
||||
}
|
||||
@@ -53,7 +53,7 @@ export class IpcService {
|
||||
|
||||
public sendV2<T, U = unknown>(channel: string, request?: IpcRequest<U>, defaultValue?: T): Observable<T>{
|
||||
if(!request){ request = {args: null, responceChannel: null}; }
|
||||
if(!request.responceChannel){ request.responceChannel = `${channel}_responce_${new Date().getTime()}`; }
|
||||
if(!request.responceChannel){ request.responceChannel = `${channel}_responce_${crypto.randomUUID()}`; }
|
||||
|
||||
const completeChannel = `${request.responceChannel}_complete`;
|
||||
const errorChannel = `${request.responceChannel}_error`;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LinkMapsModal } from "renderer/components/modal/modal-types/link-maps-modal.component";
|
||||
import { UnlinkMapsModal } from "renderer/components/modal/modal-types/unlink-maps-modal.component";
|
||||
import { Subject, Observable } from "rxjs";
|
||||
import { Subject, Observable, of } from "rxjs";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { BsmLocalMapsProgress, BsmLocalMap, DeleteMapsProgress } from "shared/models/maps/bsm-local-map.interface";
|
||||
import { IpcService } from "./ipc.service";
|
||||
@@ -11,9 +11,9 @@ import { OpenSaveDialogOption } from "shared/models/ipc";
|
||||
import { NotificationService } from "./notification.service";
|
||||
import { ConfigurationService } from "./configuration.service";
|
||||
import { ArchiveProgress } from "shared/models/archive.interface";
|
||||
import { map, last, catchError } from "rxjs/operators";
|
||||
import { of } from "rxjs";
|
||||
import { map, last, catchError, distinctUntilChanged, mergeMap } from "rxjs/operators";
|
||||
import { ProgressionInterface } from "shared/models/progress-bar";
|
||||
import { FolderLinkerService } from "./folder-linker.service";
|
||||
|
||||
export class MapsManagerService {
|
||||
|
||||
@@ -31,6 +31,7 @@ export class MapsManagerService {
|
||||
private readonly progressBar: ProgressBarService;
|
||||
private readonly notifications: NotificationService;
|
||||
private readonly config: ConfigurationService;
|
||||
private readonly linker: FolderLinkerService;
|
||||
|
||||
private readonly lastLinkedVersion$: Subject<BSVersion> = new Subject();
|
||||
private readonly lastUnlinkedVersion$: Subject<BSVersion> = new Subject();
|
||||
@@ -41,17 +42,20 @@ export class MapsManagerService {
|
||||
this.progressBar = ProgressBarService.getInstance();
|
||||
this.notifications = NotificationService.getInstance();
|
||||
this.config = ConfigurationService.getInstance();
|
||||
this.linker = FolderLinkerService.getInstance();
|
||||
}
|
||||
|
||||
private async getVersionMapsPath(version: BSVersion): Promise<string>{
|
||||
return this.ipcService.sendV2<string>("get-version-maps-path", {args: version}).toPromise();
|
||||
}
|
||||
|
||||
public getMaps(version?: BSVersion): Observable<BsmLocalMapsProgress>{
|
||||
return this.ipcService.sendV2<BsmLocalMapsProgress>("load-version-maps", {args: version}, {loaded: 0, total: 0, maps: []});
|
||||
}
|
||||
|
||||
public versionHaveMapsLinked(version: BSVersion): Promise<boolean>{
|
||||
return this.ipcService.send<boolean, BSVersion>("verion-have-maps-linked", {args: version}).then(res => {
|
||||
if(!res.success){ throw "error"; }
|
||||
return res.data;
|
||||
})
|
||||
public async versionHaveMapsLinked(version: BSVersion): Promise<boolean>{
|
||||
const versionMapsPath = await this.getVersionMapsPath(version);
|
||||
return this.linker.isFolderLinked(versionMapsPath).toPromise();
|
||||
}
|
||||
|
||||
public async linkVersion(version: BSVersion): Promise<void>{
|
||||
@@ -60,21 +64,9 @@ export class MapsManagerService {
|
||||
|
||||
if(modalRes.exitCode !== ModalExitCode.COMPLETED){ return; }
|
||||
|
||||
const showProgressBar = this.progressBar.require();
|
||||
const versionMapsPath = await this.getVersionMapsPath(version);
|
||||
|
||||
if(showProgressBar){
|
||||
this.progressBar.showFake(.01);
|
||||
}
|
||||
|
||||
const res = await this.ipcService.send<void, {version: BSVersion, keepMaps: boolean}>("link-version-maps", {args: {version, keepMaps: !!modalRes.data}});
|
||||
|
||||
if(showProgressBar){
|
||||
this.progressBar.hide(true);
|
||||
}
|
||||
|
||||
if(res.success){
|
||||
this.lastLinkedVersion$.next(version);
|
||||
}
|
||||
return this.linker.linkFolder(versionMapsPath, !!modalRes.data).toPromise();
|
||||
}
|
||||
|
||||
public async unlinkVersion(version: BSVersion): Promise<void>{
|
||||
@@ -83,21 +75,9 @@ export class MapsManagerService {
|
||||
|
||||
if(modalRes.exitCode !== ModalExitCode.COMPLETED){ return; }
|
||||
|
||||
const showProgressBar = this.progressBar.require();
|
||||
const versionMapsPath = await this.getVersionMapsPath(version);
|
||||
|
||||
if(showProgressBar){
|
||||
this.progressBar.showFake(.01);
|
||||
}
|
||||
|
||||
const res = await this.ipcService.send<void, {version: BSVersion, keepMaps: boolean}>("unlink-version-maps", {args: {version, keepMaps: !!modalRes.data}});
|
||||
|
||||
if(showProgressBar){
|
||||
this.progressBar.hide(true);
|
||||
}
|
||||
|
||||
if(res.success){
|
||||
this.lastUnlinkedVersion$.next(version);
|
||||
}
|
||||
return this.linker.unlinkFolder(versionMapsPath, !!modalRes.data).toPromise();
|
||||
}
|
||||
|
||||
public async deleteMaps(maps: BsmLocalMap[], version?: BSVersion): Promise<boolean>{
|
||||
@@ -173,4 +153,11 @@ export class MapsManagerService {
|
||||
return this.lastUnlinkedVersion$.asObservable();
|
||||
}
|
||||
|
||||
public $mapsLinkingPending(version: BSVersion): Observable<boolean>{
|
||||
return this.linker.queue$.pipe(mergeMap(async queue => {
|
||||
const versionMapsPath = await this.getVersionMapsPath(version);
|
||||
return queue.some(q => q.folder.includes(versionMapsPath));
|
||||
}), distinctUntilChanged());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -51,7 +51,7 @@ export class ModalService{
|
||||
|
||||
}
|
||||
|
||||
export type ModalComponent<T = unknown, K = any> = ({resolver, data}: {resolver : (x: ModalResponse<T>) => void, data?: K}) => JSX.Element;
|
||||
export type ModalComponent<Return = unknown, Receive = any> = ({resolver, data}: {resolver : (x: ModalResponse<Return>) => void, data?: Receive}) => JSX.Element;
|
||||
|
||||
export const enum ModalExitCode {
|
||||
NO_CHOICE = -1,
|
||||
|
||||
@@ -59,12 +59,12 @@ export class ProgressBarService{
|
||||
this._style$.next(style);
|
||||
}
|
||||
|
||||
public showFake(speed: number, style?: CSSProperties): void{
|
||||
public showFake(speed: number, style?: CSSProperties, label?: string): void{
|
||||
const obs: Observable<ProgressionInterface> = timer(1000, 100).pipe(map(val => {
|
||||
if(this._progression$.value.progression >= 100){ return {progression: 100}; }
|
||||
if(this._progression$.value.progression >= 100){ return {progression: 100, label}; }
|
||||
const currentProgress = speed * (val + 1);
|
||||
const progress = Math.round(Math.atan(currentProgress) / (Math.PI / 2) * 100 * 1000) / 1000;
|
||||
return {progression: progress} as ProgressionInterface;
|
||||
return {progression: progress, label} as ProgressionInterface;
|
||||
}));
|
||||
this.show(obs, true, style);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user