From 646ec820a50be99149dbc757574a530acd656466 Mon Sep 17 00:00:00 2001 From: MathieuG-P <40181755+Zagrios@users.noreply.github.com> Date: Wed, 8 Mar 2023 01:13:13 +0100 Subject: [PATCH] [feature-155] start share folder ui --- src/main/ipcs/bs-version-ipcs.ts | 22 +++++ src/main/ipcs/os-controls-ipcs.ts | 25 +++++- .../share-folders-modal.component.tsx | 89 +++++++++++++++++++ .../config/default-configuration.config.ts | 9 +- .../available-versions-list.components.tsx | 6 +- .../pages/settings-page.component.tsx | 7 +- .../pages/version-viewer.component.tsx | 6 ++ src/renderer/services/ipc.service.ts | 2 +- src/renderer/services/modale.service.ts | 2 +- 9 files changed, 154 insertions(+), 14 deletions(-) create mode 100644 src/renderer/components/modal/modal-types/share-folders-modal.component.tsx diff --git a/src/main/ipcs/bs-version-ipcs.ts b/src/main/ipcs/bs-version-ipcs.ts index 1fb0cfd3..e711ac01 100644 --- a/src/main/ipcs/bs-version-ipcs.ts +++ b/src/main/ipcs/bs-version-ipcs.ts @@ -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) => { 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, 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)); +}); + + diff --git a/src/main/ipcs/os-controls-ipcs.ts b/src/main/ipcs/os-controls-ipcs.ts index d84f209f..1c42e347 100644 --- a/src/main/ipcs/os-controls-ipcs.ts +++ b/src/main/ipcs/os-controls-ipcs.ts @@ -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) => { shell.openExternal(request.args); }); -ipcMain.on('choose-folder', async (event, request: IpcRequest) => { - dialog.showOpenDialog({properties: ['openDirectory'],}).then(res => { - UtilsService.getInstance().ipcSend(request.responceChannel, {success: true, data: res}); - }); +ipc.on('choose-folder', async (req: IpcRequest, reply) => { + reply( + from(dialog.showOpenDialog({properties: ['openDirectory'], defaultPath: req.args ?? ""})) + ) }); ipcMain.on("window.progression", async (event, request: IpcRequest) => { @@ -71,4 +76,16 @@ ipcMain.on("open-steam", async (event, request: IpcRequest) => { }).catch((e) => { utils.ipcSend(request.responceChannel, {success: false, error: e}); }); +}); + +ipc.on("is-folder-symlink", async (req: IpcRequest, reply) => { + const utils = UtilsService.getInstance(); + const promise = new Promise(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)); }); \ No newline at end of file diff --git a/src/renderer/components/modal/modal-types/share-folders-modal.component.tsx b/src/renderer/components/modal/modal-types/share-folders-modal.component.tsx new file mode 100644 index 00000000..9a2b33ac --- /dev/null +++ b/src/renderer/components/modal/modal-types/share-folders-modal.component.tsx @@ -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 = ({ resolver, data }) => { + + const config = ConfigurationService.getInstance(); + const ipc = IpcService.getInstance(); + + const [folders, setFolders] = useState<{path: string, linked: boolean}[]>([]); + const versionPath = useObservable(ipc.sendV2("get-version-full-path", {args: data}), ""); + + useEffect(() => { + + const defaultFolders = config.get("default-shared-folders"); + + const promises = Promise.allSettled(defaultFolders.map(async (folder) => { + const folderPath = await ipc.sendV2("relative-version-path-to-full", {args: {version: data, relative: folder}}).toPromise(); + const isSymlink = await ipc.sendV2("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("is-folder-symlink", {args: folder.filePaths[0]}).toPromise(); + const linkedFolder = {path: folder.filePaths[0], linked: isSymlink}; + + const newFolders = [...folders, linkedFolder]; + const filtredMap = new Map(newFolders.map((folder) => [folder.path, folder])); + setFolders(() => Array.from(filtredMap.values())); + } + + const removeFolder = (index: number) => { + setFolders((prev) => prev.filter((_, i) => i !== index)); + } + + return ( +
+

Share Folders

+

Shared folders allow to share their content with other BeatSaber versions. Please note that the deletion of content is also shared

+
    + {folders.map((folder, index) => ( + {removeFolder(index)}}/> + ))} +
+
+ + Add folder +
+
+ ) +} + +type FolderProps = { + path: string; + linked: boolean; + onDelete?: () => void; +} + +const FolderItem = ({path, linked, onDelete}: FolderProps) => { + + const name = path.split("\\").at(-1); + + return ( +
  • + {name} +
    + {e.preventDefault(); onDelete?.()}}/> +
    +
  • + ) + +} diff --git a/src/renderer/config/default-configuration.config.ts b/src/renderer/config/default-configuration.config.ts index a6a9eed9..6e5875dc 100644 --- a/src/renderer/config/default-configuration.config.ts +++ b/src/renderer/config/default-configuration.config.ts @@ -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" diff --git a/src/renderer/pages/available-versions-list.components.tsx b/src/renderer/pages/available-versions-list.components.tsx index 979952f1..8b06ca57 100644 --- a/src/renderer/pages/available-versions-list.components.tsx +++ b/src/renderer/pages/available-versions-list.components.tsx @@ -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); diff --git a/src/renderer/pages/settings-page.component.tsx b/src/renderer/pages/settings-page.component.tsx index f99d6183..9f68c2aa 100644 --- a/src/renderer/pages/settings-page.component.tsx +++ b/src/renderer/pages/settings-page.component.tsx @@ -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); diff --git a/src/renderer/pages/version-viewer.component.tsx b/src/renderer/pages/version-viewer.component.tsx index ab5b9a19..1054acbc 100644 --- a/src/renderer/pages/version-viewer.component.tsx +++ b/src/renderer/pages/version-viewer.component.tsx @@ -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}) ]}/> diff --git a/src/renderer/services/ipc.service.ts b/src/renderer/services/ipc.service.ts index 2f9ee7c1..af725b7d 100644 --- a/src/renderer/services/ipc.service.ts +++ b/src/renderer/services/ipc.service.ts @@ -53,7 +53,7 @@ export class IpcService { public sendV2(channel: string, request?: IpcRequest, defaultValue?: T): Observable{ 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`; diff --git a/src/renderer/services/modale.service.ts b/src/renderer/services/modale.service.ts index 82a4ba95..c63fd60e 100644 --- a/src/renderer/services/modale.service.ts +++ b/src/renderer/services/modale.service.ts @@ -51,7 +51,7 @@ export class ModalService{ } -export type ModalComponent = ({resolver, data}: {resolver : (x: ModalResponse) => void, data?: K}) => JSX.Element; +export type ModalComponent = ({resolver, data}: {resolver : (x: ModalResponse) => void, data?: Receive}) => JSX.Element; export const enum ModalExitCode { NO_CHOICE = -1,