mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0511540b98 | |||
| dcb233c957 | |||
| 804cb22f56 | |||
| 00e4059213 | |||
| 83fcbf6d04 | |||
| 420ecc9ae6 | |||
| 835e9a0678 | |||
| e1a6721f7d | |||
| 2e588ef4cb | |||
| d3e0099d14 | |||
| b9acdc9578 | |||
| fefd552f60 | |||
| 6eb2866eee | |||
| 01401c4691 | |||
| 37c2bfcd04 | |||
| e190217390 | |||
| 5fa7013106 | |||
| abf53489f5 | |||
| 0db13a8474 | |||
| e1229a45a0 | |||
| eafe26507a | |||
| 5c70a7d974 | |||
| da8bae8582 | |||
| 4cce24e275 | |||
| 18f2f18e41 | |||
| ec3c6ce26e | |||
| 9862dfa78d | |||
| e3f91ea593 | |||
| 509b2cd9b9 | |||
| 1b54d86324 | |||
| 425b2ecdb1 | |||
| 3b973e1597 | |||
| 86d2781e67 | |||
| 5d10f139a9 | |||
| 9b5a1711ce | |||
| 646ec820a5 | |||
| a144a308f6 | |||
| 07818cf8f2 |
@@ -43,7 +43,7 @@
|
||||
<a href="https://github.com/Zagrios/bs-manager/issues"><img alt="GitHub issues" src="https://img.shields.io/github/issues/Zagrios/bs-manager?style=for-the-badge"></a>
|
||||
<br>
|
||||
<a href="https://discord.gg/uSqbHVpKdV"><img src ="https://img.shields.io/badge/-DISCORD-5865f2?style=for-the-badge&logo=discord&logoColor=ffffff" alt="discord"/></a>
|
||||
<!--<a href="https://mee6.xyz/fr/m/bsmanager"><img src ="https://img.shields.io/badge/-🥰%20Support%20BSM-EC4546?style=for-the-badge" alt="Donation"/></a>-->
|
||||
<a href="https://www.patreon.com/bsmanager"><img src ="https://img.shields.io/badge/-🥰%20Support%20BSM-EC4546?style=for-the-badge" alt="Donation"/></a>
|
||||
<a href="https://twitter.com/BSManager_"><img src ="https://img.shields.io/badge/-Twitter-F5F8FA?style=for-the-badge&logo=Twitter" alt="Twitter"/></a>
|
||||
<br>
|
||||
[<a href="#readme-bot">Go to bottom</a>]
|
||||
|
||||
@@ -28,7 +28,11 @@
|
||||
"desktop": "Desktop Mode",
|
||||
"desktop-description": "This allows you to use WASD and the mouse to navigate around the menu in game. This makes testing much easier, because you don't have to put on your headset!",
|
||||
"debug": "Debug Mode",
|
||||
"debug-description": "Enables the output log window for IPA. This will show the debug console that mods use."
|
||||
"debug-description": "Enables the output log window for IPA. This will show the debug console that mods use.",
|
||||
"advanced-launch": {
|
||||
"button": "Advanced launch",
|
||||
"placeholder": "Additional arguments ex: --revert; --nowait"
|
||||
}
|
||||
},
|
||||
"maps": {
|
||||
"title": "Shared maps",
|
||||
@@ -84,7 +88,8 @@
|
||||
"verify-files": "Verify files",
|
||||
"clone": "Clone",
|
||||
"edit": "Edit",
|
||||
"uninstall": "Uninstall"
|
||||
"uninstall": "Uninstall",
|
||||
"shared-folders": "Shared Folders"
|
||||
}
|
||||
},
|
||||
"available-versions": {
|
||||
@@ -414,6 +419,14 @@
|
||||
"success":"Model installation complete",
|
||||
"error":"An error occurred while installing the model"
|
||||
}
|
||||
},
|
||||
"shared-folder": {
|
||||
"info": {
|
||||
"userdata-backup-created":{
|
||||
"title": "Backup created",
|
||||
"msg": "Sharing the 'UserData' folder can generate errors, in case of problems unlink the folder to restore the backup"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
@@ -563,6 +576,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": {
|
||||
|
||||
@@ -28,7 +28,11 @@
|
||||
"desktop": "Desktop Mod",
|
||||
"desktop-description": "Esto te permite usar WASD y el mouse para navegar por el menú en juego. Esto hace que las pruebas sean mucho más fáciles, ¡porque no tienes que ponerte el auricular!",
|
||||
"debug": "Debug Mod",
|
||||
"debug-description": "Habilita la ventana de registro de salida para IPA. Esto mostrará la consola de depuración que usan los mods."
|
||||
"debug-description": "Habilita la ventana de registro de salida para IPA. Esto mostrará la consola de depuración que usan los mods.",
|
||||
"advanced-launch": {
|
||||
"button": "Lanzamiento avanzado",
|
||||
"placeholder": "Argumentos adicionales ej: --revert; --no-wait"
|
||||
}
|
||||
},
|
||||
"maps": {
|
||||
"search-bar": {
|
||||
@@ -83,7 +87,8 @@
|
||||
"verify-files": "Verificar los archivos",
|
||||
"clone": "Clonar",
|
||||
"edit": "Editar",
|
||||
"uninstall": "Desinstalar"
|
||||
"uninstall": "Desinstalar",
|
||||
"shared-folders": "Carpetas Compartidas"
|
||||
}
|
||||
},
|
||||
"available-versions": {
|
||||
@@ -413,6 +418,14 @@
|
||||
"success":"Instalación del modelo completada",
|
||||
"error":"Se produjo un error durante la instalación del modelo"
|
||||
}
|
||||
},
|
||||
"shared-folder": {
|
||||
"info": {
|
||||
"userdata-backup-created":{
|
||||
"title": "Backup creado",
|
||||
"msg": "Compartir la carpeta 'UserData' puede generar errores, en caso de problemas desvincular la carpeta para restaurar la copia de seguridad"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
@@ -562,6 +575,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": {
|
||||
|
||||
@@ -28,7 +28,11 @@
|
||||
"desktop": "Mode Bureau",
|
||||
"desktop-description": "Cela vous permet d'utiliser WASD et la souris pour naviguer dans le menu en jeu. Cela rend les tests beaucoup plus faciles, car vous n'avez pas à mettre votre casque!",
|
||||
"debug": "Mode Debug",
|
||||
"debug-description": "Active la fenêtre de log pour IPA. Cela affichera la console de débogage utilisée par les mods."
|
||||
"debug-description": "Active la fenêtre de log pour IPA. Cela affichera la console de débogage utilisée par les mods.",
|
||||
"advanced-launch": {
|
||||
"button": "Lancement avancé",
|
||||
"placeholder": "Arguments supplémentaires ex: --revert; --nowait"
|
||||
}
|
||||
},
|
||||
"maps": {
|
||||
"search-bar": {
|
||||
@@ -83,7 +87,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 +418,14 @@
|
||||
"success":"Installation du modèle terminée",
|
||||
"error":"Une erreur s'est produite lors de l'installation du modèle"
|
||||
}
|
||||
},
|
||||
"shared-folder":{
|
||||
"info": {
|
||||
"userdata-backup-created":{
|
||||
"title": "Sauvegarde créée",
|
||||
"msg": "Le partage du dossier 'UserData' peut générer des erreurs, en cas de soucis déliez le dossier pour restaurer la sauvegarde"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
@@ -562,6 +575,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": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bs-manager",
|
||||
"version": "1.0.4",
|
||||
"version": "1.1.0",
|
||||
"description": "A foundation for scalable desktop apps",
|
||||
"main": "./dist/main/main.js",
|
||||
"author": {
|
||||
|
||||
@@ -3,3 +3,5 @@ export const OCULUS_BS_DIR = "hyperbolic-magnetism-beat-saber"
|
||||
export const BS_APP_ID = "620980";
|
||||
export const BS_DEPOT = "620981";
|
||||
export const APP_NAME = "BSManager";
|
||||
|
||||
export const STEAMVR_APP_ID = "250820";
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { copyFile, ensureDir, move, symlink } 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 async function copyDirectoryWithJunctions(source: string, destination: string): Promise<void> {
|
||||
|
||||
await ensureDir(destination);
|
||||
const items = await readdir(source);
|
||||
|
||||
for (const item of items) {
|
||||
const sourcePath = path.join(source, item);
|
||||
const destinationPath = path.join(destination, item);
|
||||
const stats = await lstat(sourcePath);
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
await copyDirectoryWithJunctions(sourcePath, destinationPath);
|
||||
} else if (stats.isFile()) {
|
||||
await copyFile(sourcePath, destinationPath);
|
||||
} else if (stats.isSymbolicLink()) {
|
||||
const symlinkTarget = await readlink(sourcePath);
|
||||
await symlink(symlinkTarget, destinationPath, "junction");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,18 @@ 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, LinkOptions } 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"
|
||||
import { VersionLinkerAction } from 'renderer/services/version-folder-linker.service';
|
||||
import { VersionFolderLinkerService } from '../services/version-folder-linker.service';
|
||||
|
||||
const ipc = IpcService.getInstance();
|
||||
|
||||
ipcMain.on('bs-version.get-version-dict', (event, req: IpcRequest<void>) => {
|
||||
BSVersionLibService.getInstance().getAvailableVersions().then(versions => {
|
||||
@@ -26,7 +38,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 +56,99 @@ 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<{version: BSVersion, options?: { relative?: boolean }}>, reply) => {
|
||||
const versionLinker = VersionFolderLinkerService.getInstance();
|
||||
reply(from(versionLinker.getLinkedFolders(req.args.version, req.args.options)));
|
||||
});
|
||||
|
||||
ipc.on("link-folder", async (req: IpcRequest<{ folder: string, options?: LinkOptions}>, reply) => {
|
||||
|
||||
req.args.options ??= {};
|
||||
|
||||
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.options?.keepContents, intermediateFolder: LocalMapsManagerService.SHARED_MAPS_FOLDER})));
|
||||
}
|
||||
|
||||
if(req.args.folder.includes("UserData")){
|
||||
req.args.options = { ...req.args.options, backup: true };
|
||||
}
|
||||
|
||||
const res = from(linker.linkFolder(req.args.folder, req.args.options));
|
||||
|
||||
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("link-version-folder-action", async (req: IpcRequest<VersionLinkerAction>, reply) => {
|
||||
const versionLinker = VersionFolderLinkerService.getInstance();
|
||||
reply(from(versionLinker.doAction(req.args)));
|
||||
});
|
||||
|
||||
ipc.on("is-version-folder-linked", async (req: IpcRequest<{ version: BSVersion, relativeFolder: string }>, reply) => {
|
||||
const versionLinker = VersionFolderLinkerService.getInstance();
|
||||
reply(from(versionLinker.isFolderLinked(req.args.version, req.args.relativeFolder)));
|
||||
});
|
||||
|
||||
ipc.on("unlink-folder", async (req: IpcRequest<{ folder: string, options?: LinkOptions}>, reply) => {
|
||||
|
||||
req.args.options ??= {};
|
||||
|
||||
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, {...req.args.options, intermediateFolder: LocalMapsManagerService.SHARED_MAPS_FOLDER})));
|
||||
}
|
||||
|
||||
if(req.args.folder.includes("UserData")){
|
||||
req.args.options = { ...req.args.options, backup: true };
|
||||
}
|
||||
|
||||
reply(from(linker.unlinkFolder(req.args.folder, req.args.options)));
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -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>) => {
|
||||
|
||||
@@ -2,6 +2,9 @@ import { ipcMain } from "electron";
|
||||
import { WindowManagerService } from "../services/window-manager.service";
|
||||
import { IpcRequest } from "shared/models/ipc";
|
||||
import { AppWindow } from "shared/models/window-manager/app-window.model";
|
||||
import { BSLauncherService } from "../services/bs-launcher.service";
|
||||
|
||||
const launcher = BSLauncherService.getInstance();
|
||||
|
||||
ipcMain.on("open-window-then-close-all", async (event, request: IpcRequest<AppWindow>) => {
|
||||
const windowManager = WindowManagerService.getInstance();
|
||||
@@ -12,11 +15,13 @@ ipcMain.on("open-window-then-close-all", async (event, request: IpcRequest<AppWi
|
||||
});
|
||||
|
||||
ipcMain.on("close-all-windows", async (event, request: IpcRequest<AppWindow>) => {
|
||||
await launcher.restoreSteamVR();
|
||||
const windowManager = WindowManagerService.getInstance();
|
||||
windowManager.closeAllWindows(request.args);
|
||||
});
|
||||
|
||||
ipcMain.on("close-windows", async (event, request: IpcRequest<AppWindow[]>) => {
|
||||
await launcher.restoreSteamVR();
|
||||
const windowManager = WindowManagerService.getInstance();
|
||||
windowManager.close(...request.args);
|
||||
});
|
||||
+10
-12
@@ -9,7 +9,7 @@
|
||||
* `./src/main.js` using webpack. This gives us some performance wins.
|
||||
*/
|
||||
import path from 'path';
|
||||
import { app, protocol } from 'electron';
|
||||
import { app, ipcMain, protocol } from 'electron';
|
||||
import log from 'electron-log';
|
||||
import './ipcs';
|
||||
import { UtilsService } from './services/utils.service';
|
||||
@@ -20,6 +20,8 @@ import { LocalMapsManagerService } from './services/additional-content/local-map
|
||||
import { LocalPlaylistsManagerService } from './services/additional-content/local-playlists-manager.service';
|
||||
import { LocalModelsManagerService } from './services/additional-content/local-models-manager.service';
|
||||
import { APP_NAME } from './constants';
|
||||
import { BSLauncherService } from './services/bs-launcher.service';
|
||||
import { IpcRequest } from 'shared/models/ipc';
|
||||
|
||||
const isDebug = process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true';
|
||||
|
||||
@@ -31,9 +33,6 @@ log.transports.file.resolvePath = (() => {
|
||||
|
||||
log.catchErrors();
|
||||
|
||||
const utilsService = UtilsService.getInstance();
|
||||
utilsService.setAssetsPath(app.isPackaged ? path.join(process.resourcesPath, 'assets') : path.join(__dirname, '../../assets'));
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
const sourceMapSupport = require('source-map-support');
|
||||
sourceMapSupport.install();
|
||||
@@ -63,14 +62,6 @@ const initServicesMustBeInitialized = () => {
|
||||
// Model
|
||||
}
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
// Respect the OSX convention of having the application in memory even
|
||||
// after all windows have been closed
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
|
||||
if(!gotTheLock){
|
||||
@@ -108,5 +99,12 @@ else{
|
||||
callback(pathname);
|
||||
});
|
||||
|
||||
BSLauncherService.getInstance().restoreSteamVR();
|
||||
|
||||
// Log renderer errors
|
||||
ipcMain.on('log-error', (event, args: IpcRequest<any>) => {
|
||||
log.error(args?.args);
|
||||
});
|
||||
|
||||
}).catch(log.error);
|
||||
}
|
||||
@@ -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});
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import path from "path";
|
||||
import { LaunchResult, LauchOption } from "shared/models/bs-launch";
|
||||
import { UtilsService } from "./utils.service";
|
||||
import { BS_EXECUTABLE, BS_APP_ID } from "../constants";
|
||||
import { BS_EXECUTABLE, BS_APP_ID, STEAMVR_APP_ID } from "../constants";
|
||||
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";
|
||||
import { rename } from "fs/promises";
|
||||
import log from "electron-log";
|
||||
import { timer } from "rxjs";
|
||||
|
||||
export class BSLauncherService{
|
||||
|
||||
@@ -30,6 +34,23 @@ export class BSLauncherService{
|
||||
this.localVersionService = BSLocalVersionService.getInstance();
|
||||
}
|
||||
|
||||
private getSteamVRPath(): Promise<string>{
|
||||
return this.steamService.getGameFolder(STEAMVR_APP_ID, "SteamVR");
|
||||
}
|
||||
|
||||
private async backupSteamVR(): Promise<void>{
|
||||
const steamVrFolder = await this.getSteamVRPath();
|
||||
if(!await pathExist(steamVrFolder)){ return; }
|
||||
return rename(steamVrFolder, steamVrFolder + ".bak").catch(log.error);
|
||||
}
|
||||
|
||||
public async restoreSteamVR(): Promise<void>{
|
||||
const steamVrFolder = await this.getSteamVRPath();
|
||||
const steamVrBackup = steamVrFolder + ".bak";
|
||||
if(!await pathExist(steamVrBackup)){ return; }
|
||||
return rename(steamVrFolder + ".bak", steamVrFolder).catch(log.error);
|
||||
}
|
||||
|
||||
public isBsRunning(): boolean{
|
||||
return this.bsProcess?.connected || this.utilsService.taskRunning(BS_EXECUTABLE);
|
||||
}
|
||||
@@ -42,24 +63,44 @@ 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 = [];
|
||||
let launchArgs = [];
|
||||
|
||||
if(launchOptions.oculus){ launchMods.push("-vrmode oculus"); }
|
||||
if(launchOptions.desktop){ launchMods.push("fpfc"); }
|
||||
if(launchOptions.debug){ launchMods.push("--verbose"); }
|
||||
if(!launchOptions.version.steam && !launchOptions.version.oculus){ launchArgs.push("--no-yeet"); }
|
||||
if(launchOptions.oculus){ launchArgs.push("-vrmode oculus"); }
|
||||
if(launchOptions.desktop){ launchArgs.push("fpfc"); }
|
||||
if(launchOptions.debug){ launchArgs.push("--verbose"); }
|
||||
if(launchOptions.additionalArgs){ launchArgs.push(...launchOptions.additionalArgs); }
|
||||
|
||||
if(launchOptions.debug){
|
||||
this.bsProcess = spawn(`\"${exePath}\"`, launchMods, {shell: true, cwd, env: {...process.env, "SteamAppId": BS_APP_ID}, detached: true, windowsVerbatimArguments: true });
|
||||
launchArgs = Array.from(new Set(launchArgs).values());
|
||||
|
||||
let restoreTimout: NodeJS.Timeout;
|
||||
|
||||
if(launchArgs.includes("fpfc")){
|
||||
await this.backupSteamVR().catch(() => {
|
||||
this.restoreSteamVR();
|
||||
}).finally(() => {
|
||||
restoreTimout = setTimeout(() => this.restoreSteamVR(), 35_000);
|
||||
});
|
||||
await timer(2_000).toPromise();
|
||||
}
|
||||
else{
|
||||
this.bsProcess = spawn(`\"${exePath}\"`, launchMods, {shell: true, cwd, env: {...process.env, "SteamAppId": BS_APP_ID} });
|
||||
await this.restoreSteamVR();
|
||||
}
|
||||
|
||||
this.bsProcess.on('message', msg => console.log(msg));
|
||||
this.bsProcess.on('error', err => console.log(err));
|
||||
this.bsProcess.on('exit', code => this.utilsService.ipcSend("bs-launch.exit", {data: code, success: code === 0}));
|
||||
if(launchOptions.debug){
|
||||
this.bsProcess = spawn(`\"${exePath}\"`, launchArgs, {shell: true, cwd, env: {...process.env, "SteamAppId": BS_APP_ID}, detached: true, windowsVerbatimArguments: true });
|
||||
}
|
||||
else{
|
||||
this.bsProcess = spawn(`\"${exePath}\"`, launchArgs, {shell: true, cwd, env: {...process.env, "SteamAppId": BS_APP_ID} });
|
||||
}
|
||||
|
||||
this.bsProcess.on('error', err => log.error(err));
|
||||
this.bsProcess.on('exit', code => {
|
||||
this.restoreSteamVR().finally(() => clearTimeout(restoreTimout));
|
||||
this.utilsService.ipcSend("bs-launch.exit", {data: code, success: code === 0})
|
||||
});
|
||||
|
||||
return "LAUNCHED";
|
||||
}
|
||||
|
||||
@@ -2,11 +2,9 @@ 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";
|
||||
import fs from "fs-extra";
|
||||
import { ConfigurationService } from "./configuration.service";
|
||||
import { rename } from "fs/promises";
|
||||
import { BsmException } from "shared/models/bsm-exception.model";
|
||||
@@ -14,6 +12,8 @@ import log from "electron-log";
|
||||
import { OculusService } from "./oculus.service";
|
||||
import { DownloadLinkType } from "shared/models/mods";
|
||||
import sanitize from "sanitize-filename";
|
||||
import { copyDirectoryWithJunctions, deleteFolder, getFoldersInFolder, pathExist } from "../helpers/fs.helpers";
|
||||
import { FolderLinkerService } from "./folder-linker.service";
|
||||
|
||||
export class BSLocalVersionService{
|
||||
|
||||
@@ -22,11 +22,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 +35,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);
|
||||
@@ -96,6 +96,7 @@ export class BSLocalVersionService{
|
||||
public async getVersionPath(version: BSVersion): Promise<string>{
|
||||
if(version.steam){ return this.steamService.getGameFolder(BS_APP_ID, "Beat Saber") }
|
||||
if(version.oculus){ return this.oculusService.getGameFolder(OCULUS_BS_DIR); }
|
||||
console.log("getVersionPath", this.getVersionFolder(version), version);
|
||||
return path.join(
|
||||
this.installLocationService.versionsDirectory,
|
||||
this.getVersionFolder(version)
|
||||
@@ -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,9 +221,9 @@ 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(() => {
|
||||
return copyDirectoryWithJunctions(originPath, newPath).then(() => {
|
||||
this.addCustomVersion(cloneVersion);
|
||||
return cloneVersion;
|
||||
}).catch((err: Error) => {
|
||||
@@ -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,117 @@
|
||||
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));
|
||||
}
|
||||
|
||||
private getBackupFolder(folderPath: string): string {
|
||||
return folderPath + "_backup";
|
||||
}
|
||||
|
||||
private async backupFolder(folderPath: string): Promise<void> {
|
||||
if(!await pathExist(folderPath)){ return; }
|
||||
return copy(folderPath, this.getBackupFolder(folderPath), { overwrite: true, errorOnExist: false });
|
||||
}
|
||||
|
||||
private async restoreFolder(folderPath: string): Promise<void> {
|
||||
if(!await pathExist( this.getBackupFolder(folderPath) )){ return; }
|
||||
return copy(this.getBackupFolder(folderPath), folderPath, { overwrite: true, errorOnExist: false }).then(() => {
|
||||
return deleteFolder(this.getBackupFolder(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(sharedPath);
|
||||
|
||||
if(options?.backup === true){
|
||||
await this.backupFolder(folderPath);
|
||||
}
|
||||
|
||||
await ensureFolderExist(folderPath);
|
||||
|
||||
if(options?.keepContents !== false){
|
||||
await moveFolderContent(folderPath, sharedPath).toPromise();
|
||||
}
|
||||
|
||||
await deleteFolder(folderPath);
|
||||
|
||||
return symlink(sharedPath, folderPath, "junction");
|
||||
}
|
||||
|
||||
public async unlinkFolder(folderPath: string, options?: UnlinkOptions): Promise<void> {
|
||||
|
||||
if(!(await this.isFolderSymlink(folderPath))){ return; }
|
||||
await unlinkPath(folderPath);
|
||||
|
||||
const sharedPath = this.getSharedFolder(folderPath, options?.intermediateFolder);
|
||||
|
||||
await ensureFolderExist(folderPath);
|
||||
|
||||
if(options?.backup === true){
|
||||
return this.restoreFolder(folderPath);
|
||||
}
|
||||
|
||||
if(options.moveContents === true){
|
||||
return moveFolderContent(sharedPath, folderPath).toPromise().then(() => {});
|
||||
}
|
||||
|
||||
if(options?.keepContents === false){ return; }
|
||||
|
||||
await ensureFolderExist(sharedPath);
|
||||
|
||||
return copy(sharedPath, folderPath, { errorOnExist: false, recursive: true });
|
||||
}
|
||||
|
||||
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,
|
||||
backup?: boolean,
|
||||
}
|
||||
|
||||
export interface UnlinkOptions extends LinkOptions {
|
||||
moveContents?: boolean,
|
||||
};
|
||||
@@ -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); }
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { IpcResponse, IpcRequest } from "shared/models/ipc";
|
||||
import { IpcRequest } from "shared/models/ipc";
|
||||
import { ipcMain } from "electron";
|
||||
import { Observable } from "rxjs";
|
||||
import { IpcChannel, IpcCompleteChannel, IpcErrorChannel } from "shared/models/ipc/ipc-response.interface";
|
||||
import { AppWindow } from "shared/models/window-manager/app-window.model";
|
||||
import { WindowManagerService } from "./window-manager.service";
|
||||
import { IpcReplier } from "shared/models/ipc/ipc-request.interface";
|
||||
import log from "electron-log";
|
||||
|
||||
export class IpcService {
|
||||
|
||||
@@ -40,13 +41,14 @@ 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{
|
||||
observable.subscribe(data => {
|
||||
this.send(channel, window, data);
|
||||
}, error => {
|
||||
log.error(error);
|
||||
this.send(this.getErrorChannel(channel), window, error);
|
||||
}, () => {
|
||||
this.send(this.getCompleteChannel(channel), window);
|
||||
|
||||
@@ -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,8 @@ 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";
|
||||
import log from "electron-log";
|
||||
|
||||
export class SteamService{
|
||||
|
||||
@@ -46,21 +48,27 @@ export class SteamService{
|
||||
}
|
||||
|
||||
public async getGameFolder(gameId: string, gameFolder?: string): Promise<string>{
|
||||
const steamPath = await this.getSteamPath();
|
||||
try{
|
||||
const steamPath = await this.getSteamPath();
|
||||
|
||||
let libraryFolders: any = path.join(steamPath, 'steamapps', 'libraryfolders.vdf');
|
||||
let libraryFolders: any = path.join(steamPath, 'steamapps', 'libraryfolders.vdf');
|
||||
|
||||
if(!this.utils.pathExist(libraryFolders)){ return null; }
|
||||
libraryFolders = parse(await readFile(libraryFolders, {encoding: 'utf-8'}));
|
||||
if(!(await pathExist(libraryFolders))){ return null; }
|
||||
libraryFolders = parse(await readFile(libraryFolders, {encoding: 'utf-8'}));
|
||||
|
||||
if(!libraryFolders.libraryfolders){ return null; }
|
||||
libraryFolders = libraryFolders.libraryfolders
|
||||
if(!libraryFolders.libraryfolders){ return null; }
|
||||
libraryFolders = libraryFolders.libraryfolders
|
||||
|
||||
for(const libKey in Object.keys(libraryFolders)){
|
||||
if(!libraryFolders[libKey] || !libraryFolders[libKey]["apps"]){ continue; }
|
||||
if(libraryFolders[libKey]["apps"][gameId]){ return path.join(libraryFolders[libKey]["path"], "steamapps", "common", gameFolder); };
|
||||
for(const libKey in Object.keys(libraryFolders)){
|
||||
if(!libraryFolders[libKey] || !libraryFolders[libKey]["apps"]){ continue; }
|
||||
if(libraryFolders[libKey]["apps"][gameId] != null){ return path.join(libraryFolders[libKey]["path"], "steamapps", "common", gameFolder); };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
catch(e){
|
||||
log.error(e);
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public openSteam(): Promise<boolean>{
|
||||
|
||||
@@ -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";
|
||||
@@ -16,7 +12,7 @@ export class UtilsService{
|
||||
|
||||
private static instance: UtilsService;
|
||||
|
||||
private assetsPath: string = '';
|
||||
private assetsPath: string = app.isPackaged ? path.join(process.resourcesPath, 'assets') : path.join(__dirname, '../../../assets');
|
||||
|
||||
private windows: Map<AppWindow, BrowserWindow> = new Map<AppWindow, BrowserWindow>();
|
||||
|
||||
@@ -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,111 @@
|
||||
import { getFoldersInFolder, pathExist } from "../helpers/fs.helpers";
|
||||
import path from "path";
|
||||
import { VersionLinkerAction, VersionLinkFolderAction, VersionUnlinkFolderAction } from "renderer/services/version-folder-linker.service";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { LocalMapsManagerService } from "./additional-content/local-maps-manager.service";
|
||||
import { BSLocalVersionService } from "./bs-local-version.service";
|
||||
import { FolderLinkerService, LinkOptions } from "./folder-linker.service";
|
||||
|
||||
export class VersionFolderLinkerService {
|
||||
|
||||
private static instance: VersionFolderLinkerService;
|
||||
|
||||
public static getInstance(): VersionFolderLinkerService{
|
||||
if(!VersionFolderLinkerService.instance){ VersionFolderLinkerService.instance = new VersionFolderLinkerService() }
|
||||
return VersionFolderLinkerService.instance;
|
||||
}
|
||||
|
||||
private readonly localVersion: BSLocalVersionService;
|
||||
private readonly folderLinker: FolderLinkerService;
|
||||
|
||||
private constructor(){
|
||||
this.localVersion = BSLocalVersionService.getInstance();
|
||||
this.folderLinker = FolderLinkerService.getInstance();
|
||||
}
|
||||
|
||||
private specialFolderOption(relativeFolder: string, options: LinkOptions): LinkOptions{
|
||||
if(relativeFolder.includes(LocalMapsManagerService.CUSTOM_LEVELS_FOLDER)){
|
||||
return {...options, intermediateFolder: LocalMapsManagerService.SHARED_MAPS_FOLDER};
|
||||
}
|
||||
|
||||
if(relativeFolder.includes("UserData")){
|
||||
return { ...options, backup: true };
|
||||
}
|
||||
|
||||
return options ?? {};
|
||||
}
|
||||
|
||||
private async isOtherVersionHaveFolderLinked(relativeFolder: string, ignorePath: string): Promise<boolean>{
|
||||
const versions = await this.localVersion.getInstalledVersions();
|
||||
const versionPaths = (await Promise.allSettled(versions.map(version => this.localVersion.getVersionPath(version)))).reduce((acc, res) => {
|
||||
if(res.status === "fulfilled"){
|
||||
acc.push(res.value);
|
||||
}
|
||||
return acc;
|
||||
}, [] as string[]);
|
||||
|
||||
for(const versionPath of versionPaths){
|
||||
const folderPath = this.relativeToFullPath(versionPath, relativeFolder);
|
||||
if(folderPath === ignorePath){ continue; }
|
||||
if(await this.folderLinker.isFolderSymlink(folderPath)){ console.log(folderPath); return true; }
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private relativeToFullPath(parentPath: string, relativePath: string): string{
|
||||
return path.join(parentPath, relativePath);
|
||||
}
|
||||
|
||||
|
||||
public async linkVersionFolder(action: VersionLinkerAction): Promise<boolean>{
|
||||
action.options = this.specialFolderOption(action.relativeFolder, action.options);
|
||||
const versionPath = await this.localVersion.getVersionPath(action.version);
|
||||
const folderPath = this.relativeToFullPath(versionPath, action.relativeFolder);
|
||||
return this.folderLinker.linkFolder(folderPath, action.options).catch(() => false).then(() => true)
|
||||
}
|
||||
|
||||
public async unlinkVersionFolder(action: VersionUnlinkFolderAction): Promise<boolean>{
|
||||
action.options = this.specialFolderOption(action.relativeFolder, action.options);
|
||||
|
||||
const versionPath = await this.localVersion.getVersionPath(action.version);
|
||||
const folderPath = this.relativeToFullPath(versionPath, action.relativeFolder);
|
||||
|
||||
action.options.moveContents = !(await this.isOtherVersionHaveFolderLinked(action.relativeFolder, folderPath));
|
||||
|
||||
return this.folderLinker.unlinkFolder(folderPath, action.options).catch(() => false).then(() => true);
|
||||
}
|
||||
|
||||
public async doAction(action: VersionLinkerAction): Promise<boolean>{
|
||||
if(action.type === "link"){
|
||||
return this.linkVersionFolder(action);
|
||||
}
|
||||
return this.unlinkVersionFolder(action as VersionUnlinkFolderAction);
|
||||
}
|
||||
|
||||
public async isFolderLinked(version: BSVersion, relativeFolder: string): Promise<boolean>{
|
||||
const versionPath = await this.localVersion.getVersionPath(version);
|
||||
const folderPath = this.relativeToFullPath(versionPath, relativeFolder);
|
||||
return this.folderLinker.isFolderSymlink(folderPath);
|
||||
}
|
||||
|
||||
public async getLinkedFolders(version: BSVersion, options?: { relative?: boolean }): Promise<string[]>{
|
||||
const versionPath = await this.localVersion.getVersionPath(version);
|
||||
const [rootFolders, beatSaberDataFolders] = await Promise.all([
|
||||
getFoldersInFolder(versionPath),
|
||||
getFoldersInFolder(path.join(versionPath, "Beat Saber_Data"))
|
||||
]);
|
||||
|
||||
const linkedFolder = await Promise.all([...rootFolders, ...beatSaberDataFolders].map(async folder => {
|
||||
if(!(await this.folderLinker.isFolderSymlink(folder))){ return null; }
|
||||
return folder;
|
||||
}));
|
||||
|
||||
if(options?.relative){
|
||||
return linkedFolder.filter(folder => folder).map(folder => path.relative(versionPath, folder));
|
||||
}
|
||||
|
||||
return linkedFolder.filter(folder => folder);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}))
|
||||
+7
-6
@@ -9,7 +9,7 @@ import { MapsDownloaderService } from "renderer/services/maps-downloader.service
|
||||
import { VariableSizeList } from "react-window"
|
||||
import { MapsRow } from "./maps-row.component"
|
||||
import { BehaviorSubject } from "rxjs"
|
||||
import { debounceTime, last, map, mergeMap, throttleTime } from "rxjs/operators"
|
||||
import { debounceTime, last, map, mergeMap } from "rxjs/operators"
|
||||
import { BeatSaverService } from "renderer/services/thrird-partys/beat-saver.service"
|
||||
import { OsDiagnosticService } from "renderer/services/os-diagnostic.service"
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook"
|
||||
@@ -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[]>([]));
|
||||
@@ -63,10 +64,10 @@ export const LocalMapsListPanel = forwardRef(({version, className, filter, searc
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
console.log(linked);
|
||||
|
||||
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 +80,7 @@ export const LocalMapsListPanel = forwardRef(({version, className, filter, searc
|
||||
subs.forEach(s => s.unsubscribe());
|
||||
mapsDownloader.removeOnMapDownloadedListener(loadMaps);
|
||||
}
|
||||
}, [isVisible, version]);
|
||||
}, [isVisible, version, linked]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
|
||||
+37
-9
@@ -15,6 +15,10 @@ 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 { VersionFolderLinkerService, VersionLinkerActionListener } from "renderer/services/version-folder-linker.service"
|
||||
|
||||
|
||||
type Props = {
|
||||
version?: BSVersion
|
||||
@@ -25,12 +29,14 @@ export function MapsPlaylistsPanel({version}: Props) {
|
||||
const mapsService = MapsManagerService.getInstance();
|
||||
const mapsDownloader = MapsDownloaderService.getInstance();
|
||||
const osDiagnostic = OsDiagnosticService.getInstance();
|
||||
const linker = VersionFolderLinkerService.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 +44,23 @@ export function MapsPlaylistsPanel({version}: Props) {
|
||||
|
||||
useEffect(() => {
|
||||
loadMapIsLinked();
|
||||
|
||||
const sub = mapsService.$mapsLinkingPending(version).pipe(debounceTime(50)).subscribe(setLinkingPending);
|
||||
|
||||
const onMapsLinked: VersionLinkerActionListener = (action, linked) => {
|
||||
if(!action.relativeFolder.includes(MapsManagerService.RELATIVE_MAPS_FOLDER)){ return; }
|
||||
loadMapIsLinked();
|
||||
}
|
||||
|
||||
linker.onVersionFolderLinked(onMapsLinked);
|
||||
linker.onVersionFolderUnlinked(onMapsLinked);
|
||||
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
linker.removeVersionFolderLinkedListener(onMapsLinked);
|
||||
linker.removeVersionFolderUnlinkedListener(onMapsLinked);
|
||||
}
|
||||
|
||||
}, [version]);
|
||||
|
||||
const loadMapIsLinked = () => {
|
||||
@@ -53,9 +76,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 +87,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 +112,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 +158,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,162 @@
|
||||
import Tippy from "@tippyjs/react";
|
||||
import { Variants } from "framer-motion";
|
||||
import { version } from "os";
|
||||
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 { BSVersionManagerService } from "renderer/services/bs-version-manager.service";
|
||||
import { ConfigurationService } from "renderer/services/configuration.service";
|
||||
import { IpcService } from "renderer/services/ipc.service";
|
||||
import { ModalComponent } from "renderer/services/modale.service";
|
||||
import { VersionFolderLinkerService, VersionLinkerActionType } from "renderer/services/version-folder-linker.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 linker = VersionFolderLinkerService.getInstance();
|
||||
const versionManager = BSVersionManagerService.getInstance();
|
||||
|
||||
const t = useTranslation();
|
||||
|
||||
const [folders, setFolders] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
setFolders(prev => Array.from(new Set([...prev, ...config.get<string[]>(SHARED_FOLDERS_KEY)]).values()));
|
||||
|
||||
linker.getLinkedFolders(data, {relative: true}).toPromise().then(linkedFolders => {
|
||||
setFolders(prev => Array.from(new Set([...prev, ...linkedFolders]).values()));
|
||||
});
|
||||
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if(!folders?.length){
|
||||
config.delete(SHARED_FOLDERS_KEY);
|
||||
return;
|
||||
}
|
||||
|
||||
config.set(SHARED_FOLDERS_KEY, folders);
|
||||
|
||||
}, [folders]);
|
||||
|
||||
const addFolder = async () => {
|
||||
const versionPath = await versionManager.getVersionPath(data).toPromise();
|
||||
const folder = await ipc.sendV2<{canceled: boolean, filePaths: string[]}, string>("choose-folder", {args: versionPath}).toPromise();
|
||||
|
||||
if(!folder || folder.canceled || !folder.filePaths?.length){ return; }
|
||||
|
||||
const relativeFolder = await ipc.sendV2<string>("full-version-path-to-relative", {args: {version: data, fullPath: folder.filePaths[0]}}).toPromise();
|
||||
|
||||
if(folders.includes(relativeFolder)){ return; }
|
||||
|
||||
setFolders(pre => [...pre, relativeFolder]);
|
||||
}
|
||||
|
||||
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} version={data} relativeFolder={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 = {
|
||||
version: BSVersion;
|
||||
relativeFolder: string;
|
||||
onDelete?: () => void;
|
||||
}
|
||||
|
||||
const FolderItem = ({version, relativeFolder, onDelete}: FolderProps) => {
|
||||
|
||||
const linker = VersionFolderLinkerService.getInstance();
|
||||
|
||||
const t = useTranslation();
|
||||
|
||||
const name = relativeFolder.split("\\").at(-1);
|
||||
const color = useThemeColor("first-color");
|
||||
|
||||
const variants: Variants = {
|
||||
hover: {rotate: 22.5},
|
||||
tap: {rotate: 45}
|
||||
};
|
||||
|
||||
const pending = useObservable(linker.isPending(version, relativeFolder));
|
||||
const processing = useObservable(linker.isProcessing(version, relativeFolder));
|
||||
const linkDisabled = pending || processing;
|
||||
|
||||
const [linked, setLinked] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadFolderIsLinked();
|
||||
}, [version, relativeFolder]);
|
||||
|
||||
const loadFolderIsLinked = () => {
|
||||
linker.isVersionFolderLinked(version, relativeFolder).toPromise().then(setLinked);
|
||||
}
|
||||
|
||||
const onClickLink = () => {
|
||||
if(linked){
|
||||
return linker.unlinkVersionFolder({
|
||||
version,
|
||||
relativeFolder,
|
||||
type: VersionLinkerActionType.Unlink,
|
||||
}).then(loadFolderIsLinked);
|
||||
}
|
||||
return linker.linkVersionFolder({
|
||||
version,
|
||||
relativeFolder,
|
||||
type: VersionLinkerActionType.Link,
|
||||
}).then(loadFolderIsLinked);
|
||||
}
|
||||
|
||||
const cancelLink = () => {
|
||||
linker.cancelAction(version, relativeFolder);
|
||||
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={relativeFolder}>{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>
|
||||
)
|
||||
|
||||
}
|
||||
@@ -16,6 +16,7 @@ export const NotificationItem = forwardRef(({resolver, notification}: {resolver?
|
||||
const renderImage = (() => {
|
||||
if(notification.type === NotificationType.SUCCESS){ return BeatRunningImg; }
|
||||
if(notification.type === NotificationType.WARNING){ return BeatWaitingImg; }
|
||||
if(notification.type === NotificationType.INFO){ return BeatWaitingImg; }
|
||||
if(notification.type === NotificationType.ERROR){ return BeatConflictImg; }
|
||||
return BeatImpatientImg;
|
||||
})();
|
||||
@@ -23,6 +24,7 @@ export const NotificationItem = forwardRef(({resolver, notification}: {resolver?
|
||||
const renderNeonColors = (() => {
|
||||
if(notification.type === NotificationType.SUCCESS){ return "bg-green-400 shadow-green-400"; }
|
||||
if(notification.type === NotificationType.WARNING){ return "bg-yellow-400 shadow-yellow-400"; }
|
||||
if(notification.type === NotificationType.INFO){ return "bg-blue-500 shadow-blue-500"; }
|
||||
if(notification.type === NotificationType.ERROR){ return "bg-red-500 shadow-red-500"; }
|
||||
return "bg-gray-800 shadow-gray-800 dark:bg-white dark:shadow-white";
|
||||
})();
|
||||
|
||||
@@ -14,7 +14,7 @@ export default function SettingColorChooser({color, onChange, pickerClassName}:
|
||||
<span className="z-[1] block h-full w-full border-2 border-white rounded-full" onClick={() => setColorVisible(!colorVisible)} style={{backgroundColor: color}}/>
|
||||
<AnimatePresence>
|
||||
{colorVisible &&
|
||||
<motion.div initial={{opacity: 0}} animate={{opacity: 1}} transition={{duration: .1}} exit={{opacity: 0}} className="fixed flex items-center justify-center translate-y-9 shadow-lg rounded-lg shadow-black">
|
||||
<motion.div initial={{opacity: 0}} animate={{opacity: 1}} transition={{duration: .1}} exit={{opacity: 0}} className="absolute flex items-center justify-center translate-y-9 shadow-lg rounded-lg shadow-black">
|
||||
<div className="absolute w-2/4 aspect-square rotate-45 bg-light-main-color-3 dark:bg-main-color-3 -translate-y-12"/>
|
||||
<HexColorPicker color={color} onChange={onChange} className={pickerClassName} />
|
||||
</motion.div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useObservable } from 'renderer/hooks/use-observable.hook';
|
||||
import { useThemeColor } from 'renderer/hooks/use-theme-color.hook';
|
||||
import { AudioPlayerService } from 'renderer/services/audio-player.service';
|
||||
@@ -17,9 +17,17 @@ export default function TitleBar({template = "index.html"} : {template: AppWindo
|
||||
const audio = AudioPlayerService.getInstance();
|
||||
|
||||
const volume = useObservable(audio.volume$, audio.volume);
|
||||
const playing = useObservable(audio.playing$);
|
||||
const color = useThemeColor("first-color");
|
||||
|
||||
const [previewVersion, setPreviewVersion] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
ipcService.send<string>("current-version").then(res => {
|
||||
if(res.data.toLocaleLowerCase().includes("alpha")){ return setPreviewVersion("ALPHA"); }
|
||||
if(res.data.toLocaleLowerCase().includes("beta")){ return setPreviewVersion("BETA"); }
|
||||
});
|
||||
})
|
||||
|
||||
const [maximized, setMaximized] = useState(false);
|
||||
|
||||
const closeWindow = () => {
|
||||
@@ -57,6 +65,7 @@ export default function TitleBar({template = "index.html"} : {template: AppWindo
|
||||
<div id="drag-region" className='grow basis-0 h-full'>
|
||||
<div id="window-title" className='pl-1'>
|
||||
<span className='text-gray-800 dark:text-gray-100 font-bold text-xs italic'>BSManager</span>
|
||||
{ previewVersion && <span className='bg-main-color-1 text-white dark:text-black dark:bg-white rounded-full ml-1 text-[10px] italic px-1 uppercase h-3.5 font-bold'>{previewVersion}</span> }
|
||||
</div>
|
||||
</div>
|
||||
<div id="window-controls" className="h-full flex shrink-0 items-center">
|
||||
@@ -64,7 +73,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>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { ChangeEvent, useEffect, useState } from "react";
|
||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { BSLauncherService, LaunchMods } from "renderer/services/bs-launcher.service";
|
||||
import { ConfigurationService } from "renderer/services/configuration.service";
|
||||
import { BSVersion } from "shared/bs-version.interface"
|
||||
@@ -10,15 +12,23 @@ type Props = {version: BSVersion};
|
||||
|
||||
export function LaunchSlide({version}: Props) {
|
||||
|
||||
const t = useTranslation();
|
||||
|
||||
const configService = ConfigurationService.getInstance();
|
||||
const bsLauncherService = BSLauncherService.getInstance();
|
||||
|
||||
const [oculusMode, setOculusMode] = useState(!!configService.get<boolean>(LaunchMods.OCULUS_MOD));
|
||||
const [desktopMode, setDesktopMode] = useState(!!configService.get<boolean>(LaunchMods.DESKTOP_MOD));
|
||||
const [debugMode, setDebugMode] = useState(!!configService.get<boolean>(LaunchMods.DEBUG_MOD));
|
||||
const [advancedLaunch, setAdvancedLaunch] = useState(false);
|
||||
const [additionalArgsString, setAdditionalArgsString] = useState<string>(configService.get<string>("additionnal-args") || "");
|
||||
|
||||
const launchState = useObservable(bsLauncherService.launchState$);
|
||||
|
||||
useEffect(() => {
|
||||
configService.set("additionnal-args", additionalArgsString);
|
||||
}, [additionalArgsString]);
|
||||
|
||||
const setMode = (mode: LaunchMods, value: boolean) => {
|
||||
if(mode === LaunchMods.DEBUG_MOD){
|
||||
setDebugMode(value);
|
||||
@@ -32,7 +42,12 @@ export function LaunchSlide({version}: Props) {
|
||||
configService.set(mode, value);
|
||||
}
|
||||
|
||||
const launch = () => bsLauncherService.launch(version, version.oculus ? false : oculusMode, desktopMode, debugMode)
|
||||
const handleAdditionalArgsChange = (e: ChangeEvent<HTMLInputElement>) => setAdditionalArgsString(() => e.target.value);
|
||||
|
||||
const launch = () => {
|
||||
const additionalArgs = advancedLaunch ? additionalArgsString.split(";").map(arg => arg.trim()).filter(arg => arg.length > 0) : undefined;
|
||||
bsLauncherService.launch(version, version.oculus ? false : oculusMode, desktopMode, debugMode, additionalArgs);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full shrink-0 items-center relative flex flex-col justify-start">
|
||||
@@ -41,8 +56,14 @@ export function LaunchSlide({version}: Props) {
|
||||
<LaunchModToogle infoText="pages.version-viewer.launch-mods.desktop-description" icon='desktop' onClick={() => setMode(LaunchMods.DESKTOP_MOD, !desktopMode)} active={desktopMode} text="pages.version-viewer.launch-mods.desktop"/>
|
||||
<LaunchModToogle infoText="pages.version-viewer.launch-mods.debug-description" icon='terminal' onClick={() => setMode(LaunchMods.DEBUG_MOD, !debugMode)} active={debugMode} text="pages.version-viewer.launch-mods.debug"/>
|
||||
</div>
|
||||
<div className="pt-4 w-2/3 flex flex-col items-center gap-3">
|
||||
<BsmButton className="rounded-full w-fit text-lg py-1 px-7 shadow-md shadow-black bg-light-main-color-2 dark:bg-main-color-2 text-gray-800 dark:text-white" text="pages.version-viewer.launch-mods.advanced-launch.button" withBar={false} onClick={e => {e.preventDefault(); setAdvancedLaunch(prev => !prev)}}/>
|
||||
<motion.div className="bg-light-main-color-2 dark:bg-main-color-2 h-9 rounded-full overflow-hidden flex items-center justify-center" initial={{width: "0px"}} animate={{width: advancedLaunch ? "100%" : "0px"}}>
|
||||
<input className="w-[calc(100%-12px)] h-[calc(100%-12px)] bg-light-main-color-1 dark:bg-main-color-1 text-black dark:text-white rounded-full outline-none text-center" type="text" placeholder={t("pages.version-viewer.launch-mods.advanced-launch.placeholder")} value={additionalArgsString} onChange={handleAdditionalArgsChange}/>
|
||||
</motion.div>
|
||||
</div>
|
||||
<div className='grow flex justify-center items-center'>
|
||||
<BsmButton onClick={launch} active={JSON.stringify(version) === JSON.stringify(launchState)} className='relative text-5xl text-gray-800 dark:text-gray-200 font-bold tracking-wide pt-1 pb-3 px-7 rounded-lg shadow-md italic shadow-black active:scale-90 transition-transform' text="misc.launch"/>
|
||||
<BsmButton onClick={launch} active={JSON.stringify(version) === JSON.stringify(launchState)} className='relative -translate-y-1/2 text-5xl text-gray-800 dark:text-gray-200 font-bold tracking-wide pt-1 pb-3 px-7 rounded-lg shadow-md italic shadow-black active:scale-90 transition-transform' text="misc.launch"/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -17,6 +17,7 @@ import { useInView } from "framer-motion";
|
||||
import { ModalExitCode, ModalService } from "renderer/services/modale.service";
|
||||
import { ModsDisclaimerModal } from "renderer/components/modal/modal-types/mods-disclaimer-modal.component";
|
||||
import { OsDiagnosticService } from "renderer/services/os-diagnostic.service";
|
||||
import { lt } from "semver";
|
||||
|
||||
export function ModsSlide({version, onDisclamerDecline}: {version: BSVersion, onDisclamerDecline: () => void}) {
|
||||
|
||||
@@ -68,7 +69,12 @@ export function ModsSlide({version, onDisclamerDecline}: {version: BSVersion, on
|
||||
|
||||
const installMods = () => {
|
||||
if(installing){ return; }
|
||||
modsManager.installMods(modsSelected, version).then(() => {
|
||||
const modsToInstall = modsSelected.filter(mod => {
|
||||
const corespondingMod = modsAvailable.get(mod.category).find(availabeMod => availabeMod._id === mod._id);
|
||||
if(lt(corespondingMod.version, mod.version)){ return false; }
|
||||
return true;
|
||||
})
|
||||
modsManager.installMods(modsToInstall, version).then(() => {
|
||||
loadMods();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -2,12 +2,19 @@ import { createRoot } from 'react-dom/client';
|
||||
import { HashRouter } from 'react-router-dom';
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import './index.css'
|
||||
import { IpcService } from './services/ipc.service';
|
||||
|
||||
const launcherContainer = document.getElementById('launcher');
|
||||
const oneclickDownloadMapContainer = document.getElementById('oneclick-download-map');
|
||||
const oneclickDownloadPlaylistContainer = document.getElementById('oneclick-download-playlist');
|
||||
const oneclickDownloadModelContainer = document.getElementById('oneclick-download-model');
|
||||
|
||||
const ipc = IpcService.getInstance();
|
||||
|
||||
window.onerror = (...data) => {
|
||||
ipc.sendLazy('log-error', {args: data});
|
||||
}
|
||||
|
||||
if(launcherContainer){
|
||||
import("./windows/Launcher").then(reactWindow => {
|
||||
createRoot(launcherContainer).render(<reactWindow.default/>);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -40,7 +40,6 @@ export function SettingsPage() {
|
||||
const modalService: ModalService = ModalService.getInsance();
|
||||
const downloaderService: BsDownloaderService = BsDownloaderService.getInstance();
|
||||
const progressBarService: ProgressBarService = ProgressBarService.getInstance();
|
||||
const authService: AuthUserService = AuthUserService.getInstance();
|
||||
const notificationService: NotificationService = NotificationService.getInstance();
|
||||
const i18nService: I18nService = I18nService.getInstance();
|
||||
const linkOpener: LinkOpenerService = LinkOpenerService.getInstance();
|
||||
@@ -49,7 +48,6 @@ export function SettingsPage() {
|
||||
const modelsManager = ModelsManagerService.getInstance();
|
||||
|
||||
const {firstColor, secondColor} = useThemeColor();
|
||||
const sessionExist = useObservable(authService.sessionExist$);
|
||||
|
||||
const themeItem: RadioItem[] = [
|
||||
{id: 0, text: "pages.settings.appearance.themes.dark", value: "dark" as ThemeConfig},
|
||||
@@ -110,11 +108,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);
|
||||
@@ -132,18 +131,12 @@ export function SettingsPage() {
|
||||
});
|
||||
}
|
||||
|
||||
const deleteSteamSession = () => {
|
||||
if(!sessionExist){ return; }
|
||||
authService.deleteSteamSession();
|
||||
notificationService.notifySuccess({title: "notifications.settings.steam.success.titles.logout", duration: 3000});
|
||||
};
|
||||
|
||||
|
||||
const toogleShowSupporters = () => {
|
||||
setShowSupporters(show => !show);
|
||||
}
|
||||
|
||||
const openSupportPage = () => linkOpener.open("https://mee6.gg/m/bsmanager");
|
||||
const openSupportPage = () => linkOpener.open("https://www.patreon.com/bsmanager");
|
||||
const openGithub = () => linkOpener.open("https://github.com/Zagrios/bs-manager");
|
||||
const openReportBug = () => linkOpener.open("https://github.com/Zagrios/bs-manager/issues/new?assignees=Zagrios&labels=bug&template=-bug--bug-report.md&title=%5BBUG%5D+%3A+");
|
||||
const openRequestFeatures = () => linkOpener.open("https://github.com/Zagrios/bs-manager/issues/new?assignees=Zagrios&labels=enhancement&template=-feat---feature-request.md&title=%5BFEAT.%5D+%3A+");
|
||||
@@ -195,10 +188,6 @@ export function SettingsPage() {
|
||||
<BsmButton className="inline-block grow-0 bg-transparent sticky h-full w-full top-20 right-20 !m-0 rounded-full p-1" onClick={() => nav(-1)} icon="close" withBar={false}/>
|
||||
</div>
|
||||
|
||||
<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}/>
|
||||
</SettingContainer>
|
||||
|
||||
<SettingContainer title="pages.settings.appearance.title" description="pages.settings.appearance.description">
|
||||
<div className="relative w-full h-8 bg-light-main-color-1 dark:bg-main-color-1 flex justify-center rounded-md py-1">
|
||||
<SettingColorChooser color={firstColor} onChange={setFirstColorSetting}/>
|
||||
@@ -293,7 +282,7 @@ export function SettingsPage() {
|
||||
|
||||
<SettingContainer title="pages.settings.patreon.title" description="pages.settings.patreon.description">
|
||||
<div className="flex gap-2">
|
||||
<BsmButton disabled color="#ef4444" className="flex w-fit rounded-md h-8 px-2 font-bold py-1 whitespace-nowrap !text-white" text="pages.settings.patreon.buttons.support" withBar={false} onClick={openSupportPage}/>
|
||||
<BsmButton color="#ef4444" className="flex w-fit rounded-md h-8 px-2 font-bold py-1 whitespace-nowrap !text-white" text="pages.settings.patreon.buttons.support" withBar={false} onClick={openSupportPage}/>
|
||||
<BsmButton className="flex w-fit rounded-md h-8 px-2 font-bold py-1 !text-white" withBar={false} text="pages.settings.patreon.buttons.supporters" color="#6c5ce7" onClick={toogleShowSupporters}/>
|
||||
</div>
|
||||
<SettingContainer className="mt-3" description="pages.settings.discord.description">
|
||||
|
||||
@@ -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})
|
||||
]}/>
|
||||
</>
|
||||
|
||||
@@ -40,8 +40,8 @@ export class BSLauncherService{
|
||||
|
||||
|
||||
|
||||
public launch(version: BSVersion, oculus: boolean, desktop: boolean, debug: boolean): Promise<NotificationResult|string>{
|
||||
const lauchOption: LauchOption = {debug, oculus, desktop, version};
|
||||
public launch(version: BSVersion, oculus: boolean, desktop: boolean, debug: boolean, additionalArgs?: string[]): Promise<NotificationResult|string>{
|
||||
const lauchOption: LauchOption = {debug, oculus, desktop, version, additionalArgs};
|
||||
if(this.launchState$.value){ return this.notificationService.notifyError({title: "notifications.bs-launch.errors.titles.BS_ALREADY_RUNNING"}); }
|
||||
this.launchState$.next(version);
|
||||
return this.ipcService.send<LaunchResult>("bs-launch.launch", {args: lauchOption}).then(res => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ModalExitCode, ModalService } from './modale.service';
|
||||
import { NotificationService } from './notification.service';
|
||||
import { ProgressBarService } from './progress-bar.service';
|
||||
import { EditVersionModal } from 'renderer/components/modal/modal-types/edit-version-modal.component';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
export class BSVersionManagerService {
|
||||
|
||||
@@ -113,4 +114,8 @@ export class BSVersionManagerService {
|
||||
})
|
||||
}
|
||||
|
||||
public getVersionPath(version: BSVersion): Observable<string>{
|
||||
return this.ipcService.sendV2("get-version-full-path", {args: version});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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,12 +11,15 @@ 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 { VersionFolderLinkerService, VersionLinkerActionType } from "./version-folder-linker.service";
|
||||
import equal from "fast-deep-equal";
|
||||
|
||||
export class MapsManagerService {
|
||||
|
||||
|
||||
|
||||
private static instance: MapsManagerService;
|
||||
|
||||
public static getInstance(): MapsManagerService{
|
||||
@@ -25,12 +28,14 @@ export class MapsManagerService {
|
||||
}
|
||||
|
||||
public static readonly REMEMBER_CHOICE_DELETE_MAP_KEY = "not-confirm-delete-map"
|
||||
public static readonly RELATIVE_MAPS_FOLDER = "Beat Saber_Data\\CustomLevels";
|
||||
|
||||
private readonly ipcService: IpcService;
|
||||
private readonly modal: ModalService;
|
||||
private readonly progressBar: ProgressBarService;
|
||||
private readonly notifications: NotificationService;
|
||||
private readonly config: ConfigurationService;
|
||||
private readonly linker: VersionFolderLinkerService;
|
||||
|
||||
private readonly lastLinkedVersion$: Subject<BSVersion> = new Subject();
|
||||
private readonly lastUnlinkedVersion$: Subject<BSVersion> = new Subject();
|
||||
@@ -41,63 +46,43 @@ export class MapsManagerService {
|
||||
this.progressBar = ProgressBarService.getInstance();
|
||||
this.notifications = NotificationService.getInstance();
|
||||
this.config = ConfigurationService.getInstance();
|
||||
this.linker = VersionFolderLinkerService.getInstance();
|
||||
}
|
||||
|
||||
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>{
|
||||
return this.linker.isVersionFolderLinked(version, MapsManagerService.RELATIVE_MAPS_FOLDER).toPromise();
|
||||
}
|
||||
|
||||
public async linkVersion(version: BSVersion): Promise<void>{
|
||||
public async linkVersion(version: BSVersion): Promise<boolean>{
|
||||
|
||||
const modalRes = await this.modal.openModal(LinkMapsModal);
|
||||
|
||||
if(modalRes.exitCode !== ModalExitCode.COMPLETED){ return; }
|
||||
if(modalRes.exitCode !== ModalExitCode.COMPLETED){ return Promise.resolve(false); }
|
||||
|
||||
const showProgressBar = this.progressBar.require();
|
||||
|
||||
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.linkVersionFolder({
|
||||
version,
|
||||
type: VersionLinkerActionType.Link,
|
||||
relativeFolder: MapsManagerService.RELATIVE_MAPS_FOLDER,
|
||||
options: { keepContents: !!modalRes.data }
|
||||
});
|
||||
}
|
||||
|
||||
public async unlinkVersion(version: BSVersion): Promise<void>{
|
||||
public async unlinkVersion(version: BSVersion): Promise<boolean>{
|
||||
|
||||
const modalRes = await this.modal.openModal(UnlinkMapsModal);
|
||||
|
||||
if(modalRes.exitCode !== ModalExitCode.COMPLETED){ return; }
|
||||
if(modalRes.exitCode !== ModalExitCode.COMPLETED){ return Promise.resolve(false); }
|
||||
|
||||
const showProgressBar = this.progressBar.require();
|
||||
|
||||
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.unlinkVersionFolder({
|
||||
version,
|
||||
type: VersionLinkerActionType.Unlink,
|
||||
relativeFolder: MapsManagerService.RELATIVE_MAPS_FOLDER,
|
||||
options: { keepContents: !!modalRes.data }
|
||||
});
|
||||
}
|
||||
|
||||
public async deleteMaps(maps: BsmLocalMap[], version?: BSVersion): Promise<boolean>{
|
||||
@@ -173,4 +158,10 @@ export class MapsManagerService {
|
||||
return this.lastUnlinkedVersion$.asObservable();
|
||||
}
|
||||
|
||||
public $mapsLinkingPending(version: BSVersion): Observable<boolean>{
|
||||
return this.linker.queue$.pipe(mergeMap(async queue => {
|
||||
return queue.some(q => q.relativeFolder.includes(MapsManagerService.RELATIVE_MAPS_FOLDER) && equal(q.version, version));
|
||||
}), 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,
|
||||
|
||||
@@ -53,6 +53,11 @@ export class NotificationService{
|
||||
return this.notify(notification);
|
||||
}
|
||||
|
||||
public notifyInfo(notification: Notification): Promise<NotificationResult|string>{
|
||||
notification.type = NotificationType.INFO;
|
||||
return this.notify(notification);
|
||||
}
|
||||
|
||||
public notifySystem(options: SystemNotificationOptions){
|
||||
this.ipc.sendLazy<SystemNotificationOptions>("notify-system", {args: options});
|
||||
}
|
||||
@@ -71,6 +76,7 @@ export enum NotificationType {
|
||||
SUCCESS = 0,
|
||||
WARNING = 1,
|
||||
ERROR = 2,
|
||||
INFO = 3,
|
||||
}
|
||||
|
||||
export interface NotificationAction {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
import { LinkOptions, UnlinkOptions } from "main/services/folder-linker.service";
|
||||
import { map, distinctUntilChanged, filter } from "rxjs/operators";
|
||||
import { BehaviorSubject, Observable } from "rxjs";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { IpcService } from "./ipc.service";
|
||||
import { ProgressBarService } from "./progress-bar.service";
|
||||
|
||||
export class VersionFolderLinkerService{
|
||||
|
||||
private static instance: VersionFolderLinkerService;
|
||||
|
||||
public static getInstance(): VersionFolderLinkerService{
|
||||
if(!VersionFolderLinkerService.instance){ VersionFolderLinkerService.instance = new VersionFolderLinkerService() }
|
||||
return VersionFolderLinkerService.instance;
|
||||
}
|
||||
|
||||
private readonly ipcService: IpcService;
|
||||
private readonly progress: ProgressBarService;
|
||||
|
||||
private readonly _queue$ = new BehaviorSubject<VersionLinkerAction[]>([]);
|
||||
|
||||
private readonly linkListeners = new Set<VersionLinkerActionListener>();
|
||||
private readonly unlinkListeners = new Set<VersionLinkerActionListener>();
|
||||
|
||||
private constructor(){
|
||||
this.ipcService = IpcService.getInstance();
|
||||
this.progress = ProgressBarService.getInstance();
|
||||
|
||||
this.currentAction$.pipe(filter(action => !!action)).subscribe(action => this.processAction(action));
|
||||
}
|
||||
|
||||
private async processAction(action: VersionLinkerAction){
|
||||
|
||||
let progressOpened = false;
|
||||
|
||||
if(!this.progress.isVisible){
|
||||
this.progress.showFake(.01, null, action.relativeFolder.split("\\").at(-1));
|
||||
progressOpened = true;
|
||||
}
|
||||
|
||||
const linked = await this.doAction(action).toPromise();
|
||||
|
||||
// Spécial notification
|
||||
|
||||
if(action.type === VersionLinkerActionType.Link){
|
||||
this.linkListeners.forEach(listener => listener(action, linked));
|
||||
}
|
||||
else{
|
||||
this.unlinkListeners.forEach(listener => listener(action, linked));
|
||||
}
|
||||
|
||||
if(progressOpened){
|
||||
this.progress.hide(true);
|
||||
}
|
||||
|
||||
const newArr = [...this._queue$.value];
|
||||
newArr.shift();
|
||||
this._queue$.next(newArr);
|
||||
|
||||
}
|
||||
|
||||
private doAction(action: VersionLinkerAction): Observable<boolean>{
|
||||
return this.ipcService.sendV2<boolean, VersionLinkerAction>("link-version-folder-action", { args: action });
|
||||
}
|
||||
|
||||
public linkVersionFolder(action: VersionLinkFolderAction): Promise<boolean>{
|
||||
const promise = new Promise<boolean>((resolve) => {
|
||||
|
||||
const callBack: VersionLinkerActionListener = (performedAction, linked) => {
|
||||
if(performedAction !== action){ return; }
|
||||
this.removeVersionFolderLinkedListener(callBack);
|
||||
resolve(linked);
|
||||
}
|
||||
|
||||
this.onVersionFolderLinked(callBack);
|
||||
});
|
||||
|
||||
this._queue$.next([...this._queue$.value, action]);
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
public unlinkVersionFolder(action: VersionUnlinkFolderAction): Promise<boolean>{
|
||||
const promise = new Promise<boolean>((resolve) => {
|
||||
|
||||
const callBack: VersionLinkerActionListener = (performedAction, linked) => {
|
||||
if(performedAction !== action){ return; }
|
||||
this.removeVersionFolderUnlinkedListener(callBack);
|
||||
resolve(linked);
|
||||
}
|
||||
|
||||
this.onVersionFolderUnlinked(callBack);
|
||||
});
|
||||
|
||||
this._queue$.next([...this._queue$.value, action]);
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
public onVersionFolderLinked(listener: VersionLinkerActionListener): void{
|
||||
this.linkListeners.add(listener);
|
||||
}
|
||||
|
||||
public removeVersionFolderLinkedListener(listener: VersionLinkerActionListener): void{
|
||||
this.linkListeners.delete(listener);
|
||||
}
|
||||
|
||||
public onVersionFolderUnlinked(listener: VersionLinkerActionListener): void{
|
||||
this.unlinkListeners.add(listener);
|
||||
}
|
||||
|
||||
public removeVersionFolderUnlinkedListener(listener: VersionLinkerActionListener): void{
|
||||
this.unlinkListeners.delete(listener);
|
||||
}
|
||||
|
||||
public cancelAction(version: BSVersion, relativeFolder: string): void{
|
||||
this._queue$.next(this._queue$.value.filter(a => a.version !== version || a.relativeFolder !== relativeFolder));
|
||||
}
|
||||
|
||||
public isVersionFolderLinked(version: BSVersion, relativeFolder: string): Observable<boolean>{
|
||||
return this.ipcService.sendV2("is-version-folder-linked", { args: { version, relativeFolder } });
|
||||
}
|
||||
|
||||
public isPending(version: BSVersion, relativeFolder: string): Observable<boolean> {
|
||||
return this.queue$.pipe(map(queue => queue.length > 0 && queue.some(action => action.version === version && action.relativeFolder === relativeFolder), distinctUntilChanged()));
|
||||
}
|
||||
|
||||
public isProcessing(version: BSVersion, relativeFolder: string): Observable<boolean> {
|
||||
return this.currentAction$.pipe(map(action => ( action && action.version === version && action.relativeFolder === relativeFolder)));
|
||||
}
|
||||
|
||||
public getLinkedFolders(version: BSVersion, options?: { relative?: boolean }): Observable<string[]>{
|
||||
return this.ipcService.sendV2("get-linked-folders", { args: { version, options } });
|
||||
}
|
||||
|
||||
public get currentAction$(): Observable<VersionLinkerAction>{
|
||||
return this._queue$.pipe(map(actions => actions.at(0)), distinctUntilChanged());
|
||||
}
|
||||
|
||||
public get queue$(): Observable<VersionLinkerAction[]>{
|
||||
return this._queue$.asObservable();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const enum VersionLinkerActionType {
|
||||
Link = "link",
|
||||
Unlink = "unlink"
|
||||
}
|
||||
|
||||
export interface VersionLinkerAction {
|
||||
version: BSVersion;
|
||||
relativeFolder: string;
|
||||
type: VersionLinkerActionType
|
||||
options?: LinkOptions
|
||||
}
|
||||
|
||||
export interface VersionLinkFolderAction extends VersionLinkerAction {
|
||||
type: VersionLinkerActionType.Link
|
||||
}
|
||||
|
||||
export interface VersionUnlinkFolderAction extends VersionLinkerAction {
|
||||
type: VersionLinkerActionType.Unlink
|
||||
options?: UnlinkOptions
|
||||
}
|
||||
|
||||
export type VersionLinkerActionListener = (action: VersionLinkerAction, linked: boolean) => void;
|
||||
@@ -1,8 +1,9 @@
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
|
||||
export interface LauchOption {
|
||||
version: BSVersion,
|
||||
oculus: boolean,
|
||||
desktop: boolean,
|
||||
debug: boolean
|
||||
version: BSVersion,
|
||||
oculus: boolean,
|
||||
desktop: boolean,
|
||||
debug: boolean,
|
||||
additionalArgs?: string[]
|
||||
}
|
||||
Reference in New Issue
Block a user