feat(materials): show save progress overlay while optimizing images
Block the editor with a spinner and percent stages during material upsert so long image optimization is visible and non-interactive. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+18
-5
@@ -83,6 +83,16 @@ function emitScenePreviewImportProgress(evt: ScenePreviewImportEvent): void {
|
||||
}
|
||||
}
|
||||
|
||||
function emitMaterialUpsertProgress(evt: {
|
||||
percent: number;
|
||||
stage: string;
|
||||
detail?: string;
|
||||
}): void {
|
||||
for (const win of BrowserWindow.getAllWindows()) {
|
||||
win.webContents.send(ipcChannels.project.materialUpsertProgress, evt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Отключение GPU ломает скорость вторичных окон (презентация/пульт — WebGL). По умолчанию не трогаем.
|
||||
* При чёрном экране в упакованной сборке: `DND_DISABLE_GPU=1`.
|
||||
@@ -647,11 +657,14 @@ async function main() {
|
||||
}
|
||||
filePath = filePaths[0];
|
||||
}
|
||||
const project = await projectStore.upsertMaterial({
|
||||
...(materialId ? { materialId } : {}),
|
||||
name,
|
||||
...(filePath ? { filePath } : {}),
|
||||
});
|
||||
const project = await projectStore.upsertMaterial(
|
||||
{
|
||||
...(materialId ? { materialId } : {}),
|
||||
name,
|
||||
...(filePath ? { filePath } : {}),
|
||||
},
|
||||
(p) => emitMaterialUpsertProgress(p),
|
||||
);
|
||||
syncMaterialsOverlayWithProject(project);
|
||||
emitMaterialsOverlayState();
|
||||
emitSessionState();
|
||||
|
||||
@@ -1117,11 +1117,17 @@ export class ZipProjectStore {
|
||||
* Создаёт или обновляет материал кампании.
|
||||
* При создании `filePath` обязателен; при обновлении можно сменить только имя или только картинку.
|
||||
*/
|
||||
async upsertMaterial(input: {
|
||||
materialId?: MaterialId;
|
||||
name: string;
|
||||
filePath?: string;
|
||||
}): Promise<Project> {
|
||||
async upsertMaterial(
|
||||
input: {
|
||||
materialId?: MaterialId;
|
||||
name: string;
|
||||
filePath?: string;
|
||||
},
|
||||
onProgress?: (p: { percent: number; stage: string; detail?: string }) => void,
|
||||
): Promise<Project> {
|
||||
const report = (percent: number, stage: string, detail?: string) => {
|
||||
onProgress?.({ percent, stage, ...(detail ? { detail } : {}) });
|
||||
};
|
||||
const open = this.openProject;
|
||||
if (!open) throw new Error('No open project');
|
||||
const name = input.name.trim();
|
||||
@@ -1133,6 +1139,8 @@ export class ZipProjectStore {
|
||||
throw new Error('Material name already exists');
|
||||
}
|
||||
|
||||
report(2, 'start', 'Подождите…');
|
||||
|
||||
let nextAssetId: AssetId | null = null;
|
||||
let stagedAsset: MediaAsset | null = null;
|
||||
if (input.filePath) {
|
||||
@@ -1142,13 +1150,16 @@ export class ZipProjectStore {
|
||||
if (!['.png', '.jpg', '.jpeg', '.webp'].includes(ext)) {
|
||||
throw new Error('Material must be an image (png/jpg/webp)');
|
||||
}
|
||||
report(8, 'read', 'Чтение изображения…');
|
||||
let buf = await fs.readFile(input.filePath);
|
||||
report(18, 'optimize', 'Оптимизация изображения…');
|
||||
try {
|
||||
const opt = await optimizeImageBufferVisuallyLossless(buf);
|
||||
if (opt.buffer && opt.buffer.length > 0) buf = Buffer.from(opt.buffer);
|
||||
} catch {
|
||||
// keep original buffer
|
||||
}
|
||||
report(72, 'write', 'Сохранение файла…');
|
||||
const sha256 = crypto.createHash('sha256').update(buf).digest('hex');
|
||||
const id = asAssetId(this.randomId());
|
||||
const orig = path.basename(input.filePath);
|
||||
@@ -1161,6 +1172,7 @@ export class ZipProjectStore {
|
||||
nextAssetId = id;
|
||||
}
|
||||
|
||||
report(88, 'project', 'Обновление проекта…');
|
||||
await this.updateProject((p) => {
|
||||
const materials = [...(p.materials ?? [])];
|
||||
const assets = { ...p.assets };
|
||||
@@ -1192,6 +1204,7 @@ export class ZipProjectStore {
|
||||
|
||||
const latest = this.getOpenProject();
|
||||
if (!latest) throw new Error('No open project');
|
||||
report(100, 'done', 'Готово');
|
||||
return latest;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user