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:
+20
-8
@@ -392,8 +392,10 @@ async function main() {
|
||||
emitSessionState();
|
||||
return { scene: next };
|
||||
});
|
||||
registerHandler(ipcChannels.project.importMedia, async ({ sceneId }) => {
|
||||
const { canceled, filePaths } = await dialog.showOpenDialog({
|
||||
registerHandler(ipcChannels.project.importMedia, async ({ sceneId, filePaths: pathsFromDrop }) => {
|
||||
let filePaths = pathsFromDrop ?? [];
|
||||
if (!pathsFromDrop) {
|
||||
const { canceled, filePaths: picked } = await dialog.showOpenDialog({
|
||||
properties: ['openFile', 'multiSelections'],
|
||||
filters: [
|
||||
{
|
||||
@@ -402,17 +404,21 @@ async function main() {
|
||||
},
|
||||
],
|
||||
});
|
||||
if (canceled || filePaths.length === 0) {
|
||||
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({
|
||||
registerHandler(ipcChannels.project.importCampaignAudio, async ({ filePaths: pathsFromDrop }) => {
|
||||
let filePaths = pathsFromDrop ?? [];
|
||||
if (!pathsFromDrop) {
|
||||
const { canceled, filePaths: picked } = await dialog.showOpenDialog({
|
||||
properties: ['openFile', 'multiSelections'],
|
||||
filters: [
|
||||
{
|
||||
@@ -421,11 +427,13 @@ async function main() {
|
||||
},
|
||||
],
|
||||
});
|
||||
if (canceled || filePaths.length === 0) {
|
||||
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();
|
||||
return { canceled: false as const, ...result };
|
||||
@@ -435,7 +443,9 @@ async function main() {
|
||||
emitSessionState();
|
||||
return { project };
|
||||
});
|
||||
registerHandler(ipcChannels.project.importScenePreview, async ({ sceneId }) => {
|
||||
registerHandler(ipcChannels.project.importScenePreview, async ({ sceneId, filePath: pathFromDrop }) => {
|
||||
let filePath = pathFromDrop;
|
||||
if (!filePath) {
|
||||
const { canceled, filePaths } = await dialog.showOpenDialog({
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
@@ -450,7 +460,9 @@ async function main() {
|
||||
if (!project) throw new Error('No open project');
|
||||
return { project, assetId: null, background: false };
|
||||
}
|
||||
const result = await projectStore.importScenePreviewMedia(sceneId, filePaths[0]);
|
||||
filePath = filePaths[0];
|
||||
}
|
||||
const result = await projectStore.importScenePreviewMedia(sceneId, filePath);
|
||||
emitSessionState();
|
||||
emitScenePreviewImportProgress({
|
||||
sceneId,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { contextBridge } from 'electron';
|
||||
import { contextBridge, webUtils } from 'electron';
|
||||
|
||||
import type { IpcEventMap, IpcInvokeMap } from '../shared/ipc/contracts';
|
||||
|
||||
@@ -10,9 +10,14 @@ export type DndApi = {
|
||||
payload: IpcInvokeMap[K]['req'],
|
||||
) => Promise<IpcInvokeMap[K]['res']>;
|
||||
on: <K extends keyof IpcEventMap>(channel: K, listener: (payload: IpcEventMap[K]) => void) => () => void;
|
||||
getPathForFile: (file: File) => string;
|
||||
};
|
||||
|
||||
const api: DndApi = { invoke, on };
|
||||
const api: DndApi = {
|
||||
invoke,
|
||||
on,
|
||||
getPathForFile: (file) => webUtils.getPathForFile(file),
|
||||
};
|
||||
|
||||
contextBridge.exposeInMainWorld('dnd', api);
|
||||
|
||||
|
||||
@@ -661,6 +661,15 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
transition:
|
||||
border-color 0.15s ease,
|
||||
background 0.15s ease;
|
||||
}
|
||||
|
||||
.previewBoxDragOver {
|
||||
border-color: var(--accent);
|
||||
border-style: dashed;
|
||||
background: var(--accent-fill-soft);
|
||||
}
|
||||
|
||||
.previewFill {
|
||||
@@ -754,6 +763,32 @@
|
||||
padding: 10px;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
position: relative;
|
||||
transition:
|
||||
border-color 0.15s ease,
|
||||
background 0.15s ease;
|
||||
}
|
||||
|
||||
.audioDropDragOver {
|
||||
border-color: var(--accent);
|
||||
background: var(--accent-fill-soft);
|
||||
}
|
||||
|
||||
.dropHintOverlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
border-radius: inherit;
|
||||
background: var(--accent-fill-soft-2);
|
||||
color: var(--accent);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.audioList {
|
||||
|
||||
@@ -25,6 +25,12 @@ import { Button, Input } from '../shared/ui/controls';
|
||||
import { LayoutShell } from '../shared/ui/LayoutShell';
|
||||
import { useAssetUrl } from '../shared/useAssetImageUrl';
|
||||
|
||||
import {
|
||||
filterAudioFilePaths,
|
||||
pickFirstPreviewFilePath,
|
||||
useFileDropZone,
|
||||
} from './fileDrop';
|
||||
|
||||
import type { SceneImportResolution, StorylineImportMergeReport, StorylineSelection } from '../../shared/graph/storylineExportImport';
|
||||
import {
|
||||
buildSceneResolutionsForImport,
|
||||
@@ -765,6 +771,18 @@ export function EditorApp() {
|
||||
}
|
||||
})();
|
||||
}}
|
||||
onDropAudio={(filePaths) => {
|
||||
void (async () => {
|
||||
try {
|
||||
await actions.importCampaignAudioFromPaths(filePaths);
|
||||
} catch (e) {
|
||||
setAppNotice({
|
||||
title: t('common.error'),
|
||||
message: e instanceof Error ? e.message : String(e),
|
||||
});
|
||||
}
|
||||
})();
|
||||
}}
|
||||
/>
|
||||
<div className={styles.spacer18} />
|
||||
<div className={styles.inspectorTitle}>{t('scenes.inspectorScene')}</div>
|
||||
@@ -834,6 +852,34 @@ export function EditorApp() {
|
||||
void actions.updateScene(sid, { previewRotationDeg })
|
||||
}
|
||||
onUploadMedia={() => void actions.importMediaToScene(sid)}
|
||||
onDropAudio={(filePaths) => {
|
||||
void (async () => {
|
||||
try {
|
||||
await actions.importMediaToSceneFromPaths(sid, filePaths);
|
||||
} catch (e) {
|
||||
setAppNotice({
|
||||
title: t('common.error'),
|
||||
message: e instanceof Error ? e.message : String(e),
|
||||
});
|
||||
}
|
||||
})();
|
||||
}}
|
||||
onDropPreview={(filePath) => {
|
||||
if (previewBusy) return;
|
||||
setPreviewDialogSceneId(sid);
|
||||
void (async () => {
|
||||
try {
|
||||
await actions.importScenePreviewFromPath(sid, filePath);
|
||||
} catch (e) {
|
||||
setAppNotice({
|
||||
title: t('common.error'),
|
||||
message: e instanceof Error ? e.message : String(e),
|
||||
});
|
||||
} finally {
|
||||
setPreviewDialogSceneId((cur) => (cur === sid ? null : cur));
|
||||
}
|
||||
})();
|
||||
}}
|
||||
onSideStoryLineTitleChange={(graphNodeId, title) =>
|
||||
void actions.updateSideStoryLineTitle(graphNodeId, title)
|
||||
}
|
||||
@@ -1886,6 +1932,8 @@ type SceneInspectorProps = {
|
||||
onClearPreview: () => void;
|
||||
onRotatePreview: (deg: 0 | 90 | 180 | 270) => void;
|
||||
onUploadMedia: () => void;
|
||||
onDropAudio: (filePaths: string[]) => void;
|
||||
onDropPreview: (filePath: string) => void;
|
||||
onSideStoryLineTitleChange: (graphNodeId: GraphNodeId, title: string) => void;
|
||||
};
|
||||
|
||||
@@ -1894,6 +1942,7 @@ type CampaignInspectorProps = {
|
||||
audioRefs: SceneAudioRef[];
|
||||
onAudioRefsChange: (next: SceneAudioRef[]) => void;
|
||||
onUploadAudio: () => void;
|
||||
onDropAudio: (filePaths: string[]) => void;
|
||||
};
|
||||
|
||||
function CampaignInspector({
|
||||
@@ -1901,13 +1950,27 @@ function CampaignInspector({
|
||||
audioRefs,
|
||||
onAudioRefsChange,
|
||||
onUploadAudio,
|
||||
onDropAudio,
|
||||
}: CampaignInspectorProps) {
|
||||
const { t } = useEditorI18n();
|
||||
const audioById = useMemo(() => new Map(audioRefs.map((a) => [a.assetId, a])), [audioRefs]);
|
||||
const audioDrop = useFileDropZone({
|
||||
onDropPaths: onDropAudio,
|
||||
filterPaths: filterAudioFilePaths,
|
||||
});
|
||||
return (
|
||||
<div className={styles.sceneInspector}>
|
||||
<div className={styles.labelSm}>{t('campaign.label')}</div>
|
||||
<div className={styles.audioDrop}>
|
||||
<div
|
||||
className={[styles.audioDrop, audioDrop.dragOver ? styles.audioDropDragOver : ''].join(' ')}
|
||||
onDragEnter={audioDrop.onDragEnter}
|
||||
onDragLeave={audioDrop.onDragLeave}
|
||||
onDragOver={audioDrop.onDragOver}
|
||||
onDrop={audioDrop.onDrop}
|
||||
>
|
||||
{audioDrop.dragOver ? (
|
||||
<div className={styles.dropHintOverlay}>{t('drop.hintAudio')}</div>
|
||||
) : null}
|
||||
{mediaAssets.filter((a) => a.type === 'audio').length === 0 ? (
|
||||
<div className={[styles.muted, styles.spanSm].join(' ')}>{t('campaign.noFiles')}</div>
|
||||
) : (
|
||||
@@ -1998,11 +2061,24 @@ function SceneInspector({
|
||||
onClearPreview,
|
||||
onRotatePreview,
|
||||
onUploadMedia,
|
||||
onDropAudio,
|
||||
onDropPreview,
|
||||
onSideStoryLineTitleChange,
|
||||
}: SceneInspectorProps) {
|
||||
const { t } = useEditorI18n();
|
||||
const previewUrl = useAssetUrl(previewAssetId);
|
||||
const audioById = useMemo(() => new Map(audioRefs.map((a) => [a.assetId, a])), [audioRefs]);
|
||||
const previewDrop = useFileDropZone({
|
||||
disabled: previewBusy,
|
||||
onDropPaths: (paths) => {
|
||||
const filePath = pickFirstPreviewFilePath(paths);
|
||||
if (filePath) onDropPreview(filePath);
|
||||
},
|
||||
});
|
||||
const sceneAudioDrop = useFileDropZone({
|
||||
onDropPaths: onDropAudio,
|
||||
filterPaths: filterAudioFilePaths,
|
||||
});
|
||||
return (
|
||||
<div className={styles.sceneInspector}>
|
||||
<div className={styles.labelSm}>{t('scene.title')}</div>
|
||||
@@ -2032,7 +2108,16 @@ function SceneInspector({
|
||||
<div className={styles.spacer6} />
|
||||
<div className={styles.labelSm}>{t('scene.preview')}</div>
|
||||
<div className={styles.hint}>{t('scene.previewHint')}</div>
|
||||
<div className={styles.previewBox}>
|
||||
<div
|
||||
className={[styles.previewBox, previewDrop.dragOver ? styles.previewBoxDragOver : ''].join(' ')}
|
||||
onDragEnter={previewDrop.onDragEnter}
|
||||
onDragLeave={previewDrop.onDragLeave}
|
||||
onDragOver={previewDrop.onDragOver}
|
||||
onDrop={previewDrop.onDrop}
|
||||
>
|
||||
{previewDrop.dragOver ? (
|
||||
<div className={styles.dropHintOverlay}>{t('drop.hintPreview')}</div>
|
||||
) : null}
|
||||
{previewUrl && previewAssetType === 'image' ? (
|
||||
<div className={styles.previewFill}>
|
||||
<RotatedImage
|
||||
@@ -2107,7 +2192,16 @@ function SceneInspector({
|
||||
) : null}
|
||||
<div className={styles.spacer6} />
|
||||
<div className={styles.labelSm}>{t('scene.audio')}</div>
|
||||
<div className={styles.audioDrop}>
|
||||
<div
|
||||
className={[styles.audioDrop, sceneAudioDrop.dragOver ? styles.audioDropDragOver : ''].join(' ')}
|
||||
onDragEnter={sceneAudioDrop.onDragEnter}
|
||||
onDragLeave={sceneAudioDrop.onDragLeave}
|
||||
onDragOver={sceneAudioDrop.onDragOver}
|
||||
onDrop={sceneAudioDrop.onDrop}
|
||||
>
|
||||
{sceneAudioDrop.dragOver ? (
|
||||
<div className={styles.dropHintOverlay}>{t('drop.hintAudio')}</div>
|
||||
) : null}
|
||||
{mediaAssets.filter((a) => a.type === 'audio').length === 0 ? (
|
||||
<div className={[styles.muted, styles.spanSm].join(' ')}>{t('campaign.noFiles')}</div>
|
||||
) : (
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
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 };
|
||||
}
|
||||
@@ -313,6 +313,8 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
|
||||
'campaign.loop': 'Цикл',
|
||||
'campaign.removeTitle': 'Убрать из кампании',
|
||||
'campaign.upload': 'Загрузить',
|
||||
'drop.hintAudio': 'Перетащите аудиофайлы сюда',
|
||||
'drop.hintPreview': 'Перетащите изображение или видео',
|
||||
|
||||
'scene.title': 'НАЗВАНИЕ СЦЕНЫ',
|
||||
'scene.description': 'ОПИСАНИЕ',
|
||||
@@ -689,6 +691,8 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
|
||||
'campaign.loop': 'Loop',
|
||||
'campaign.removeTitle': 'Remove from campaign',
|
||||
'campaign.upload': 'Upload',
|
||||
'drop.hintAudio': 'Drop audio files here',
|
||||
'drop.hintPreview': 'Drop an image or video',
|
||||
|
||||
'scene.title': 'SCENE TITLE',
|
||||
'scene.description': 'DESCRIPTION',
|
||||
|
||||
@@ -34,6 +34,7 @@ type Actions = {
|
||||
createScene: () => Promise<void>;
|
||||
selectScene: (id: SceneId) => Promise<void>;
|
||||
importCampaignAudio: () => Promise<void>;
|
||||
importCampaignAudioFromPaths: (filePaths: string[]) => Promise<void>;
|
||||
updateCampaignAudios: (next: Project['campaignAudios']) => Promise<void>;
|
||||
updateScene: (
|
||||
sceneId: SceneId,
|
||||
@@ -53,7 +54,12 @@ type Actions = {
|
||||
) => Promise<void>;
|
||||
updateConnections: (sceneId: SceneId, connections: SceneId[]) => Promise<void>;
|
||||
importMediaToScene: (sceneId: SceneId) => Promise<void>;
|
||||
importMediaToSceneFromPaths: (sceneId: SceneId, filePaths: string[]) => Promise<void>;
|
||||
importScenePreview: (sceneId: SceneId) => Promise<{ assetId: AssetId | null; background: boolean }>;
|
||||
importScenePreviewFromPath: (
|
||||
sceneId: SceneId,
|
||||
filePath: string,
|
||||
) => Promise<{ assetId: AssetId | null; background: boolean }>;
|
||||
clearScenePreview: (sceneId: SceneId) => Promise<void>;
|
||||
updateSceneGraphNodePosition: (nodeId: GraphNodeId, x: number, y: number) => Promise<void>;
|
||||
addSceneGraphNode: (sceneId: SceneId, x: number, y: number) => Promise<void>;
|
||||
@@ -348,6 +354,14 @@ export function useProjectState(licenseActive: boolean, opts?: ProjectStateOpts)
|
||||
await refreshProjects();
|
||||
};
|
||||
|
||||
const importCampaignAudioFromPaths = async (filePaths: string[]) => {
|
||||
if (filePaths.length === 0) return;
|
||||
const res = await api.invoke(ipcChannels.project.importCampaignAudio, { filePaths });
|
||||
if (res.imported.length === 0) return;
|
||||
setState((s) => ({ ...s, project: res.project }));
|
||||
await refreshProjects();
|
||||
};
|
||||
|
||||
const updateCampaignAudios = async (next: Project['campaignAudios']) => {
|
||||
const res = await api.invoke(ipcChannels.project.updateCampaignAudios, { audios: next });
|
||||
setState((s) => ({ ...s, project: res.project }));
|
||||
@@ -422,6 +436,14 @@ export function useProjectState(licenseActive: boolean, opts?: ProjectStateOpts)
|
||||
await refreshProjects();
|
||||
};
|
||||
|
||||
const importMediaToSceneFromPaths = async (sceneId: SceneId, filePaths: string[]) => {
|
||||
if (filePaths.length === 0) return;
|
||||
const res = await api.invoke(ipcChannels.project.importMedia, { sceneId, filePaths });
|
||||
if (res.imported.length === 0) return;
|
||||
setState((s) => ({ ...s, project: res.project }));
|
||||
await refreshProjects();
|
||||
};
|
||||
|
||||
const importScenePreview = async (sceneId: SceneId) => {
|
||||
const res = await api.invoke(ipcChannels.project.importScenePreview, { sceneId });
|
||||
setState((s) => ({ ...s, project: res.project }));
|
||||
@@ -438,6 +460,22 @@ export function useProjectState(licenseActive: boolean, opts?: ProjectStateOpts)
|
||||
return { assetId: res.assetId, background: res.background };
|
||||
};
|
||||
|
||||
const importScenePreviewFromPath = async (sceneId: SceneId, filePath: string) => {
|
||||
const res = await api.invoke(ipcChannels.project.importScenePreview, { sceneId, filePath });
|
||||
setState((s) => ({ ...s, project: res.project }));
|
||||
if (res.assetId !== null && res.background) {
|
||||
setState((s) => ({
|
||||
...s,
|
||||
scenePreviewImports: {
|
||||
...s.scenePreviewImports,
|
||||
[sceneId]: { assetId: res.assetId, phase: 'queued' },
|
||||
},
|
||||
}));
|
||||
}
|
||||
await refreshProjects();
|
||||
return { assetId: res.assetId, background: res.background };
|
||||
};
|
||||
|
||||
const clearScenePreview = async (sceneId: SceneId) => {
|
||||
const res = await api.invoke(ipcChannels.project.clearScenePreview, { sceneId });
|
||||
setState((s) => ({ ...s, project: res.project }));
|
||||
@@ -650,11 +688,14 @@ export function useProjectState(licenseActive: boolean, opts?: ProjectStateOpts)
|
||||
createScene,
|
||||
selectScene,
|
||||
importCampaignAudio,
|
||||
importCampaignAudioFromPaths,
|
||||
updateCampaignAudios,
|
||||
updateScene,
|
||||
updateConnections,
|
||||
importMediaToScene,
|
||||
importMediaToSceneFromPaths,
|
||||
importScenePreview,
|
||||
importScenePreviewFromPath,
|
||||
clearScenePreview,
|
||||
updateSceneGraphNodePosition,
|
||||
addSceneGraphNode,
|
||||
|
||||
@@ -219,11 +219,11 @@ export type IpcInvokeMap = {
|
||||
res: { currentGraphNodeId: GraphNodeId | null; currentSceneId: SceneId | null };
|
||||
};
|
||||
[ipcChannels.project.importMedia]: {
|
||||
req: { sceneId: SceneId };
|
||||
req: { sceneId: SceneId; filePaths?: string[] };
|
||||
res: { project: Project; imported: MediaAsset[] };
|
||||
};
|
||||
[ipcChannels.project.importCampaignAudio]: {
|
||||
req: Record<string, never>;
|
||||
req: { filePaths?: string[] };
|
||||
res: { canceled: boolean; project: Project; imported: MediaAsset[] };
|
||||
};
|
||||
[ipcChannels.project.updateCampaignAudios]: {
|
||||
@@ -231,7 +231,7 @@ export type IpcInvokeMap = {
|
||||
res: { project: Project };
|
||||
};
|
||||
[ipcChannels.project.importScenePreview]: {
|
||||
req: { sceneId: SceneId };
|
||||
req: { sceneId: SceneId; filePath?: string };
|
||||
res: { project: Project; assetId: AssetId | null; background: boolean };
|
||||
};
|
||||
[ipcChannels.project.clearScenePreview]: {
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ function killTree(child) {
|
||||
}
|
||||
}
|
||||
|
||||
function waitForVite(url = 'http://127.0.0.1:5173/editor.html', timeoutMs = 60000) {
|
||||
function waitForVite(url = 'http://localhost:5173/editor.html', timeoutMs = 60000) {
|
||||
const started = Date.now();
|
||||
return new Promise((resolve, reject) => {
|
||||
const tick = () => {
|
||||
|
||||
@@ -57,6 +57,7 @@ export default defineConfig(({ mode }) => {
|
||||
},
|
||||
},
|
||||
server: {
|
||||
host: '127.0.0.1',
|
||||
port: 5173,
|
||||
strictPort: true,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user