From 83fcbf6d043a91e116cc7948b1b3efbf7b6e3aad Mon Sep 17 00:00:00 2001 From: MathieuG-P <40181755+Zagrios@users.noreply.github.com> Date: Mon, 20 Mar 2023 22:16:38 +0100 Subject: [PATCH] [hotfix] move instead of copy if no other version use the linked folder --- src/main/ipcs/bs-version-ipcs.ts | 22 ++- src/main/ipcs/os-controls-ipcs.ts | 11 -- src/main/services/bs-local-version.service.ts | 1 + src/main/services/folder-linker.service.ts | 16 +- .../services/version-folder-linker.service.ts | 111 +++++++++++ .../local-maps-list-panel.component.tsx | 4 +- .../maps-playlists-panel.component.tsx | 17 +- .../share-folders-modal.component.tsx | 88 ++++----- .../services/bs-version-manager.service.ts | 5 + .../services/folder-linker.service.ts | 185 ------------------ src/renderer/services/maps-manager.service.ts | 46 +++-- .../services/version-folder-linker.service.ts | 167 ++++++++++++++++ 12 files changed, 387 insertions(+), 286 deletions(-) create mode 100644 src/main/services/version-folder-linker.service.ts delete mode 100644 src/renderer/services/folder-linker.service.ts create mode 100644 src/renderer/services/version-folder-linker.service.ts diff --git a/src/main/ipcs/bs-version-ipcs.ts b/src/main/ipcs/bs-version-ipcs.ts index bbd00224..43637528 100644 --- a/src/main/ipcs/bs-version-ipcs.ts +++ b/src/main/ipcs/bs-version-ipcs.ts @@ -14,6 +14,8 @@ import { FolderLinkerService, LinkOptions } from '../services/folder-linker.serv 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(); @@ -57,9 +59,7 @@ ipcMain.on("bs-version.clone", async (event, req: IpcRequest<{version: BSVersion ipc.on("get-version-full-path", async (req: IpcRequest, reply) => { const localVersions = BSLocalVersionService.getInstance(); - reply(from( - localVersions.getVersionPath(req.args) - )); + reply(from( localVersions.getVersionPath(req.args) )); }); ipc.on("relative-version-path-to-full", async (req: IpcRequest<{version: BSVersion, relative: string}>, reply) => { @@ -80,9 +80,9 @@ ipc.on("full-version-path-to-relative", async (req: IpcRequest<{version: BSVersi reply(from(promise)); }); -ipc.on("get-linked-folders", async (req: IpcRequest, reply) => { - const localVersions = BSLocalVersionService.getInstance(); - reply(from(localVersions.getLinkedFolders(req.args))); +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) => { @@ -121,6 +121,16 @@ ipc.on("link-folder", async (req: IpcRequest<{ folder: string, options?: LinkOpt }); +ipc.on("link-version-folder-action", async (req: IpcRequest, 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 ??= {}; diff --git a/src/main/ipcs/os-controls-ipcs.ts b/src/main/ipcs/os-controls-ipcs.ts index ee08eaac..aec59b72 100644 --- a/src/main/ipcs/os-controls-ipcs.ts +++ b/src/main/ipcs/os-controls-ipcs.ts @@ -77,15 +77,4 @@ 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 promise = new Promise(async resolve => { - if(!(await pathExist(req.args))){ return resolve(false); } - lstat(req.args, (err, stats) => { - if(err){ return resolve(false); } - resolve(stats.isSymbolicLink()); - }); - }); - reply(from(promise)); }); \ No newline at end of file diff --git a/src/main/services/bs-local-version.service.ts b/src/main/services/bs-local-version.service.ts index b4822b4c..d0360f8f 100644 --- a/src/main/services/bs-local-version.service.ts +++ b/src/main/services/bs-local-version.service.ts @@ -96,6 +96,7 @@ export class BSLocalVersionService{ public async getVersionPath(version: BSVersion): Promise{ 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) diff --git a/src/main/services/folder-linker.service.ts b/src/main/services/folder-linker.service.ts index c0cc70dd..c0fe772d 100644 --- a/src/main/services/folder-linker.service.ts +++ b/src/main/services/folder-linker.service.ts @@ -69,7 +69,7 @@ export class FolderLinkerService { return symlink(sharedPath, folderPath, "junction"); } - public async unlinkFolder(folderPath: string, options?: LinkOptions): Promise { + public async unlinkFolder(folderPath: string, options?: UnlinkOptions): Promise { if(!(await this.isFolderSymlink(folderPath))){ return; } await unlinkPath(folderPath); @@ -82,10 +82,14 @@ export class FolderLinkerService { 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 }); } @@ -105,5 +109,9 @@ export class FolderLinkerService { export interface LinkOptions { keepContents?: boolean, intermediateFolder?: string, - backup?: boolean -} \ No newline at end of file + backup?: boolean, +} + +export interface UnlinkOptions extends LinkOptions { + moveContents?: boolean, +}; \ No newline at end of file diff --git a/src/main/services/version-folder-linker.service.ts b/src/main/services/version-folder-linker.service.ts new file mode 100644 index 00000000..f82024bd --- /dev/null +++ b/src/main/services/version-folder-linker.service.ts @@ -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{ + 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{ + 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{ + 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{ + if(action.type === "link"){ + return this.linkVersionFolder(action); + } + return this.unlinkVersionFolder(action as VersionUnlinkFolderAction); + } + + public async isFolderLinked(version: BSVersion, relativeFolder: string): Promise{ + 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{ + 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); + } + +} \ No newline at end of file diff --git a/src/renderer/components/maps-mangement-components/local-maps-list-panel.component.tsx b/src/renderer/components/maps-mangement-components/local-maps-list-panel.component.tsx index b3b69704..c9521b0a 100644 --- a/src/renderer/components/maps-mangement-components/local-maps-list-panel.component.tsx +++ b/src/renderer/components/maps-mangement-components/local-maps-list-panel.component.tsx @@ -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" @@ -64,6 +64,8 @@ export const LocalMapsListPanel = forwardRef(({version, className, filter, searc useEffect(() => { + console.log(linked); + if(isVisible){ loadMaps(); mapsDownloader.addOnMapDownloadedListener((map, targerVersion) => { diff --git a/src/renderer/components/maps-mangement-components/maps-playlists-panel.component.tsx b/src/renderer/components/maps-mangement-components/maps-playlists-panel.component.tsx index 2162e71d..dddebbe5 100644 --- a/src/renderer/components/maps-mangement-components/maps-playlists-panel.component.tsx +++ b/src/renderer/components/maps-mangement-components/maps-playlists-panel.component.tsx @@ -17,7 +17,8 @@ import { BsmIcon } from "../svgs/bsm-icon.component" import { useTranslation } from "renderer/hooks/use-translation.hook" import { LinkButton } from "./link-button.component" import { debounceTime } from "rxjs/operators" -import { FolderLinkerService } from "renderer/services/folder-linker.service" +import { VersionFolderLinkerService, VersionLinkerActionListener } from "renderer/services/version-folder-linker.service" + type Props = { version?: BSVersion @@ -28,7 +29,7 @@ export function MapsPlaylistsPanel({version}: Props) { const mapsService = MapsManagerService.getInstance(); const mapsDownloader = MapsDownloaderService.getInstance(); const osDiagnostic = OsDiagnosticService.getInstance(); - const linker = FolderLinkerService.getInstance(); + const linker = VersionFolderLinkerService.getInstance(); const [tabIndex, setTabIndex] = useState(0); const [mapFilter, setMapFilter] = useState({}); @@ -46,18 +47,18 @@ export function MapsPlaylistsPanel({version}: Props) { const sub = mapsService.$mapsLinkingPending(version).pipe(debounceTime(50)).subscribe(setLinkingPending); - const onMapsLinked = (folder: string) => { - if(!folder.includes("CustomLevels")){ return; } + const onMapsLinked: VersionLinkerActionListener = (action, linked) => { + if(!action.relativeFolder.includes(MapsManagerService.RELATIVE_MAPS_FOLDER)){ return; } loadMapIsLinked(); } - linker.onFolderLinked(onMapsLinked); - linker.onFolderUnlinked(onMapsLinked); + linker.onVersionFolderLinked(onMapsLinked); + linker.onVersionFolderUnlinked(onMapsLinked); return () => { sub.unsubscribe(); - linker.removeOnFolderLinked(onMapsLinked); - linker.removeOnFolderUnlinked(onMapsLinked); + linker.removeVersionFolderLinkedListener(onMapsLinked); + linker.removeVersionFolderUnlinkedListener(onMapsLinked); } }, [version]); 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 index ed999188..2467c279 100644 --- a/src/renderer/components/modal/modal-types/share-folders-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/share-folders-modal.component.tsx @@ -1,5 +1,6 @@ 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"; @@ -8,10 +9,11 @@ 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 { FolderLinkerService } from "renderer/services/folder-linker.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 = ({ data }) => { @@ -20,32 +22,18 @@ export const ShareFoldersModal: ModalComponent = ({ data }) => const config = ConfigurationService.getInstance(); const ipc = IpcService.getInstance(); + const linker = VersionFolderLinkerService.getInstance(); + const versionManager = BSVersionManagerService.getInstance(); const t = useTranslation(); const [folders, setFolders] = useState([]); - const versionPath = useObservable(ipc.sendV2("get-version-full-path", {args: data})); useEffect(() => { - const defaultFolders = config.get(SHARED_FOLDERS_KEY); + setFolders(prev => Array.from(new Set([...prev, ...config.get(SHARED_FOLDERS_KEY)]).values())); - const promises = Promise.allSettled(defaultFolders.map(async (folder) => { - return ipc.sendV2("relative-version-path-to-full", {args: {version: data, relative: folder}}).toPromise(); - })); - - promises.then((res) => { - - const fullPathFolders = res.reduce((acc, curent) => { - if(curent.status !== "fulfilled"){ return acc; } - acc.push(curent.value); - return acc; - }, []); - - setFolders(prev => Array.from(new Set([...prev, ...fullPathFolders]).values())); - }); - - ipc.sendV2("get-linked-folders", {args: data}).toPromise().then(linkedFolders => { + linker.getLinkedFolders(data, {relative: true}).toPromise().then(linkedFolders => { setFolders(prev => Array.from(new Set([...prev, ...linkedFolders]).values())); }); @@ -57,31 +45,22 @@ export const ShareFoldersModal: ModalComponent = ({ data }) => config.delete(SHARED_FOLDERS_KEY); return; } - - const promises = Promise.allSettled(folders.map(async folder => { - return ipc.sendV2("full-version-path-to-relative", {args: {version: data, fullPath: folder}}).toPromise(); - })); - - promises.then((res) => { - const sharedFolders = res.reduce((acc, curent) => { - if(curent.status !== "fulfilled"){ return acc; } - acc.push(curent.value); - return acc; - }, [] as string[]); - config.set(SHARED_FOLDERS_KEY, sharedFolders); - }); + + 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 linkedFolder = folder.filePaths[0]; - if(folders.includes(linkedFolder)){ return; } + const relativeFolder = await ipc.sendV2("full-version-path-to-relative", {args: {version: data, fullPath: folder.filePaths[0]}}).toPromise(); - setFolders(pre => [...pre, linkedFolder]); + if(folders.includes(relativeFolder)){ return; } + + setFolders(pre => [...pre, relativeFolder]); } const removeFolder = (index: number) => { @@ -94,7 +73,7 @@ export const ShareFoldersModal: ModalComponent = ({ data }) =>

{t("modals.shared-folders.description")}

    {folders.map((folder, index) => ( - {removeFolder(index)}}/> + {removeFolder(index)}}/> ))}
@@ -106,17 +85,18 @@ export const ShareFoldersModal: ModalComponent = ({ data }) => } type FolderProps = { - path: string; + version: BSVersion; + relativeFolder: string; onDelete?: () => void; } -const FolderItem = ({path, onDelete}: FolderProps) => { +const FolderItem = ({version, relativeFolder, onDelete}: FolderProps) => { - const linker = FolderLinkerService.getInstance(); + const linker = VersionFolderLinkerService.getInstance(); const t = useTranslation(); - const name = path.split("\\").at(-1); + const name = relativeFolder.split("\\").at(-1); const color = useThemeColor("first-color"); const variants: Variants = { @@ -124,35 +104,43 @@ const FolderItem = ({path, onDelete}: FolderProps) => { tap: {rotate: 45} }; - const pending = useObservable(linker.isPending(path)); - const processing = useObservable(linker.isProcessing(path)); + 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(); - }, [path]); + }, [version, relativeFolder]); const loadFolderIsLinked = () => { - linker.isFolderLinked(path).toPromise().then(setLinked); + linker.isVersionFolderLinked(version, relativeFolder).toPromise().then(setLinked); } const onClickLink = () => { if(linked){ - return linker.unlinkFolder(path).toPromise().then(loadFolderIsLinked); + return linker.unlinkVersionFolder({ + version, + relativeFolder, + type: VersionLinkerActionType.Unlink, + }).then(loadFolderIsLinked); } - return linker.linkFolder(path).toPromise().then(loadFolderIsLinked); + return linker.linkVersionFolder({ + version, + relativeFolder, + type: VersionLinkerActionType.Link, + }).then(loadFolderIsLinked); } - const cancelLink = () => { - linker.cancelFolder(path); + const cancelLink = () => { + linker.cancelAction(version, relativeFolder); loadFolderIsLinked(); } return (
  • - {name} + {name}
    diff --git a/src/renderer/services/bs-version-manager.service.ts b/src/renderer/services/bs-version-manager.service.ts index c5f2249e..284413bf 100644 --- a/src/renderer/services/bs-version-manager.service.ts +++ b/src/renderer/services/bs-version-manager.service.ts @@ -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{ + return this.ipcService.sendV2("get-version-full-path", {args: version}); + } + } diff --git a/src/renderer/services/folder-linker.service.ts b/src/renderer/services/folder-linker.service.ts deleted file mode 100644 index a5a57fec..00000000 --- a/src/renderer/services/folder-linker.service.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { BehaviorSubject } from "rxjs"; -import { map, distinctUntilChanged, filter } from "rxjs/operators"; -import { from } from "rxjs"; -import { Observable } from "rxjs"; -import { IpcService } from "./ipc.service"; -import { ProgressBarService } from "./progress-bar.service"; -import { NotificationService } from "./notification.service"; -import { LinkOptions } from "main/services/folder-linker.service"; - -export class FolderLinkerService { - - private static instance: FolderLinkerService; - - public static getInstance(): FolderLinkerService { - if (!FolderLinkerService.instance) { - FolderLinkerService.instance = new FolderLinkerService(); - } - return FolderLinkerService.instance; - } - - private readonly ipc: IpcService; - private readonly progress: ProgressBarService; - private readonly notification: NotificationService; - - private readonly onLinkedListeners = new Map void>(); - private readonly onUnlinkedListeners = new Map void>(); - private readonly onRemovedFromQueueListeners = new Map void>(); - - private readonly publicOnLinkedListeners = new Set<(folder: string) => void>(); - private readonly publicOnUnlinkedListeners = new Set<(folder: string) => void>(); - - private readonly _currentAction$ = new BehaviorSubject(null); - private readonly _queue$ = new BehaviorSubject([]); - - private constructor() { - this.ipc = IpcService.getInstance(); - this.progress = ProgressBarService.getInstance(); - this.notification = NotificationService.getInstance(); - - this.queue$.pipe(map(queue => !!queue.length), distinctUntilChanged(), filter(haveAction => haveAction)).subscribe(() => { - this.startQueue(); - }); - } - - private async startQueue(): Promise{ - - while(this._queue$.value.at(0)){ - - const toDo = this._queue$.value.at(0); - - let progressOpened = false; - - if(!this.progress.isVisible){ - this.progress.showFake(.01, null, toDo.folder.split("\\").at(-1)); - progressOpened = true; - } - - this._currentAction$.next(toDo); - await this.doAction(toDo).toPromise(); - - this.specialFolderNotification(toDo); - - if(toDo.type === LinkActionType.Link){ - this.onLinkedListeners.get(toDo.folder)?.(); - this.publicOnLinkedListeners.forEach(listener => listener(toDo.folder)); - this.onLinkedListeners.delete(toDo.folder); - } - else{ - this.onUnlinkedListeners.get(toDo.folder)?.(); - this.publicOnUnlinkedListeners.forEach(listener => listener(toDo.folder)); - this.onUnlinkedListeners.delete(toDo.folder); - } - - const newArr = [...this._queue$.value]; - newArr.shift(); - this._queue$.next(newArr); - - if(progressOpened){ - this.progress.hide(true); - } - - } - - this._currentAction$.next(null); - } - - private specialFolderNotification(action: LinkAction): void{ - if(action.type === LinkActionType.Link && action.folder.includes("UserData")){ - this.notification.notifyInfo({ - title: "notifications.shared-folder.info.userdata-backup-created.title", - desc: "notifications.shared-folder.info.userdata-backup-created.msg", - duration: 7000 - }); - return; - } - } - - private doAction(action: LinkAction): Observable{ - return this.ipc.sendV2(action.type === LinkActionType.Link ? "link-folder" : "unlink-folder", { args: { folder: action.folder, options: action.options } }); - } - - private addFolderToQueue(folder: string, type: LinkActionType, options: LinkOptions){ - if(this._queue$.value.some(action => action.folder === folder)){ return; } - this._queue$.next([...this._queue$.value, {folder, type, options}]); - } - - private removeFolderFromQueue(folder: string){ - const queue = this._queue$.value; - const index = queue.findIndex(action => action.folder === folder); - if(index === -1){ return; } - this.onRemovedFromQueueListeners.get(folder)?.(); - queue.splice(index, 1); - this._queue$.next(queue); - } - - private onLinked(folder: string, callback: () => void){ this.onLinkedListeners.set(folder, callback); } - private onUnlinked(folder: string, callback: () => void){ this.onUnlinkedListeners.set(folder, callback); } - private onRemovedFromQueue(folder: string, callback: () => void){ this.onRemovedFromQueueListeners.set(folder, callback); } - - public isFolderLinked(path: string): Observable { - return this.ipc.sendV2("is-folder-symlink", {args: path}); - } - - public linkFolders(...folders: string[]): Map> { - return new Map(folders.map(folder => [folder, this.linkFolder(folder)])); - } - - public linkFolder(folder: string, options: LinkOptions = {}): Observable { - options.keepContents ??= true; - const promise = new Promise(resolve => { - this.onLinked(folder, resolve); - this.onRemovedFromQueue(folder, resolve); - }); - - this.addFolderToQueue(folder, LinkActionType.Link, options); - return from(promise); - } - - public unlinkFolders(...folders: string[]): Map> { - return new Map(folders.map(folder => [folder, this.unlinkFolder(folder)])); - } - - public unlinkFolder(folder: string, options: LinkOptions = {}): Observable { - options.keepContents ??= true; - const promise = new Promise(resolve => { - this.onUnlinked(folder, resolve); - this.onRemovedFromQueue(folder, resolve); - }); - - this.addFolderToQueue(folder, LinkActionType.Unlink, options); - return from(promise); - } - - public cancelFolder(folder: string): void { - this.removeFolderFromQueue(folder); - } - - public get queue$(): Observable { return this._queue$.asObservable(); } - public get currentAction$(): Observable { return this._currentAction$.asObservable(); } - - public isPending(folder: string): Observable { - return this.queue$.pipe(map(queue => queue.some(action => action.folder === folder), distinctUntilChanged())); - } - - public isProcessing(folder: string): Observable { - return this.currentAction$.pipe(distinctUntilChanged(), map(action => action?.folder === folder)); - } - - public onFolderLinked(callback: (folder: string) => void): void { this.publicOnLinkedListeners.add(callback); } - public removeOnFolderLinked(callback: (folder: string) => void): void { this.publicOnLinkedListeners.delete(callback); } - public onFolderUnlinked(callback: (folder: string) => void): void { this.publicOnUnlinkedListeners.add(callback); } - public removeOnFolderUnlinked(callback: (folder: string) => void): void { this.publicOnUnlinkedListeners.delete(callback); } - -} - -export interface LinkAction { - folder: string; - type: LinkActionType; - options?: LinkOptions; -} - -export enum LinkActionType { - Link, - Unlink -} \ No newline at end of file diff --git a/src/renderer/services/maps-manager.service.ts b/src/renderer/services/maps-manager.service.ts index cdb2580f..45e3f56a 100644 --- a/src/renderer/services/maps-manager.service.ts +++ b/src/renderer/services/maps-manager.service.ts @@ -13,10 +13,13 @@ import { ConfigurationService } from "./configuration.service"; import { ArchiveProgress } from "shared/models/archive.interface"; import { map, last, catchError, distinctUntilChanged, mergeMap } from "rxjs/operators"; import { ProgressionInterface } from "shared/models/progress-bar"; -import { FolderLinkerService } from "./folder-linker.service"; +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,13 +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: FolderLinkerService; + private readonly linker: VersionFolderLinkerService; private readonly lastLinkedVersion$: Subject = new Subject(); private readonly lastUnlinkedVersion$: Subject = new Subject(); @@ -42,11 +46,7 @@ export class MapsManagerService { this.progressBar = ProgressBarService.getInstance(); this.notifications = NotificationService.getInstance(); this.config = ConfigurationService.getInstance(); - this.linker = FolderLinkerService.getInstance(); - } - - private async getVersionMapsPath(version: BSVersion): Promise{ - return this.ipcService.sendV2("get-version-maps-path", {args: version}).toPromise(); + this.linker = VersionFolderLinkerService.getInstance(); } public getMaps(version?: BSVersion): Observable{ @@ -54,30 +54,35 @@ export class MapsManagerService { } public async versionHaveMapsLinked(version: BSVersion): Promise{ - const versionMapsPath = await this.getVersionMapsPath(version); - return this.linker.isFolderLinked(versionMapsPath).toPromise(); + return this.linker.isVersionFolderLinked(version, MapsManagerService.RELATIVE_MAPS_FOLDER).toPromise(); } - public async linkVersion(version: BSVersion): Promise{ + public async linkVersion(version: BSVersion): Promise{ const modalRes = await this.modal.openModal(LinkMapsModal); - if(modalRes.exitCode !== ModalExitCode.COMPLETED){ return; } + if(modalRes.exitCode !== ModalExitCode.COMPLETED){ return Promise.resolve(false); } - const versionMapsPath = await this.getVersionMapsPath(version); - - return this.linker.linkFolder(versionMapsPath, {keepContents: !!modalRes.data}).toPromise(); + return this.linker.linkVersionFolder({ + version, + type: VersionLinkerActionType.Link, + relativeFolder: MapsManagerService.RELATIVE_MAPS_FOLDER, + options: { keepContents: !!modalRes.data } + }); } - public async unlinkVersion(version: BSVersion): Promise{ + public async unlinkVersion(version: BSVersion): Promise{ const modalRes = await this.modal.openModal(UnlinkMapsModal); - if(modalRes.exitCode !== ModalExitCode.COMPLETED){ return; } + if(modalRes.exitCode !== ModalExitCode.COMPLETED){ return Promise.resolve(false); } - const versionMapsPath = await this.getVersionMapsPath(version); - - return this.linker.unlinkFolder(versionMapsPath, {keepContents: !!modalRes.data}).toPromise(); + 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{ @@ -155,8 +160,7 @@ export class MapsManagerService { public $mapsLinkingPending(version: BSVersion): Observable{ return this.linker.queue$.pipe(mergeMap(async queue => { - const versionMapsPath = await this.getVersionMapsPath(version); - return queue.some(q => q.folder.includes(versionMapsPath)); + return queue.some(q => q.relativeFolder.includes(MapsManagerService.RELATIVE_MAPS_FOLDER) && equal(q.version, version)); }), distinctUntilChanged()); } diff --git a/src/renderer/services/version-folder-linker.service.ts b/src/renderer/services/version-folder-linker.service.ts new file mode 100644 index 00000000..eba43938 --- /dev/null +++ b/src/renderer/services/version-folder-linker.service.ts @@ -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([]); + + private readonly linkListeners = new Set(); + private readonly unlinkListeners = new Set(); + + 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{ + return this.ipcService.sendV2("link-version-folder-action", { args: action }); + } + + public linkVersionFolder(action: VersionLinkFolderAction): Promise{ + const promise = new Promise((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{ + const promise = new Promise((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{ + return this.ipcService.sendV2("is-version-folder-linked", { args: { version, relativeFolder } }); + } + + public isPending(version: BSVersion, relativeFolder: string): Observable { + 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 { + return this.currentAction$.pipe(map(action => ( action && action.version === version && action.relativeFolder === relativeFolder))); + } + + public getLinkedFolders(version: BSVersion, options?: { relative?: boolean }): Observable{ + return this.ipcService.sendV2("get-linked-folders", { args: { version, options } }); + } + + public get currentAction$(): Observable{ + return this._queue$.pipe(map(actions => actions.at(-1)), distinctUntilChanged()); + } + + public get queue$(): Observable{ + 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; \ No newline at end of file