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:
Ivan Fontosh
2026-07-25 14:50:35 +08:00
parent 9d82e74272
commit 4aa0f257d5
5 changed files with 93 additions and 10 deletions
+18 -5
View File
@@ -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;
}