[feature-155] start share folder ui

This commit is contained in:
MathieuG-P
2023-03-08 01:13:13 +01:00
parent a144a308f6
commit 646ec820a5
9 changed files with 154 additions and 14 deletions
+22
View File
@@ -6,6 +6,11 @@ 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';
const ipc = IpcService.getInstance();
ipcMain.on('bs-version.get-version-dict', (event, req: IpcRequest<void>) => {
BSVersionLibService.getInstance().getAvailableVersions().then(versions => {
@@ -44,3 +49,20 @@ 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) => {
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));
});
+21 -4
View File
@@ -4,10 +4,15 @@ 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';
// 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 +37,10 @@ ipcMain.on('new-window', async (event, request: IpcRequest<string>) => {
shell.openExternal(request.args);
});
ipcMain.on('choose-folder', async (event, request: IpcRequest<void>) => {
dialog.showOpenDialog({properties: ['openDirectory'],}).then(res => {
UtilsService.getInstance().ipcSend(request.responceChannel, {success: true, data: res});
});
ipc.on('choose-folder', async (req: IpcRequest<string>, reply) => {
reply(
from(dialog.showOpenDialog({properties: ['openDirectory'], defaultPath: req.args ?? ""}))
)
});
ipcMain.on("window.progression", async (event, request: IpcRequest<number>) => {
@@ -71,4 +76,16 @@ ipcMain.on("open-steam", async (event, request: IpcRequest<void>) => {
}).catch((e) => {
utils.ipcSend(request.responceChannel, {success: false, error: e});
});
});
ipc.on("is-folder-symlink", async (req: IpcRequest<string>, reply) => {
const utils = UtilsService.getInstance();
const promise = new Promise<boolean>(async resolve => {
if(!utils.pathExist(req.args)){ return resolve(false); }
lstat(req.args, (err, stats) => {
if(err){ return resolve(false); }
resolve(stats.isSymbolicLink());
});
});
reply(from(promise));
});
@@ -0,0 +1,89 @@
import { useEffect, useState } from "react";
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 { ConfigurationService } from "renderer/services/configuration.service";
import { IpcService } from "renderer/services/ipc.service";
import { ModalComponent, ModalExitCode } from "renderer/services/modale.service";
import { BSVersion } from "shared/bs-version.interface";
export const ShareFoldersModal: ModalComponent<void, BSVersion> = ({ resolver, data }) => {
const config = ConfigurationService.getInstance();
const ipc = IpcService.getInstance();
const [folders, setFolders] = useState<{path: string, linked: boolean}[]>([]);
const versionPath = useObservable(ipc.sendV2<string>("get-version-full-path", {args: data}), "");
useEffect(() => {
const defaultFolders = config.get<string[]>("default-shared-folders");
const promises = Promise.allSettled(defaultFolders.map(async (folder) => {
const folderPath = await ipc.sendV2<string>("relative-version-path-to-full", {args: {version: data, relative: folder}}).toPromise();
const isSymlink = await ipc.sendV2<boolean>("is-folder-symlink", {args: folderPath}).toPromise();
return {path: folderPath, linked: isSymlink};
}));
promises.then((res) => {
setFolders(() => res.reduce((acc, curent) => {
if(curent.status !== "fulfilled"){ return acc; }
acc.push(curent.value);
return acc;
}, [] as {path: string, linked: boolean}[]));
});
}, []);
const addFolder = async () => {
const folder = await ipc.sendV2<{canceled: boolean, filePaths: string[]}, string>("choose-folder", {args: versionPath}).toPromise();
if(!folder || folder.canceled || !folder.filePaths?.length){ return; }
const isSymlink = await ipc.sendV2<boolean>("is-folder-symlink", {args: folder.filePaths[0]}).toPromise();
const linkedFolder = {path: folder.filePaths[0], linked: isSymlink};
const newFolders = [...folders, linkedFolder];
const filtredMap = new Map<string, {path: string, linked: boolean}>(newFolders.map((folder) => [folder.path, folder]));
setFolders(() => Array.from(filtredMap.values()));
}
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">Share Folders</h1>
<p className="my-3">Shared folders allow to share their content with other BeatSaber versions. Please note that the deletion of content is also shared</p>
<ul className="flex flex-col gap-1 mb-2 h-[300px] max-h-[300px] overflow-scroll scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900 px-1">
{folders.map((folder, index) => (
<FolderItem key={index} path={folder.path} linked={folder.linked} onDelete={() => {removeFolder(index)}}/>
))}
</ul>
<div className="w-full h-12 bg-main-color-1 rounded-md flex justify-center items-center cursor-pointer hover:bg-main-color-3" onClick={addFolder}>
<BsmIcon className="aspect-square h-8" icon="add"/>
<span className="font-bold">Add folder</span>
</div>
</form>
)
}
type FolderProps = {
path: string;
linked: boolean;
onDelete?: () => void;
}
const FolderItem = ({path, linked, onDelete}: FolderProps) => {
const name = path.split("\\").at(-1);
return (
<li className="w-full h-12 bg-main-color-1 rounded-md shrink-0 flex flex-row items-center justify-between px-2 font-bold">
<span className="cursor-help" title={path}>{name}</span>
<div>
<BsmButton className="aspect-square h-7 rounded-md p-1 bg-main-color-3" icon="trash" withBar={false} onClick={e => {e.preventDefault(); onDelete?.()}}/>
</div>
</li>
)
}
@@ -4,9 +4,14 @@ export const defaultConfiguration: {[key in DefaultConfigKey]: any} = {
"theme": "os",
"language": window.navigator.language.length <= 2 ? `${window.navigator.language}-${window.navigator.language.toLocaleUpperCase()}` : window.navigator.language,
"supported_languages": ["en-US", "en-EN", "fr-FR", "es-ES"],
"default_mods": ["SongCore", "WhyIsThereNoLeaderboard", "BeatSaverDownloader", "BeatSaverVoting", "PlaylistManager"]
"default_mods": ["SongCore", "WhyIsThereNoLeaderboard", "BeatSaverDownloader", "BeatSaverVoting", "PlaylistManager"],
"default-shared-folders": [
"Beat Saber_Data\\CustomLevels",
"Beat Saber_Data\\CustomWIPLevels",
"DLC"
]
}
export type DefaultConfigKey = "first-color" | "second-color" | "theme" | "language" | "supported_languages" | "default_mods";
export type DefaultConfigKey = "first-color" | "second-color" | "theme" | "language" | "supported_languages" | "default_mods" | "default-shared-folders";
export type ThemeConfig = "dark" | "light" | "os"
@@ -54,15 +54,15 @@ export function AvailableVersionsList() {
if(modalRes.exitCode !== ModalExitCode.COMPLETED){ return; }
const folderRes = await ipc.send<{canceled: boolean, filePaths: string[]}>("choose-folder");
const folderRes = await ipc.sendV2<{canceled: boolean, filePaths: string[]}>("choose-folder").toPromise();
if(!folderRes.success || folderRes.data.canceled || !folderRes.data.filePaths?.length){ return; }
if(!folderRes || folderRes.canceled || !folderRes.filePaths?.length){ return; }
notification.notifySuccess({title: "notifications.bs-import-version.success.start-import.title", desc: "notifications.bs-import-version.success.start-import.desc", duration: 6_000});
progressBar.showFake(.008);
const toImport = folderRes.data.filePaths.at(0);
const toImport = folderRes.filePaths.at(0);
const imported = await installer.importVersion(toImport);
@@ -110,11 +110,12 @@ export function SettingsPage() {
modalService.openModal(InstallationFolderModal).then(async res => {
if(res.exitCode !== ModalExitCode.COMPLETED){ return; }
const fileChooserRes = await ipcService.send<{canceled: boolean, filePaths: string[]}>("choose-folder");
if(fileChooserRes.success && !fileChooserRes.data.canceled && fileChooserRes.data.filePaths?.length){
const fileChooserRes = await ipcService.sendV2<{canceled: boolean, filePaths: string[]}>("choose-folder").toPromise();
if(!fileChooserRes.canceled && fileChooserRes.filePaths?.length){
progressBarService.showFake(.008);
downloaderService.setInstallationFolder(fileChooserRes.data.filePaths[0]).then(res => {
downloaderService.setInstallationFolder(fileChooserRes.filePaths[0]).then(res => {
setTimeout(() => {
progressBarService.complete();
setTimeout(() => progressBarService.hide(true), 1000);
@@ -15,6 +15,7 @@ import { LaunchSlide } from 'renderer/components/version-viewer/slides/launch/la
import { ModsSlide } from 'renderer/components/version-viewer/slides/mods/mods-slide.component';
import { UninstallModal } from 'renderer/components/modal/modal-types/uninstall-modal.component';
import { MapsPlaylistsPanel } from 'renderer/components/maps-mangement-components/maps-playlists-panel.component';
import { ShareFoldersModal } from 'renderer/components/modal/modal-types/share-folders-modal.component';
export function VersionViewer() {
@@ -66,6 +67,10 @@ export function VersionViewer() {
setCurrentTabIndex(() => 0);
}
const openShareFolderModal = () => {
modalService.openModal(ShareFoldersModal, state);
}
return (
<>
@@ -87,6 +92,7 @@ export function VersionViewer() {
((!state.steam && !state.oculus) && {text: "pages.version-viewer.dropdown.verify-files", icon: "task", onClick: verifyFiles}),
((!state.steam && !state.oculus) && {text: "pages.version-viewer.dropdown.edit", icon: "edit", onClick: edit}),
(!state.oculus && {text: "pages.version-viewer.dropdown.clone", icon: "copy", onClick: clone}),
{text: "Share folders", icon: "link", onClick: openShareFolderModal},
((!state.steam && !state.oculus) && {text: "pages.version-viewer.dropdown.uninstall", icon:"trash", onClick: uninstall})
]}/>
</>
+1 -1
View File
@@ -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 -1
View File
@@ -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,