mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
Merge pull request #575 from silentrald/feat/75
[feat-75] support drag and drop for map zip files
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
+10
-5
@@ -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>
|
||||
);
|
||||
}
|
||||
Vendored
+5
@@ -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>> {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user