Files
DndGamePlayer/app/renderer/editor/fileDrop.ts
T
Ivan Fontosh b7e6ff6915 feat(editor): drag-and-drop для аудио и превью сцены
Добавлена загрузка файлов перетаскиванием в блоки «Аудио игры», «Аудио сцены»
и «Превью сцены»: подсветка зоны, подсказки, фильтрация по типу, импорт без диалога.

IPC и main принимают пути файлов напрямую; для Electron 41 пути получаются через
webUtils.getPathForFile в preload (File.path в renderer больше недоступен).

Улучшен dev-запуск: Vite слушает 127.0.0.1, ожидание готовности по localhost.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 19:22:51 +08:00

110 lines
2.8 KiB
TypeScript

import { useCallback, useRef, useState, type DragEvent } from 'react';
import { getDndApi } from '../shared/dndApi';
const AUDIO_EXTENSIONS = new Set(['.mp3', '.wav', '.ogg', '.m4a', '.aac']);
const PREVIEW_EXTENSIONS = new Set([
'.png',
'.jpg',
'.jpeg',
'.webp',
'.gif',
'.bmp',
'.mp4',
'.webm',
'.mov',
]);
function fileExtension(filePath: string): string {
const dot = filePath.lastIndexOf('.');
return dot >= 0 ? filePath.slice(dot).toLowerCase() : '';
}
export function getDroppedFilePaths(e: DragEvent): string[] {
const files = e.dataTransfer?.files;
if (!files?.length) return [];
const getPathForFile = getDndApi().getPathForFile;
return Array.from(files)
.map((file) => getPathForFile(file))
.filter((path) => path.length > 0);
}
export function filterAudioFilePaths(paths: string[]): string[] {
return paths.filter((path) => AUDIO_EXTENSIONS.has(fileExtension(path)));
}
export function pickFirstPreviewFilePath(paths: string[]): string | null {
for (const path of paths) {
if (PREVIEW_EXTENSIONS.has(fileExtension(path))) return path;
}
return null;
}
function dragHasFiles(e: DragEvent): boolean {
return Array.from(e.dataTransfer?.types ?? []).includes('Files');
}
type UseFileDropZoneOptions = {
disabled?: boolean;
onDropPaths: (paths: string[]) => void;
filterPaths?: (paths: string[]) => string[];
};
export function useFileDropZone({
disabled = false,
onDropPaths,
filterPaths,
}: UseFileDropZoneOptions) {
const [dragOver, setDragOver] = useState(false);
const depthRef = useRef(0);
const onDragEnter = useCallback(
(e: DragEvent) => {
if (disabled || !dragHasFiles(e)) return;
e.preventDefault();
e.stopPropagation();
depthRef.current += 1;
setDragOver(true);
},
[disabled],
);
const onDragLeave = useCallback(
(e: DragEvent) => {
if (disabled) return;
e.preventDefault();
e.stopPropagation();
depthRef.current = Math.max(0, depthRef.current - 1);
if (depthRef.current === 0) setDragOver(false);
},
[disabled],
);
const onDragOver = useCallback(
(e: DragEvent) => {
if (disabled || !dragHasFiles(e)) return;
e.preventDefault();
e.stopPropagation();
e.dataTransfer.dropEffect = 'copy';
},
[disabled],
);
const onDrop = useCallback(
(e: DragEvent) => {
if (disabled || !dragHasFiles(e)) return;
e.preventDefault();
e.stopPropagation();
depthRef.current = 0;
setDragOver(false);
const raw = getDroppedFilePaths(e);
const paths = filterPaths ? filterPaths(raw) : raw;
if (paths.length === 0) return;
onDropPaths(paths);
},
[disabled, filterPaths, onDropPaths],
);
return { dragOver, onDragEnter, onDragLeave, onDragOver, onDrop };
}