mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
[feature-593] We can now delete duplicates maps
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { createContext, useRef, useState } from "react";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { LocalMapsListPanel } from "./maps/local-maps-list-panel.component";
|
||||
import { LocalMapsListPanel, LocalMapsListPanelRef } from "./maps/local-maps-list-panel.component";
|
||||
import { BsmDropdownButton, DropDownItem } from "../shared/bsm-dropdown-button.component";
|
||||
import { FilterPanel } from "./maps/filter-panel.component";
|
||||
import { MapFilter } from "shared/models/maps/beat-saver.model";
|
||||
@@ -56,7 +56,7 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
setPlaylists: playlists$.next.bind(playlists$),
|
||||
}));
|
||||
|
||||
const mapsRef = useRef<any>();
|
||||
const mapsRef = useRef<LocalMapsListPanelRef>();
|
||||
const playlistsRef = useRef<LocalPlaylistsListRef>();
|
||||
|
||||
const [mapFilter, setMapFilter] = useState<MapFilter>({});
|
||||
@@ -106,6 +106,7 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
return [
|
||||
{ icon: "export", text: "pages.version-viewer.maps.search-bar.dropdown.export-maps", onClick: () => mapsRef.current.exportMaps?.() },
|
||||
{ icon: "trash", text: "pages.version-viewer.maps.search-bar.dropdown.delete-maps", onClick: () => mapsRef.current.deleteMaps?.() },
|
||||
{ icon: "clean", text: "pages.version-viewer.maps.search-bar.dropdown.delete-duplicate-maps", onClick: () => mapsRef.current.removeDuplicates?.() },
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
+14
-2
@@ -2,7 +2,7 @@ import { MapsManagerService } from "renderer/services/maps-manager.service";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useState } from "react";
|
||||
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
|
||||
import { Subscription, BehaviorSubject } from "rxjs";
|
||||
import { Subscription, BehaviorSubject, lastValueFrom } from "rxjs";
|
||||
import { MapFilter } from "shared/models/maps/beat-saver.model";
|
||||
import { MapsDownloaderService } from "renderer/services/maps-downloader.service";
|
||||
import { last, tap } from "rxjs/operators";
|
||||
@@ -34,7 +34,13 @@ type Props = {
|
||||
isActive?: boolean;
|
||||
};
|
||||
|
||||
export const LocalMapsListPanel = forwardRef<unknown, Props>(({ version, className, filter, search, linkedState, isActive }, forwardRef) => {
|
||||
export type LocalMapsListPanelRef = {
|
||||
deleteMaps: () => void;
|
||||
exportMaps: () => void;
|
||||
removeDuplicates: () => void;
|
||||
}
|
||||
|
||||
export const LocalMapsListPanel = forwardRef<LocalMapsListPanelRef, Props>(({ version, className, filter, search, linkedState, isActive }, forwardRef) => {
|
||||
const mapsManager = useService(MapsManagerService);
|
||||
const mapsDownloader = useService(MapsDownloaderService);
|
||||
|
||||
@@ -63,6 +69,12 @@ export const LocalMapsListPanel = forwardRef<unknown, Props>(({ version, classNa
|
||||
},
|
||||
exportMaps() {
|
||||
mapsManager.exportMaps(version, selectedMaps);
|
||||
},
|
||||
removeDuplicates() {
|
||||
return lastValueFrom(mapsManager.deleteDuplicateMaps(maps$.value)).then(res => {
|
||||
if(!res?.current){ return; }
|
||||
loadMaps();
|
||||
}).catch(noop);
|
||||
}
|
||||
}),[selectedMaps, maps, version]);
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import { BsmImage } from "renderer/components/shared/bsm-image.component";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { ModalComponent, ModalExitCode } from "renderer/services/modale.service";
|
||||
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
|
||||
import BeatConflict from "../../../../../assets/images/apngs/beat-conflict.png";
|
||||
import { useConstant } from "renderer/hooks/use-constant.hook";
|
||||
|
||||
export const DeleteDuplicateMapsModal: ModalComponent<void, { maps: BsmLocalMap[] }> = ({ resolver, options: {data : { maps }}}) => {
|
||||
|
||||
const t = useTranslation();
|
||||
const multiple = useConstant(() => maps.length > 1);
|
||||
|
||||
return (
|
||||
<form className="text-gray-800 dark:text-gray-200 max-w-sm">
|
||||
<h1 className="text-3xl uppercase tracking-wide w-full text-center">{t("modals.maps-actions.delete-duplicate-maps.title")}</h1>
|
||||
<BsmImage className="mx-auto h-24" image={BeatConflict} />
|
||||
<p>{
|
||||
multiple
|
||||
? t("modals.maps-actions.delete-duplicate-maps.desc-plural", { nb: `${maps.length}` })
|
||||
: t("modals.maps-actions.delete-duplicate-maps.desc", { map: `${maps.at(0).rawInfo._songName}` })
|
||||
}</p>
|
||||
<div className="grid grid-flow-col grid-cols-2 gap-4 mt-4 h-8">
|
||||
<BsmButton typeColor="cancel" className="rounded-md flex justify-center items-center transition-all" onClick={() => resolver({ exitCode: ModalExitCode.CANCELED })} withBar={false} text="misc.cancel" />
|
||||
<BsmButton typeColor="primary" className="rounded-md flex justify-center items-center transition-all" onClick={() => resolver({ exitCode: ModalExitCode.COMPLETED })} withBar={false} text="misc.delete" />
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -79,7 +79,7 @@ export const BsmDropdownButton = forwardRef(({ className, classNames, buttonColo
|
||||
{items?.map(
|
||||
i =>
|
||||
i && (
|
||||
<div key={crypto.randomUUID()} onClick={() => i.onClick?.()} className="flex w-full px-3 py-2 hover:backdrop-brightness-150">
|
||||
<div key={crypto.randomUUID()} onClick={() => { setExpanded(() => false); i.onClick?.()}} className="flex w-full px-3 py-2 hover:backdrop-brightness-150">
|
||||
{i.icon && <BsmIcon icon={i.icon} className="h-5 w-5 mr-1 text-inherit" />}
|
||||
<span className="w-max">{t(i.text)}</span>
|
||||
</div>
|
||||
|
||||
@@ -61,9 +61,10 @@ import { EyeCrossIcon } from "./icons/eye-cross-icon.component";
|
||||
import { ShortcutIcon } from "./icons/shortcut-icon.component";
|
||||
import { BackupRestoreIcon } from "./icons/backup-restore-icon.component";
|
||||
import { SongDetailDiffCharactertistic } from "shared/models/maps/song-details-cache/song-details-cache.model";
|
||||
import { CleanIcon } from "./icons/clean-icon.component";
|
||||
|
||||
|
||||
export type BsmIconType = SongDetailDiffCharactertistic | ("settings" | "trash" | "favorite" | "folder" | "bsNote" | "check" | "three-dots" | "twitch" | "eye" | "play" | "checkCircleIcon" | "discord" | "info" | "eye-cross" | "terminal" | "desktop" | "oculus" | "add" | "cross" | "task" | "github" | "close" | "thumbUpFill" | "timerFill" | "pause" | "twitter" | "sync" | "chevron-top" | "copy" | "steam" | "edit" | "export" | "patreon" | "search" | "bsMapDifficulty" | "link" | "unlink" | "download" | "filter" | "mee6" | "volume-up" | "volume-off" | "volume-down" | "shortcut" | "backup-restore" | "web-site" | "fr-FR-flag" | "es-ES-flag" | "en-US-flag" | "en-EN-flag" | "de-DE-flag" | "ru-RU-flag" | "zh-CN-flag" | "zh-TW-flag" | "ja-JP-flag");
|
||||
export type BsmIconType = SongDetailDiffCharactertistic | ("settings" | "trash" | "favorite" | "folder" | "bsNote" | "check" | "three-dots" | "twitch" | "eye" | "play" | "checkCircleIcon" | "discord" | "info" | "eye-cross" | "terminal" | "desktop" | "oculus" | "add" | "cross" | "task" | "github" | "close" | "thumbUpFill" | "timerFill" | "pause" | "twitter" | "sync" | "chevron-top" | "copy" | "steam" | "edit" | "export" | "patreon" | "search" | "bsMapDifficulty" | "link" | "unlink" | "download" | "filter" | "mee6" | "volume-up" | "volume-off" | "volume-down" | "shortcut" | "backup-restore" | "web-site" | "clean" | "fr-FR-flag" | "es-ES-flag" | "en-US-flag" | "en-EN-flag" | "de-DE-flag" | "ru-RU-flag" | "zh-CN-flag" | "zh-TW-flag" | "ja-JP-flag");
|
||||
|
||||
export const BsmIcon = memo(({ className, icon, style }: { className?: string; icon: BsmIconType; style?: CSSProperties }) => {
|
||||
// TODO : Very ugly very messy, need to find a better way to do this
|
||||
@@ -256,6 +257,11 @@ export const BsmIcon = memo(({ className, icon, style }: { className?: string; i
|
||||
if(icon === "web-site") {
|
||||
return <WebSiteIcon className={className} style={style} />;
|
||||
}
|
||||
|
||||
if(icon === "clean") {
|
||||
return <CleanIcon className={className} style={style} />;
|
||||
}
|
||||
|
||||
return <TrashIcon className={className} style={style} />;
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { createSvgIcon } from "../svg-icon.type";
|
||||
|
||||
export const CleanIcon = createSvgIcon((props, ref) => {
|
||||
return (
|
||||
<svg ref={ref} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"color="currentColor" fill="none" {...props}>
|
||||
<path d="m21 3-8 8.5m-3.554-.415c-2.48.952-4.463.789-6.446.003.5 6.443 3.504 8.92 7.509 9.912 0 0 3.017-2.134 3.452-7.193.047-.548.07-.821-.043-1.13-.114-.309-.338-.53-.785-.973-.736-.728-1.103-1.092-1.54-1.184-.437-.09-1.007.128-2.147.565" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M4.5 16.446S7 16.93 9.5 15" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M8.5 7.25a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0Z" stroke="currentColor" strokeWidth="2"/>
|
||||
<path d="M11 4v.1" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
);
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LinkMapsModal } from "renderer/components/modal/modal-types/link-maps-modal.component";
|
||||
import { UnlinkMapsModal } from "renderer/components/modal/modal-types/unlink-maps-modal.component";
|
||||
import { Subject, Observable, of, lastValueFrom } from "rxjs";
|
||||
import { Subject, Observable, of, lastValueFrom, Subscription, throwError } from "rxjs";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { BsmLocalMapsProgress, BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
|
||||
import { IpcService } from "./ipc.service";
|
||||
@@ -13,6 +13,8 @@ import { map, last, catchError } from "rxjs/operators";
|
||||
import { ProgressionInterface } from "shared/models/progress-bar";
|
||||
import { FolderLinkState, VersionFolderLinkerService } from "./version-folder-linker.service";
|
||||
import { SongDetails } from "shared/models/maps";
|
||||
import { Progression } from "main/helpers/fs.helpers";
|
||||
import { DeleteDuplicateMapsModal } from "renderer/components/modal/modal-types/delete-duplicate-maps-modal.component";
|
||||
|
||||
export class MapsManagerService {
|
||||
private static instance: MapsManagerService;
|
||||
@@ -146,6 +148,54 @@ export class MapsManagerService {
|
||||
});
|
||||
}
|
||||
|
||||
public deleteDuplicateMaps(maps: BsmLocalMap[]): Observable<Progression> {
|
||||
const hashMap = Object.groupBy(maps, m => m.hash);
|
||||
const mapsToDelete: BsmLocalMap[] = [];
|
||||
|
||||
for (const maps of Object.values(hashMap)) {
|
||||
if(!Array.isArray(maps)){ continue; }
|
||||
maps.sort((a, b) => b.path.length - a.path.length).pop(); // The longest paths are most likely to be the duplicate ones (path: '../Awesome maps (copy)')
|
||||
mapsToDelete.push(...maps);
|
||||
}
|
||||
|
||||
if(mapsToDelete.length === 0){
|
||||
this.notifications.notifySuccess({ title: "notifications.maps.no-duplicates-maps.title", desc: "notifications.maps.no-duplicates-maps.msg", duration: 4000 });
|
||||
return of({ current: 0, total: 0 });
|
||||
}
|
||||
|
||||
if(!this.progressBar.require()){
|
||||
return throwError(() => new Error("Progress bar is required to delete duplicate maps"));
|
||||
}
|
||||
|
||||
return new Observable<Progression>(obs => {
|
||||
let sub: Subscription;
|
||||
(async () => {
|
||||
const modalRes = await this.modal.openModal(DeleteDuplicateMapsModal, { data: { maps: mapsToDelete } });
|
||||
|
||||
if(modalRes.exitCode !== ModalExitCode.COMPLETED){
|
||||
throw new Error("Operation to delete duplicate maps as been canceled by the user");
|
||||
}
|
||||
|
||||
const progress$: Observable<Progression> = this.ipcService.sendV2("delete-maps", mapsToDelete).pipe(map(p => ({ total: p.total, current: p.deleted })));
|
||||
|
||||
this.progressBar.show(progress$);
|
||||
sub = progress$.subscribe(obs);
|
||||
|
||||
await lastValueFrom(progress$);
|
||||
})()
|
||||
.then(() => this.notifications.notifySuccess({ title: "notifications.maps.duplicates-maps-deleted.title", desc: "notifications.maps.duplicates-maps-deleted.msg", duration: 4000 }))
|
||||
.catch(e => obs.error(e))
|
||||
.finally(() => obs.complete());
|
||||
|
||||
return () => {
|
||||
if(sub){
|
||||
this.progressBar.hide();
|
||||
sub.unsubscribe();
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public getMapsInfoFromHashs(hashs: string[]): Observable<SongDetails[]> {
|
||||
return this.ipcService.sendV2("get-maps-info-from-cache", hashs);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user