Merge pull request #575 from silentrald/feat/75

[feat-75] support drag and drop for map zip files
This commit is contained in:
MathieuG-P
2024-10-17 15:16:01 +02:00
committed by GitHub
19 changed files with 455 additions and 13 deletions
+1
View File
@@ -59,6 +59,7 @@ module.exports = {
"react/function-component-definition": "off",
"jsx-a11y/control-has-associated-label": "off",
"react/button-has-type": "off",
"no-labels": ["error", { "allowLoop": true }]
},
parserOptions: {
ecmaVersion: 2020,
+16
View File
@@ -65,6 +65,10 @@
"empty-maps": {
"text": "Keine Maps",
"button": "Maps herunterladen"
},
"drop-zone": {
"text": "Deine Karten importieren",
"subtext": "Lasse deine ZIP-Dateien hier fallen, um deine Karten zu importieren"
}
}
}
@@ -505,6 +509,18 @@
"duplicates-maps-deleted": {
"title": "Duplikate gelöscht",
"msg": "Duplikate wurden gelöscht"
},
"import-map": {
"titles": {
"success": "Kartenimport abgeschlossen",
"error": "Beim Kartenimport ist ein Fehler aufgetreten"
},
"msgs": {
"success": "Die Karten wurden erfolgreich importiert.",
"some-success": "Einige Karten wurden erfolgreich importiert.",
"only-accept-zip": "Nur Zip-Dateien werden unterstützt.",
"invalid-zip": "Die Zip-Datei(en) enthalten keine Karten."
}
}
},
"playlists": {
+17
View File
@@ -65,7 +65,12 @@
"empty-maps": {
"text": "No maps",
"button": "Download maps"
},
"drop-zone": {
"text": "Import your maps",
"subtext": "Drop your zip files here to import your maps"
}
}
}
},
@@ -512,6 +517,18 @@
"duplicates-maps-deleted": {
"title": "Duplicates Deleted",
"msg": "Duplicates were deleted"
},
"import-map": {
"titles": {
"success": "Maps import completed",
"error": "An error occurred during the maps import"
},
"msgs": {
"success": "Maps successfully imported.",
"some-success": "Some maps were successfully imported.",
"only-accept-zip": "Only zip files are supported.",
"invalid-zip": "The zip file(s) do not contain any maps."
}
}
},
"playlists": {
+16
View File
@@ -65,6 +65,10 @@
"empty-maps": {
"text": "No hay mapas",
"button": "Descargar mapas"
},
"drop-zone": {
"text": "Importar tus mapas",
"subtext": "Suelta tus archivos zip aquí para importar tus mapas"
}
}
}
@@ -505,6 +509,18 @@
"duplicates-maps-deleted": {
"title": "Duplicados eliminados",
"msg": "Se eliminaron los duplicados"
},
"import-map": {
"titles": {
"success": "Importación de mapas completada",
"error": "Ocurrió un error durante la importación de mapas"
},
"msgs": {
"success": "Los mapas se han importado con éxito.",
"some-success": "Algunos mapas se han importado con éxito.",
"only-accept-zip": "Solo se admiten archivos zip.",
"invalid-zip": "El o los archivos zip no contienen mapas."
}
}
},
"playlists": {
+16
View File
@@ -65,6 +65,10 @@
"empty-maps": {
"text": "Aucune map",
"button": "Télécharger des maps"
},
"drop-zone": {
"text": "Importer vos maps",
"subtext": "Déposez vos fichiers zip ici pour importer vos maps"
}
}
}
@@ -505,6 +509,18 @@
"duplicates-maps-deleted": {
"title": "Doublons supprimés",
"msg": "Les doublons ont été supprimés"
},
"import-map": {
"titles": {
"success": "Importation des maps terminée",
"error": "Une erreur est survenue lors de l'importation des maps"
},
"msgs": {
"success": "Les maps ont été importées avec succès.",
"some-success": "Certaines maps ont été importées avec succès.",
"only-accept-zip": "Seules les fichiers zip sont pris en charge.",
"invalid-zip": "Le ou les fichiers zip ne contiennent aucune maps."
}
}
},
"playlists": {
+16
View File
@@ -65,6 +65,10 @@
"empty-maps": {
"text": "マップがありません",
"button": "マップをダウンロードする"
},
"drop-zone": {
"text": "マップをインポート",
"subtext": "ZIPファイルをここにドロップしてマップをインポート"
}
}
}
@@ -505,6 +509,18 @@
"duplicates-maps-deleted": {
"title": "重複削除",
"msg": "重複が削除されました"
},
"import-map": {
"titles": {
"success": "マップのインポートが完了しました",
"error": "マップのインポート中にエラーが発生しました"
},
"msgs": {
"success": "マップが正常にインポートされました。",
"some-success": "一部のマップが正常にインポートされました。",
"only-accept-zip": "zipファイルのみがサポートされています。",
"invalid-zip": "zipファイルにマップが含まれていません。"
}
}
},
"playlists": {
+16
View File
@@ -65,6 +65,10 @@
"empty-maps": {
"text": "Нет карт",
"button": "Скачать карты"
},
"drop-zone": {
"text": "Импортируйте свои карты",
"subtext": "Перетащите сюда файлы ZIP, чтобы импортировать свои карты"
}
}
}
@@ -505,6 +509,18 @@
"duplicates-maps-deleted": {
"title": "Дубликаты удалены",
"msg": "Дубликаты были удалены"
},
"import-map": {
"titles": {
"success": "Импорт карт завершен",
"error": "Произошла ошибка при импорте карт"
},
"msgs": {
"success": "Карты были успешно импортированы.",
"some-success": "Некоторые карты успешно импортированы.",
"only-accept-zip": "Поддерживаются только zip-файлы.",
"invalid-zip": "В zip-файле(ах) нет карт."
}
}
},
"playlists": {
+16
View File
@@ -65,6 +65,10 @@
"empty-maps": {
"text": "無譜面",
"button": "下載譜面"
},
"drop-zone": {
"text": "導入您的地圖",
"subtext": "將ZIP文件拖放到此處以導入您的地圖"
}
}
}
@@ -505,6 +509,18 @@
"duplicates-maps-deleted": {
"title": "重複已刪除",
"msg": "重複已刪除"
},
"import-map": {
"titles": {
"success": "地圖匯入完成",
"error": "地圖匯入時發生錯誤"
},
"msgs": {
"success": "地圖已成功匯入。",
"some-success": "部分地圖已成功匯入。",
"only-accept-zip": "僅支援zip檔案。",
"invalid-zip": "zip檔案中沒有地圖。"
}
}
},
"playlists": {
+16
View File
@@ -65,6 +65,10 @@
"empty-maps": {
"text": "无谱面",
"button": "下载谱面"
},
"drop-zone": {
"text": "导入您的地图",
"subtext": "将ZIP文件拖放到此处以导入您的地图"
}
}
}
@@ -505,6 +509,18 @@
"duplicates-maps-deleted": {
"title": "重复已删除",
"msg": "重复已删除"
},
"import-map": {
"titles": {
"success": "地图导入完成",
"error": "导入地图时发生错误"
},
"msgs": {
"success": "地图已成功导入。",
"some-success": "部分地图已成功导入。",
"only-accept-zip": "仅支持zip文件。",
"invalid-zip": "zip文件中没有地图。"
}
}
},
"playlists": {
+6 -1
View File
@@ -22,7 +22,12 @@ ipc.on("export-maps", async (args, reply) => {
reply(await maps.exportMaps(args.version, args.maps, args.outPath));
});
ipc.on("download-map", async (args, reply) => {
ipc.on("bs-maps.import-maps", async (args, reply) => {
const maps = LocalMapsManagerService.getInstance();
reply(maps.importMaps(args.paths, args.version));
})
ipc.on("bs-maps.download-map", async (args, reply) => {
const maps = LocalMapsManagerService.getInstance();
reply(from(maps.downloadMap(args.map, args.version)));
});
+4 -1
View File
@@ -1,4 +1,4 @@
import { contextBridge, ipcRenderer, IpcRendererEvent } from "electron";
import { contextBridge, ipcRenderer, IpcRendererEvent, webUtils } from "electron";
import { ProviderPlatform } from "shared/models/provider-platform.enum";
const sep = process.platform === ProviderPlatform.WINDOWS ? "\\" : "/";
@@ -30,5 +30,8 @@ contextBridge.exposeInMainWorld("electron", {
join: (...args: string[]): string => {
return args.join(sep);
}
},
webUtils: {
getPathForFile: webUtils.getPathForFile
}
});
@@ -28,8 +28,8 @@ import { sToMs } from "../../../../shared/helpers/time.helpers";
import { FieldRequired } from "shared/helpers/type.helpers";
import { MapInfo } from "shared/models/maps/info/map-info.model";
import { parseMapInfoDat } from "shared/parsers/maps/map-info.parser";
import { tryit } from "shared/helpers/error.helpers";
import { CustomError } from "shared/models/exceptions/custom-error.class";
import { tryit } from "shared/helpers/error.helpers";
export class LocalMapsManagerService {
private static instance: LocalMapsManagerService;
@@ -326,6 +326,110 @@ export class LocalMapsManagerService {
return null;
}
public importMaps(zipPaths: string[], version?: BSVersion): Observable<Progression<BsmLocalMap>> {
return new Observable<Progression<BsmLocalMap>>(obs => {
let unsubscribed = false;
let progress: Progression<BsmLocalMap> = { total: 0, current: 0 };
let nbImportedMaps = 0;
(async () => {
const mapsPath = await this.getMapsFolderPath(version);
for(const zipPath of zipPaths) {
if(unsubscribed) {
log.info("Maps importation from zip has been cancelled");
return;
}
progress = { total: 0, current: 0 };
obs.next(progress); // reset progress for each zip
if(!pathExistsSync(zipPath)) { continue; }
const zip = new StreamZip.async({ file: zipPath });
const { result: zipEntries, error } = await tryit(() => zip.entries());
const zipEntriesValues = Object.values(zipEntries);
if(error) {
log.error("Could not read zip entries", zipPath, error);
await zip.close();
continue;
}
const mapsFolders = zipEntriesValues.reduce((acc, entry) => {
if(!/(^|\/)[Ii]nfo\.dat$/.test(entry.name)){ return acc; }
acc.push(path.dirname(entry.name));
return acc;
}, []);
if(mapsFolders.length === 0) {
log.warn("No maps \"info.dat\" found in zip", zipPath);
}
progress.total = mapsFolders.length;
obs.next(progress);
for(const folder of mapsFolders) {
if(unsubscribed) {
log.info("Maps importation from zip has been cancelled");
await zip.close();
return;
}
const isRoot = folder === ".";
const dest = isRoot ? path.join(mapsPath, path.basename(zipPath, ".zip")) : path.join(mapsPath, folder);
let extract: () => Promise<BsmLocalMap>;
if(isRoot){
const entries = zipEntriesValues.filter(entry => entry.isFile && path.dirname(entry.name) === ".");
extract = async () => {
await Promise.all(entries.map(entry => {
log.info("Extracting", `"${entry.name}"`, "from", `"${zipPath}"`, "into", `"${path.join(dest, path.basename(entry.name))}"`);
return zip.extract(entry.name, path.join(dest, path.basename(entry.name)));
}));
return this.loadMapInfoFromPath(dest);
};
} else {
extract = async () => {
log.info("Extracting", `"${folder}"`, "from", `"${zipPath}"`, "into", `"${mapsPath}"`);
await zip.extract(folder, dest);
return this.loadMapInfoFromPath(dest);
}
}
await ensureDir(dest);
const { result: bsmMap, error } = await tryit(extract);
if(error) {
log.error("Could not extract map", zipPath, folder, mapsPath, error);
continue;
}
nbImportedMaps++;
progress.current++;
progress.data = bsmMap;
obs.next(progress);
}
await zip.close();
}
})()
.then(() => {
if(!nbImportedMaps){
throw new CustomError("No \"Info.dat\" file located in any of the zip files", "invalid-zip");
}
return log.info("Successfully imported", nbImportedMaps, "maps from", zipPaths.length, "zips");
})
.catch(e => obs.error(e))
.finally(() => obs.complete());
return () => {
unsubscribed = true;
};
});
}
public async downloadMap(map: BsvMapDetail, version?: BSVersion): Promise<BsmLocalMap> {
if (!map.versions.at(0).hash) {
@@ -14,7 +14,7 @@ import { BsmButton } from "../shared/bsm-button.component";
import { MapIcon } from "../svgs/icons/map-icon.component";
import { PlaylistIcon } from "../svgs/icons/playlist-icon.component";
import { useObservable } from "renderer/hooks/use-observable.hook";
import { BehaviorSubject, of } from "rxjs";
import { BehaviorSubject, lastValueFrom, of } from "rxjs";
import { LocalPlaylistsListPanel, LocalPlaylistsListRef } from "./playlists/local-playlists-list-panel.component";
import { PlaylistsManagerService } from "renderer/services/playlists-manager.service";
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
@@ -23,6 +23,9 @@ import { LocalBPListsDetails } from "shared/models/playlists/local-playlist.mode
import { PlaylistDownloaderService } from "renderer/services/playlist-downloader.service";
import { LocalPlaylistFilter, LocalPlaylistFilterPanel } from "./playlists/local-playlist-filter-panel.component";
import { noop } from "shared/helpers/function.helpers";
import { Dropzone } from "../shared/dropzone.component";
import { NotificationService } from "renderer/services/notification.service";
import { logRenderError } from "renderer";
type Props = {
readonly version?: BSVersion;
@@ -42,6 +45,7 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
const mapsDownloader = useService(MapsDownloaderService);
const playlistsManager = useService(PlaylistsManagerService);
const playlistsDownloader = useService(PlaylistDownloaderService);
const notifications = useService(NotificationService);
const t = useTranslation();
const [tabIndex, setTabIndex] = useState(0);
@@ -101,6 +105,27 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
return playlistsManager.unlinkVersion(version);
}
const handleFileDrop = async (files: FileList) => {
const zipMimeTypes = ["application/zip", "application/zip-compressed", "application/x-zip-compressed"];
const paths: string[] = Array.from(files).reduce((acc, file) => {
if (zipMimeTypes.includes(file.type)) {
acc.push(window.electron.webUtils.getPathForFile(file));
}
return acc;
}, []);
if (paths.length === 0) {
notifications.notifyError({
title: "notifications.maps.import-map.titles.error",
desc: "notifications.maps.import-map.msgs.only-accept-zip"
});
return;
}
const import$ = mapsManager.importMaps(paths, version);
return lastValueFrom(import$).catch(logRenderError);
}
const dropDownItems = ((): DropDownItem[] => {
if (tabIndex === 0) {
return [
@@ -175,7 +200,17 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
]}
>
<InstalledMapsContext.Provider value={mapsContextValue}>
<LocalMapsListPanel className="w-full h-full shrink-0" isActive={isActive && tabIndex === 0} ref={mapsRef} version={version} filter={mapFilter} search={search} linkedState={mapsLinkedState} />
<Dropzone
className="w-full h-full shrink-0"
onDrop={event => {
handleFileDrop(event.dataTransfer.files);
}}
text={t("pages.version-viewer.maps.tabs.maps.drop-zone.text")}
subtext={t("pages.version-viewer.maps.tabs.maps.drop-zone.subtext")}
>
<LocalMapsListPanel className="w-full h-full shrink-0" isActive={isActive && tabIndex === 0} ref={mapsRef} version={version} filter={mapFilter} search={search} linkedState={mapsLinkedState} />
</Dropzone>
<LocalPlaylistsListPanel className="w-full h-full shrink-0" isActive={isActive && tabIndex === 1} ref={playlistsRef} version={version} linkedState={playlistLinkedState} filter={playlistFilter} search={search}/>
</InstalledMapsContext.Provider>
</BsContentTabPanel>
@@ -5,7 +5,7 @@ import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
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";
import { bufferTime, last, tap } from "rxjs/operators";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import BeatConflict from "../../../../../assets/images/apngs/beat-conflict.png";
import { BsmImage } from "../../shared/bsm-image.component";
@@ -104,19 +104,24 @@ export const LocalMapsListPanel = forwardRef<LocalMapsListPanelRef, Props>(({ ve
useEffect(() => {
let sub: Subscription;
const subs: Subscription[] = [];
if(isActiveOnce){
sub = mapsDownloader.lastDownloadedMap$.subscribe({ next: ({map, version: targetVersion}) => {
subs.push(mapsDownloader.lastDownloadedMap$.subscribe({ next: ({map, version: targetVersion}) => {
if (!equal(targetVersion, version)) {
return;
}
setMaps((maps$.value ? [map, ...maps$.value] : [map]));
}});
}}));
subs.push(mapsManager.$onMapImported(version).pipe(bufferTime(500)).subscribe({ next: newMaps => {
const maps = (maps$.value ?? []).filter(map => !newMaps.some(newMap => newMap.path === map.path));
setMaps([...newMaps, ...maps]);
}}));
}
return () => {
sub?.unsubscribe();
subs.forEach(s => s.unsubscribe());
}
}, [isActiveOnce, version])
@@ -0,0 +1,89 @@
import { AnimatePresence, motion } from "framer-motion";
import { DragEvent, ReactNode, useCallback, useState } from "react";
import { BsmImage } from "./bsm-image.component";
import BeatImpatient from "../../../../assets/images/apngs/beat-impatient.png";
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
import Color from "color";
type Props = Readonly<{
children?: ReactNode;
className?: string;
text?: string;
subtext?: string;
onDrop?: (event: DragEvent<HTMLDivElement>) => void;
}>;
// Supports drop and drop functionality for files.
export function Dropzone({
children,
className,
text,
subtext,
onDrop,
}: Props) {
const [dragCount, setDragCount] = useState(0);
const [dropZoneDragging, setDropZoneDragging] = useState(false);
const themeColor = useThemeColor("first-color");
const color = new Color(themeColor).lighten(.25).saturate(.8).hex();
const handleDrop = useCallback((event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
event.stopPropagation();
setDragCount(0);
if (onDrop) onDrop(event);
}, []);
const onDragOver = useCallback((event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
event.stopPropagation();
}, []);
const onDragEnter = useCallback((event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
event.stopPropagation();
setDragCount(prev => prev + 1);
}, []);
const onDragLeave = useCallback((event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
event.stopPropagation();
setDragCount(prev => prev - 1);
}, []);
return (
<div className={className} onDragEnter={onDragEnter} onDragLeave={onDragLeave}>
<AnimatePresence>
{dragCount ? (
<div onDragEnter={onDragEnter} onDragLeave={onDragLeave} onDrop={onDragLeave} className="absolute size-full top-0 right-0 z-10">
<motion.div className="absolute size-full top-0 right-0 bg-black" initial={{ opacity: 0 }} animate={{ opacity: .6 }} exit={{ opacity: 0 }}/>
<motion.div className="absolute size-full top-0 right-0 flex justify-center items-center p-5" initial={{ y: "100%" }} animate={{ y: "0" }} exit={{ y: "100%" }}>
<div className="h-72 max-h-full w-full max-w-lg sm:h-96 sm:max-w-2xl bg-theme-3 rounded-md shadow-black shadow-lg p-7">
<div
onDragOver={onDragOver}
onDrop={handleDrop}
onDragEnter={() => setDropZoneDragging(true)}
onDragLeave={() => setDropZoneDragging(false)}
className="relative size-full z-[1] border-dashed border-3 rounded-md flex flex-col gap-1 justify-center items-center"
style={{ borderColor: color }}
>
<div className="absolute size-full top-0 right-0 pointer-events-none -z-[1] transition-opacity duration-150" style={{
backgroundColor: "transparent",
backgroundImage: `radial-gradient(${color} 1px, transparent 0)`,
backgroundSize: "60px 60px",
backgroundPosition: "5px",
opacity: dropZoneDragging ? .7 : .2,
}}/>
<BsmImage className="size-28 pointer-events-none" image={BeatImpatient}/>
<span className="text-2xl font-bold pointer-events-none">{text}</span>
<span className="pointer-events-none">{subtext}</span>
</div>
</div>
</motion.div>
</div>
): undefined}
</AnimatePresence>
{children}
</div>
);
}
+5
View File
@@ -1,3 +1,5 @@
import { webUtils } from "electron";
declare global {
interface Window {
electron: {
@@ -13,6 +15,9 @@ declare global {
basename: (path: string) => string;
join: (...args: string[]) => string;
};
webUtils: {
getPathForFile: typeof webUtils.getPathForFile;
};
};
}
}
@@ -71,7 +71,7 @@ export class MapsDownloaderService {
if (this.os.isOffline) {
return null;
}
return this.ipc.sendV2("download-map", { map, version });
return this.ipc.sendV2("bs-maps.download-map", { map, version });
}
public async openDownloadMapModal(version?: BSVersion, ownedMaps: BsmLocalMap[] = []): Promise<ModalResponse<void>> {
+66 -1
View File
@@ -9,12 +9,13 @@ import { DeleteMapsModal } from "renderer/components/modal/modal-types/delete-ma
import { ProgressBarService } from "./progress-bar.service";
import { NotificationService } from "./notification.service";
import { ConfigurationService } from "./configuration.service";
import { map, last, catchError } from "rxjs/operators";
import { map, last, catchError, filter, distinctUntilChanged } 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";
import equal from "fast-deep-equal";
export class MapsManagerService {
private static instance: MapsManagerService;
@@ -39,6 +40,8 @@ export class MapsManagerService {
private readonly lastLinkedVersion$: Subject<BSVersion> = new Subject();
private readonly lastUnlinkedVersion$: Subject<BSVersion> = new Subject();
private readonly lastImportedMap$: Subject<{ version: BSVersion, map: BsmLocalMap }> = new Subject();
private constructor() {
this.ipcService = IpcService.getInstance();
this.modal = ModalService.getInstance();
@@ -196,6 +199,64 @@ export class MapsManagerService {
})
}
public importMaps(paths: string[], version?: BSVersion): Observable<Progression<BsmLocalMap>> {
if(!this.progressBar.require()){
return throwError(() => new Error("Another operation is already in progress (importMaps)"));
}
return new Observable<Progression<BsmLocalMap>>(obs => {
const subs: Subscription[] = [];
(async () => {
const import$ = this.ipcService.sendV2("bs-maps.import-maps", { paths, version });
this.progressBar.show(import$.pipe(
catchError(() => of()),
map(progress => ({
progression: (progress.current / progress.total) * 100,
label: progress.data?.songDetails?.name
} as ProgressionInterface))
));
subs.push(import$.subscribe(obs));
subs.push(import$.pipe(catchError(() => of()), map(progress => progress?.data), filter(Boolean), distinctUntilChanged((a, b) => equal(a, b))).subscribe({ next: map => {
this.lastImportedMap$.next({ version, map });
}}));
return lastValueFrom(import$);
})()
.then(progress => {
if (progress.current === progress.total) {
this.notifications.notifySuccess({
title: "notifications.maps.import-map.titles.success",
desc: "notifications.maps.import-map.msgs.success",
});
} else if (progress.current > 0) {
this.notifications.notifySuccess({
title: "notifications.maps.import-map.titles.success",
desc: "notifications.maps.import-map.msgs.some-success",
});
}
}).catch(err => {
this.notifications.notifyError({
title: "notifications.maps.import-map.titles.error",
desc: ["invalid-zip"].includes(err?.code)
? `notifications.maps.import-map.msgs.${err.code}`
: "misc.unknown"
});
obs.error(err);
})
.finally(() => obs.complete());
return () => {
if(subs.length){
this.progressBar.hide();
subs.forEach(s => s.unsubscribe());
}
}
});
}
public getMapsInfoFromHashs(hashs: string[]): Observable<SongDetails[]> {
return this.ipcService.sendV2("get-maps-info-from-cache", hashs);
}
@@ -220,6 +281,10 @@ export class MapsManagerService {
return this.lastUnlinkedVersion$.asObservable();
}
public $onMapImported(version: BSVersion): Observable<BsmLocalMap> {
return this.lastImportedMap$.pipe(filter(newMap => equal(newMap.version, version)), map(m => m.map));
}
public $mapsFolderLinkState(version: BSVersion): Observable<FolderLinkState> {
return this.linker.$folderLinkedState(version, MapsManagerService.RELATIVE_MAPS_FOLDER);
}
+2 -1
View File
@@ -58,7 +58,8 @@ export interface IpcChannelMapping {
"load-version-maps": { request: BSVersion, response: BsmLocalMapsProgress};
"delete-maps": { request: BsmLocalMap[], response: DeleteMapsProgress };
"export-maps": { request: { version: BSVersion; maps: BsmLocalMap[]; outPath: string }, response: Progression };
"download-map": { request: { map: BsvMapDetail; version: BSVersion }, response: BsmLocalMap };
"bs-maps.import-maps": { request: { paths: string[]; version: BSVersion | undefined }, response: Progression<BsmLocalMap> };
"bs-maps.download-map": { request: { map: BsvMapDetail; version: BSVersion | undefined }, response: BsmLocalMap };
"last-downloaded-map": { request: void, response: { version?: BSVersion, map: BsmLocalMap } };
"one-click-install-map": { request: BsvMapDetail, response: void };
"register-maps-deep-link": { request: void, response: boolean };