feat(editor): show scene preview immediately and optimize in background

Return control after copying the original asset, then run image optimization
and thumbnail generation in the background with IPC progress updates.
Block duplicate preview uploads and scene creation while requests are in flight.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-07-03 22:38:51 +08:00
parent 83a326b0b9
commit 10bb7013e6
9 changed files with 514 additions and 209 deletions
+40 -6
View File
@@ -1,7 +1,6 @@
import { app, BrowserWindow, dialog, Menu, protocol } from 'electron';
import { ipcChannels, type SessionState } from '../shared/ipc/contracts';
import type { Project } from '../shared/types';
import { ipcChannels, type ScenePreviewImportEvent, type SessionState } from '../shared/ipc/contracts';
import {
PROJECT_ZIP_OPEN_DIALOG_FILTER,
PROJECT_ZIP_SAVE_DIALOG_FILTER,
@@ -10,6 +9,7 @@ import {
projectZipFileNameFromBase,
stripProjectZipExtension,
} from '../shared/project/projectZipExtension';
import type { Project } from '../shared/types';
import { EffectsStore } from './effects/effectsStore';
import { SceneDarknessStore } from './effects/sceneDarknessStore';
@@ -53,6 +53,12 @@ function emitZipProgress(evt: {
}
}
function emitScenePreviewImportProgress(evt: ScenePreviewImportEvent): void {
for (const win of BrowserWindow.getAllWindows()) {
win.webContents.send(ipcChannels.project.scenePreviewImportProgress, evt);
}
}
/**
* Отключение GPU ломает скорость вторичных окон (презентация/пульт — WebGL). По умолчанию не трогаем.
* При чёрном экране в упакованной сборке: `DND_DISABLE_GPU=1`.
@@ -374,7 +380,7 @@ async function main() {
registerHandler(ipcChannels.project.updateScene, async ({ sceneId, patch }) => {
const next = await projectStore.updateScene(sceneId, patch);
const project = projectStore.getOpenProject();
if (project && project.currentSceneId === sceneId && patch.darkenScene !== undefined) {
if (project?.currentSceneId === sceneId && patch.darkenScene !== undefined) {
syncSceneDarknessForProject(project);
emitSceneDarknessState();
}
@@ -442,11 +448,39 @@ async function main() {
if (canceled || !filePaths[0]) {
const project = projectStore.getOpenProject();
if (!project) throw new Error('No open project');
return { project };
return { project, assetId: null, background: false };
}
const project = await projectStore.importScenePreviewMedia(sceneId, filePaths[0]);
const result = await projectStore.importScenePreviewMedia(sceneId, filePaths[0]);
emitSessionState();
return { project };
emitScenePreviewImportProgress({
sceneId,
assetId: result.assetId,
phase: 'queued',
project: result.project,
});
void (async () => {
try {
emitScenePreviewImportProgress({ sceneId, assetId: result.assetId, phase: 'optimizing' });
const finalized = await projectStore.finalizeScenePreviewImport(sceneId, result.assetId);
if (finalized.changed) {
emitSessionState();
emitScenePreviewImportProgress({
sceneId,
assetId: result.assetId,
phase: 'done',
project: finalized.project,
});
}
} catch (e) {
emitScenePreviewImportProgress({
sceneId,
assetId: result.assetId,
phase: 'error',
message: e instanceof Error ? e.message : String(e),
});
}
})();
return result;
});
registerHandler(ipcChannels.project.clearScenePreview, async ({ sceneId }) => {
const project = await projectStore.clearScenePreview(sceneId);