feat(materials): free rotate session overlays and show save progress

Match NPC drag-to-rotate on the material frame during playback, and change the add/edit Save button to Saving… while import runs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-07-21 14:56:42 +08:00
parent 0cab7ce7ca
commit c1c332364c
8 changed files with 238 additions and 67 deletions
+26 -8
View File
@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import { createPortal, flushSync } from 'react-dom';
import type { MaterialId, ProjectMaterial } from '../../shared/types';
import { Button, Input } from '../shared/ui/controls';
@@ -63,11 +63,11 @@ export function MaterialEditModal({
useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
if (e.key === 'Escape' && !saving) onClose();
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [onClose, open]);
}, [onClose, open, saving]);
const setPreviewFromPathAndUrl = (path: string, previewUrl: string) => {
setFilePath(path);
@@ -102,13 +102,28 @@ export function MaterialEditModal({
return createPortal(
<>
<button type="button" aria-label={t('common.close')} onClick={onClose} className={styles.modalBackdrop} />
<button
type="button"
aria-label={t('common.close')}
onClick={() => {
if (!saving) onClose();
}}
className={styles.modalBackdrop}
/>
<div role="dialog" aria-modal="true" className={styles.modalDialog}>
<div className={styles.modalHeader}>
<div className={styles.modalTitle}>
{initial ? t('materials.editTitle') : t('materials.addTitle')}
</div>
<button type="button" aria-label={t('common.close')} onClick={onClose} className={styles.modalClose}>
<button
type="button"
aria-label={t('common.close')}
onClick={() => {
if (!saving) onClose();
}}
className={styles.modalClose}
disabled={saving}
>
×
</button>
</div>
@@ -151,6 +166,7 @@ export function MaterialEditModal({
<div className={styles.muted}>{t('materials.imageEmpty')}</div>
)}
<Button
disabled={saving}
onClick={() => {
void (async () => {
const picked = await onPickImage();
@@ -177,8 +193,10 @@ export function MaterialEditModal({
onClick={() => {
if (!canSave) return;
void (async () => {
setSaving(true);
setError(null);
flushSync(() => {
setSaving(true);
setError(null);
});
try {
await onSave(filePath ? { name: trimmed, filePath } : { name: trimmed });
onClose();
@@ -190,7 +208,7 @@ export function MaterialEditModal({
})();
}}
>
{t('common.save')}
{saving ? t('common.saving') : t('common.save')}
</Button>
</div>
</div>