mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
[hotfix] move instead of copy if no other version use the linked folder
This commit is contained in:
@@ -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<BSVersion>, 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<BSVersion>, 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<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 ??= {};
|
||||
|
||||
@@ -77,15 +77,4 @@ ipcMain.on("open-steam", async (event, request: IpcRequest<void>) => {
|
||||
}).catch((e) => {
|
||||
utils.ipcSend(request.responceChannel, {success: false, error: e});
|
||||
});
|
||||
});
|
||||
|
||||
ipc.on("is-folder-symlink", async (req: IpcRequest<string>, reply) => {
|
||||
const promise = new Promise<boolean>(async resolve => {
|
||||
if(!(await pathExist(req.args))){ return resolve(false); }
|
||||
lstat(req.args, (err, stats) => {
|
||||
if(err){ return resolve(false); }
|
||||
resolve(stats.isSymbolicLink());
|
||||
});
|
||||
});
|
||||
reply(from(promise));
|
||||
});
|
||||
@@ -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)
|
||||
|
||||
@@ -69,7 +69,7 @@ export class FolderLinkerService {
|
||||
return symlink(sharedPath, folderPath, "junction");
|
||||
}
|
||||
|
||||
public async unlinkFolder(folderPath: string, options?: LinkOptions): Promise<void> {
|
||||
public async unlinkFolder(folderPath: string, options?: UnlinkOptions): Promise<void> {
|
||||
|
||||
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
|
||||
}
|
||||
backup?: boolean,
|
||||
}
|
||||
|
||||
export interface UnlinkOptions extends LinkOptions {
|
||||
moveContents?: boolean,
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+3
-1
@@ -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) => {
|
||||
|
||||
+9
-8
@@ -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<MapFilter>({});
|
||||
@@ -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]);
|
||||
|
||||
@@ -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<void, BSVersion> = ({ data }) => {
|
||||
@@ -20,32 +22,18 @@ export const ShareFoldersModal: ModalComponent<void, BSVersion> = ({ data }) =>
|
||||
|
||||
const config = ConfigurationService.getInstance();
|
||||
const ipc = IpcService.getInstance();
|
||||
const linker = VersionFolderLinkerService.getInstance();
|
||||
const versionManager = BSVersionManagerService.getInstance();
|
||||
|
||||
const t = useTranslation();
|
||||
|
||||
const [folders, setFolders] = useState<string[]>([]);
|
||||
const versionPath = useObservable(ipc.sendV2<string>("get-version-full-path", {args: data}));
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const defaultFolders = config.get<string[]>(SHARED_FOLDERS_KEY);
|
||||
setFolders(prev => Array.from(new Set([...prev, ...config.get<string[]>(SHARED_FOLDERS_KEY)]).values()));
|
||||
|
||||
const promises = Promise.allSettled(defaultFolders.map(async (folder) => {
|
||||
return ipc.sendV2<string>("relative-version-path-to-full", {args: {version: data, relative: folder}}).toPromise();
|
||||
}));
|
||||
|
||||
promises.then((res) => {
|
||||
|
||||
const fullPathFolders = res.reduce((acc, curent) => {
|
||||
if(curent.status !== "fulfilled"){ return acc; }
|
||||
acc.push(curent.value);
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
setFolders(prev => Array.from(new Set([...prev, ...fullPathFolders]).values()));
|
||||
});
|
||||
|
||||
ipc.sendV2<string[]>("get-linked-folders", {args: data}).toPromise().then(linkedFolders => {
|
||||
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<void, BSVersion> = ({ data }) =>
|
||||
config.delete(SHARED_FOLDERS_KEY);
|
||||
return;
|
||||
}
|
||||
|
||||
const promises = Promise.allSettled(folders.map(async folder => {
|
||||
return ipc.sendV2<string>("full-version-path-to-relative", {args: {version: data, fullPath: folder}}).toPromise();
|
||||
}));
|
||||
|
||||
promises.then((res) => {
|
||||
const sharedFolders = res.reduce((acc, curent) => {
|
||||
if(curent.status !== "fulfilled"){ return acc; }
|
||||
acc.push(curent.value);
|
||||
return acc;
|
||||
}, [] as string[]);
|
||||
config.set(SHARED_FOLDERS_KEY, sharedFolders);
|
||||
});
|
||||
|
||||
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<string>("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<void, BSVersion> = ({ data }) =>
|
||||
<p className="my-3">{t("modals.shared-folders.description")}</p>
|
||||
<ul className="flex flex-col gap-1 mb-2 h-[300px] max-h-[300px] overflow-scroll scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900 px-1">
|
||||
{folders.map((folder, index) => (
|
||||
<FolderItem key={index} path={folder} onDelete={() => {removeFolder(index)}}/>
|
||||
<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}>
|
||||
@@ -106,17 +85,18 @@ export const ShareFoldersModal: ModalComponent<void, BSVersion> = ({ 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 (
|
||||
<li className="w-full h-12 rounded-md shrink-0 flex flex-row items-center justify-between px-2 font-bold bg-light-main-color-1 dark:bg-main-color-1">
|
||||
<span className="cursor-help" title={path}>{name}</span>
|
||||
<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}/>
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<string, () => void>();
|
||||
private readonly onUnlinkedListeners = new Map<string, () => void>();
|
||||
private readonly onRemovedFromQueueListeners = new Map<string, () => void>();
|
||||
|
||||
private readonly publicOnLinkedListeners = new Set<(folder: string) => void>();
|
||||
private readonly publicOnUnlinkedListeners = new Set<(folder: string) => void>();
|
||||
|
||||
private readonly _currentAction$ = new BehaviorSubject<LinkAction>(null);
|
||||
private readonly _queue$ = new BehaviorSubject<LinkAction[]>([]);
|
||||
|
||||
private constructor() {
|
||||
this.ipc = IpcService.getInstance();
|
||||
this.progress = ProgressBarService.getInstance();
|
||||
this.notification = NotificationService.getInstance();
|
||||
|
||||
this.queue$.pipe(map(queue => !!queue.length), distinctUntilChanged(), filter(haveAction => haveAction)).subscribe(() => {
|
||||
this.startQueue();
|
||||
});
|
||||
}
|
||||
|
||||
private async startQueue(): Promise<void>{
|
||||
|
||||
while(this._queue$.value.at(0)){
|
||||
|
||||
const toDo = this._queue$.value.at(0);
|
||||
|
||||
let progressOpened = false;
|
||||
|
||||
if(!this.progress.isVisible){
|
||||
this.progress.showFake(.01, null, toDo.folder.split("\\").at(-1));
|
||||
progressOpened = true;
|
||||
}
|
||||
|
||||
this._currentAction$.next(toDo);
|
||||
await this.doAction(toDo).toPromise();
|
||||
|
||||
this.specialFolderNotification(toDo);
|
||||
|
||||
if(toDo.type === LinkActionType.Link){
|
||||
this.onLinkedListeners.get(toDo.folder)?.();
|
||||
this.publicOnLinkedListeners.forEach(listener => listener(toDo.folder));
|
||||
this.onLinkedListeners.delete(toDo.folder);
|
||||
}
|
||||
else{
|
||||
this.onUnlinkedListeners.get(toDo.folder)?.();
|
||||
this.publicOnUnlinkedListeners.forEach(listener => listener(toDo.folder));
|
||||
this.onUnlinkedListeners.delete(toDo.folder);
|
||||
}
|
||||
|
||||
const newArr = [...this._queue$.value];
|
||||
newArr.shift();
|
||||
this._queue$.next(newArr);
|
||||
|
||||
if(progressOpened){
|
||||
this.progress.hide(true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
this._currentAction$.next(null);
|
||||
}
|
||||
|
||||
private specialFolderNotification(action: LinkAction): void{
|
||||
if(action.type === LinkActionType.Link && action.folder.includes("UserData")){
|
||||
this.notification.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<void>{
|
||||
return this.ipc.sendV2<void, {folder: string, options?: LinkOptions}>(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<boolean> {
|
||||
return this.ipc.sendV2<boolean>("is-folder-symlink", {args: path});
|
||||
}
|
||||
|
||||
public linkFolders(...folders: string[]): Map<string, Observable<void>> {
|
||||
return new Map(folders.map(folder => [folder, this.linkFolder(folder)]));
|
||||
}
|
||||
|
||||
public linkFolder(folder: string, options: LinkOptions = {}): Observable<void> {
|
||||
options.keepContents ??= true;
|
||||
const promise = new Promise<void>(resolve => {
|
||||
this.onLinked(folder, resolve);
|
||||
this.onRemovedFromQueue(folder, resolve);
|
||||
});
|
||||
|
||||
this.addFolderToQueue(folder, LinkActionType.Link, options);
|
||||
return from(promise);
|
||||
}
|
||||
|
||||
public unlinkFolders(...folders: string[]): Map<string, Observable<void>> {
|
||||
return new Map(folders.map(folder => [folder, this.unlinkFolder(folder)]));
|
||||
}
|
||||
|
||||
public unlinkFolder(folder: string, options: LinkOptions = {}): Observable<void> {
|
||||
options.keepContents ??= true;
|
||||
const promise = new Promise<void>(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<LinkAction[]> { return this._queue$.asObservable(); }
|
||||
public get currentAction$(): Observable<LinkAction> { return this._currentAction$.asObservable(); }
|
||||
|
||||
public isPending(folder: string): Observable<boolean> {
|
||||
return this.queue$.pipe(map(queue => queue.some(action => action.folder === folder), distinctUntilChanged()));
|
||||
}
|
||||
|
||||
public isProcessing(folder: string): Observable<boolean> {
|
||||
return this.currentAction$.pipe(distinctUntilChanged(), map(action => action?.folder === folder));
|
||||
}
|
||||
|
||||
public onFolderLinked(callback: (folder: string) => void): void { this.publicOnLinkedListeners.add(callback); }
|
||||
public removeOnFolderLinked(callback: (folder: string) => void): void { this.publicOnLinkedListeners.delete(callback); }
|
||||
public onFolderUnlinked(callback: (folder: string) => void): void { this.publicOnUnlinkedListeners.add(callback); }
|
||||
public removeOnFolderUnlinked(callback: (folder: string) => void): void { this.publicOnUnlinkedListeners.delete(callback); }
|
||||
|
||||
}
|
||||
|
||||
export interface LinkAction {
|
||||
folder: string;
|
||||
type: LinkActionType;
|
||||
options?: LinkOptions;
|
||||
}
|
||||
|
||||
export enum LinkActionType {
|
||||
Link,
|
||||
Unlink
|
||||
}
|
||||
@@ -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<BSVersion> = new Subject();
|
||||
private readonly lastUnlinkedVersion$: Subject<BSVersion> = 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<string>{
|
||||
return this.ipcService.sendV2<string>("get-version-maps-path", {args: version}).toPromise();
|
||||
this.linker = VersionFolderLinkerService.getInstance();
|
||||
}
|
||||
|
||||
public getMaps(version?: BSVersion): Observable<BsmLocalMapsProgress>{
|
||||
@@ -54,30 +54,35 @@ export class MapsManagerService {
|
||||
}
|
||||
|
||||
public async versionHaveMapsLinked(version: BSVersion): Promise<boolean>{
|
||||
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<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 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<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 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<boolean>{
|
||||
@@ -155,8 +160,7 @@ export class MapsManagerService {
|
||||
|
||||
public $mapsLinkingPending(version: BSVersion): Observable<boolean>{
|
||||
return this.linker.queue$.pipe(mergeMap(async queue => {
|
||||
const versionMapsPath = await this.getVersionMapsPath(version);
|
||||
return queue.some(q => q.folder.includes(versionMapsPath));
|
||||
return queue.some(q => q.relativeFolder.includes(MapsManagerService.RELATIVE_MAPS_FOLDER) && equal(q.version, version));
|
||||
}), distinctUntilChanged());
|
||||
}
|
||||
|
||||
|
||||
@@ -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(-1)), 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;
|
||||
Reference in New Issue
Block a user