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:
@@ -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,
|
||||
);
|
||||
|
||||
@@ -365,6 +365,9 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
|
||||
'materials.add': 'Добавить',
|
||||
'materials.addTitle': 'Новый материал',
|
||||
'materials.editTitle': 'Изменить материал',
|
||||
'materials.savingTitle': 'Сохранение материала',
|
||||
'materials.savingWait': 'Подождите…',
|
||||
'materials.savingProgress': 'Прогресс сохранения материала',
|
||||
'materials.edit': 'Изменить',
|
||||
'materials.search': 'Поиск материалов…',
|
||||
'materials.searchEmpty': 'Ничего не найдено.',
|
||||
@@ -912,6 +915,9 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
|
||||
'materials.add': 'Add',
|
||||
'materials.addTitle': 'New material',
|
||||
'materials.editTitle': 'Edit material',
|
||||
'materials.savingTitle': 'Saving material',
|
||||
'materials.savingWait': 'Please wait…',
|
||||
'materials.savingProgress': 'Material save progress',
|
||||
'materials.edit': 'Edit',
|
||||
'materials.search': 'Search materials…',
|
||||
'materials.searchEmpty': 'No matches.',
|
||||
|
||||
Reference in New Issue
Block a user