[hotfix] move instead of copy if no other version use the linked folder

This commit is contained in:
MathieuG-P
2023-03-20 22:16:38 +01:00
parent 420ecc9ae6
commit 83fcbf6d04
12 changed files with 387 additions and 286 deletions
@@ -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
}
+25 -21
View File
@@ -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;