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>
This commit is contained in:
Ivan Fontosh
2026-07-09 19:22:51 +08:00
parent c8ab9dd567
commit b7e6ff6915
10 changed files with 353 additions and 52 deletions
+55 -43
View File
@@ -392,39 +392,47 @@ async function main() {
emitSessionState();
return { scene: next };
});
registerHandler(ipcChannels.project.importMedia, async ({ sceneId }) => {
const { canceled, filePaths } = await dialog.showOpenDialog({
properties: ['openFile', 'multiSelections'],
filters: [
{
name: 'Видео и аудио',
extensions: ['mp4', 'webm', 'mov', 'mp3', 'wav', 'ogg', 'm4a', 'aac'],
},
],
});
if (canceled || filePaths.length === 0) {
const project = projectStore.getOpenProject();
if (!project) throw new Error('No open project');
return { project, imported: [] };
registerHandler(ipcChannels.project.importMedia, async ({ sceneId, filePaths: pathsFromDrop }) => {
let filePaths = pathsFromDrop ?? [];
if (!pathsFromDrop) {
const { canceled, filePaths: picked } = await dialog.showOpenDialog({
properties: ['openFile', 'multiSelections'],
filters: [
{
name: 'Видео и аудио',
extensions: ['mp4', 'webm', 'mov', 'mp3', 'wav', 'ogg', 'm4a', 'aac'],
},
],
});
if (canceled || picked.length === 0) {
const project = projectStore.getOpenProject();
if (!project) throw new Error('No open project');
return { project, imported: [] };
}
filePaths = picked;
}
const result = await projectStore.importMediaFiles(sceneId, filePaths);
emitSessionState();
return result;
});
registerHandler(ipcChannels.project.importCampaignAudio, async () => {
const { canceled, filePaths } = await dialog.showOpenDialog({
properties: ['openFile', 'multiSelections'],
filters: [
{
name: 'Аудио',
extensions: ['mp3', 'wav', 'ogg', 'm4a', 'aac'],
},
],
});
if (canceled || filePaths.length === 0) {
const project = projectStore.getOpenProject();
if (!project) throw new Error('No open project');
return { canceled: true as const, project, imported: [] };
registerHandler(ipcChannels.project.importCampaignAudio, async ({ filePaths: pathsFromDrop }) => {
let filePaths = pathsFromDrop ?? [];
if (!pathsFromDrop) {
const { canceled, filePaths: picked } = await dialog.showOpenDialog({
properties: ['openFile', 'multiSelections'],
filters: [
{
name: 'Аудио',
extensions: ['mp3', 'wav', 'ogg', 'm4a', 'aac'],
},
],
});
if (canceled || picked.length === 0) {
const project = projectStore.getOpenProject();
if (!project) throw new Error('No open project');
return { canceled: true as const, project, imported: [] };
}
filePaths = picked;
}
const result = await projectStore.importCampaignAudioFiles(filePaths);
emitSessionState();
@@ -435,22 +443,26 @@ async function main() {
emitSessionState();
return { project };
});
registerHandler(ipcChannels.project.importScenePreview, async ({ sceneId }) => {
const { canceled, filePaths } = await dialog.showOpenDialog({
properties: ['openFile'],
filters: [
{
name: 'Изображения и видео',
extensions: ['png', 'jpg', 'jpeg', 'webp', 'gif', 'bmp', 'mp4', 'webm', 'mov'],
},
],
});
if (canceled || !filePaths[0]) {
const project = projectStore.getOpenProject();
if (!project) throw new Error('No open project');
return { project, assetId: null, background: false };
registerHandler(ipcChannels.project.importScenePreview, async ({ sceneId, filePath: pathFromDrop }) => {
let filePath = pathFromDrop;
if (!filePath) {
const { canceled, filePaths } = await dialog.showOpenDialog({
properties: ['openFile'],
filters: [
{
name: 'Изображения и видео',
extensions: ['png', 'jpg', 'jpeg', 'webp', 'gif', 'bmp', 'mp4', 'webm', 'mov'],
},
],
});
if (canceled || !filePaths[0]) {
const project = projectStore.getOpenProject();
if (!project) throw new Error('No open project');
return { project, assetId: null, background: false };
}
filePath = filePaths[0];
}
const result = await projectStore.importScenePreviewMedia(sceneId, filePaths[0]);
const result = await projectStore.importScenePreviewMedia(sceneId, filePath);
emitSessionState();
emitScenePreviewImportProgress({
sceneId,