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
+43
View File
@@ -1,7 +1,9 @@
import React, { useCallback, useEffect, useState } from 'react';
import { createPortal, flushSync } from 'react-dom';
import { ipcChannels } from '../../shared/ipc/contracts';
import type { MaterialId, MaterialLegend, ProjectMaterial } from '../../shared/types';
import { getDndApi } from '../shared/dndApi';
import { Button, Input } from '../shared/ui/controls';
import { useAssetUrl } from '../shared/useAssetImageUrl';
@@ -38,10 +40,12 @@ export function MaterialEditModal({
onSave,
}: MaterialEditModalProps) {
const { t } = useEditorI18n();
const api = getDndApi();
const [name, setName] = useState('');
const [filePath, setFilePath] = useState<string | null>(null);
const [localPreviewUrl, setLocalPreviewUrl] = useState<string | null>(null);
const [saving, setSaving] = useState(false);
const [saveProgress, setSaveProgress] = useState<{ percent: number; detail: string } | null>(null);
const [error, setError] = useState<string | null>(null);
const existingUrl = useAssetUrl(initial?.assetId ?? null);
@@ -51,6 +55,7 @@ export function MaterialEditModal({
setFilePath(null);
setLocalPreviewUrl(null);
setSaving(false);
setSaveProgress(null);
setError(null);
}, [initial, open]);
@@ -60,6 +65,16 @@ export function MaterialEditModal({
};
}, [localPreviewUrl]);
useEffect(() => {
if (!open) return;
return api.on(ipcChannels.project.materialUpsertProgress, (evt) => {
setSaveProgress({
percent: Math.max(0, Math.min(100, Math.round(evt.percent))),
detail: evt.detail?.trim() || t('materials.savingWait'),
});
});
}, [api, open, t]);
useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => {
@@ -99,6 +114,9 @@ export function MaterialEditModal({
if (!open) return null;
const progressPercent = saveProgress?.percent ?? (saving ? 0 : 0);
const progressDetail = saveProgress?.detail ?? t('materials.savingWait');
return createPortal(
<>
<button
@@ -194,6 +212,7 @@ export function MaterialEditModal({
void (async () => {
flushSync(() => {
setSaving(true);
setSaveProgress({ percent: 0, detail: t('materials.savingWait') });
setError(null);
});
try {
@@ -203,6 +222,7 @@ export function MaterialEditModal({
setError(e instanceof Error ? e.message : String(e));
} finally {
setSaving(false);
setSaveProgress(null);
}
})();
}}
@@ -211,6 +231,29 @@ export function MaterialEditModal({
</Button>
</div>
</div>
{saving ? (
<div
className={styles.progressOverlay}
role="dialog"
aria-label={t('materials.savingProgress')}
aria-busy
>
<div className={styles.progressModal}>
<div className={styles.progressTitle}>{t('materials.savingTitle')}</div>
<div className={styles.previewSpinner} aria-hidden />
<div className={styles.progressBar}>
<div
className={styles.progressFill}
style={{ width: `${String(Math.max(0, Math.min(100, progressPercent)))}%` }}
/>
</div>
<div className={styles.progressMeta}>
<div>{progressDetail}</div>
<div>{progressPercent}%</div>
</div>
</div>
</div>
) : null}
</>,
document.body,
);