[feat-75] support drag and drop for map zip files

This commit is contained in:
silentrald
2024-09-06 00:01:41 +08:00
parent d5b7dff7a9
commit 0f3de55e9e
16 changed files with 326 additions and 6 deletions
@@ -23,6 +23,8 @@ 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";
type Props = {
readonly version?: BSVersion;
@@ -42,6 +44,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 +104,26 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
return playlistsManager.unlinkVersion(version);
}
const handleFileDrop = async (file: File) => {
if (file.type !== "application/zip") {
notifications.notifyError({
title: "notifications.maps.import-map.titles.error",
desc: "notifications.maps.import-map.msgs.zip-accept-only"
});
return;
}
const localMap = await mapsManager.importMap(file.path, version);
if (localMap) {
notifications.notifySuccess({
title: "notifications.maps.import-map.titles.success",
desc: t("notifications.maps.import-map.msgs.success", {
mapName: localMap.rawInfo._songName
})
});
}
}
const dropDownItems = ((): DropDownItem[] => {
if (tabIndex === 0) {
return [
@@ -175,7 +198,20 @@ 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[0]);
}}
overlay={
<div className="text-3xl pointer-events-none">
Drop map files here 😃
</div>
}
>
<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>
@@ -0,0 +1,73 @@
import React, { ReactNode, useState } from "react";
type Props = Readonly<{
children?: ReactNode;
className?: string;
overlay?: ReactNode;
overlayColor?: string;
overlayZIndex?: number;
onDrop?: (event: React.DragEvent<HTMLDivElement>) => void;
}>;
// Supports drop and drop functionality for files.
export function Dropzone({
children,
className,
overlay,
overlayColor,
overlayZIndex,
onDrop
}: Props) {
const [dragging, setDragging] = useState(false);
const normalizeZIndex = (zIndex?: number) => {
return (!zIndex || zIndex <= 0) ? 50 : zIndex;
}
const renderFileOverlay = () => {
return (
<div
className="absolute w-full h-full flex items-center justify-center"
style={{
backgroundColor: overlayColor || "#000000CC",
zIndex: normalizeZIndex(overlayZIndex),
}}
onDrop={event => {
event.preventDefault();
event.stopPropagation();
setDragging(false);
if (onDrop) onDrop(event);
}}
onDragOver={event => {
event.preventDefault();
event.stopPropagation();
}}
onDragEnter={event => {
// Should consume the event 1 more time to avoid multiple onDragEnter
// triggers by the parent div
event.preventDefault();
event.stopPropagation();
}}
onDragLeave={event => {
event.preventDefault();
event.stopPropagation();
setDragging(false);
}}
>
{overlay}
</div>
)
}
return (
<div className={className} onDragEnter={event => {
event.preventDefault();
event.stopPropagation();
setDragging(true);
}}>
{dragging && renderFileOverlay()}
{children}
</div>
);
}
@@ -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>> {
@@ -196,6 +196,27 @@ export class MapsManagerService {
})
}
public async importMap(path: string, version?: BSVersion): Promise<BsmLocalMap | null> {
try {
if (!this.progressBar.require()) {
return null;
}
return await lastValueFrom(this.ipcService.sendV2(
"bs-maps.import-map",
{ path, version }
));
} catch (error: any) {
this.notifications.notifyError({
title: "notifications.maps.import-map.titles.error",
desc: ["not-found-zip", "invalid-zip"].includes(error?.code)
? `notifications.maps.import-map.msgs.${error.code}`
: "Unknown"
});
return null;
}
}
public getMapsInfoFromHashs(hashs: string[]): Observable<SongDetails[]> {
return this.ipcService.sendV2("get-maps-info-from-cache", hashs);
}