import React, { useCallback, useEffect, useState } from 'react'; import { createPortal, flushSync } from 'react-dom'; import type { MaterialId, MaterialLegend, ProjectMaterial } from '../../shared/types'; import { Button, Input } from '../shared/ui/controls'; import { useAssetUrl } from '../shared/useAssetImageUrl'; import styles from './EditorApp.module.css'; import { filterMaterialImagePaths, getDroppedFileEntries, pickFirstMaterialImagePath, useFileDropZone, } from './fileDrop'; import { useEditorI18n } from './i18n/EditorI18nContext'; import { MaterialsBrowser } from './MaterialsBrowser'; import matStyles from './MaterialsModals.module.css'; function normalizeName(input: string): string { return input.trim().toLowerCase(); } type MaterialEditModalProps = { open: boolean; initial: ProjectMaterial | null; existingNames: string[]; onClose: () => void; onPickImage: () => Promise<{ filePath: string; previewDataUrl: string } | null>; onSave: (input: { name: string; filePath?: string }) => Promise; }; export function MaterialEditModal({ open, initial, existingNames, onClose, onPickImage, onSave, }: MaterialEditModalProps) { const { t } = useEditorI18n(); const [name, setName] = useState(''); const [filePath, setFilePath] = useState(null); const [localPreviewUrl, setLocalPreviewUrl] = useState(null); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const existingUrl = useAssetUrl(initial?.assetId ?? null); useEffect(() => { if (!open) return; setName(initial?.name ?? ''); setFilePath(null); setLocalPreviewUrl(null); setSaving(false); setError(null); }, [initial, open]); useEffect(() => { return () => { if (localPreviewUrl?.startsWith('blob:')) URL.revokeObjectURL(localPreviewUrl); }; }, [localPreviewUrl]); useEffect(() => { if (!open) return; const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape' && !saving) onClose(); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [onClose, open, saving]); const setPreviewFromPathAndUrl = (path: string, previewUrl: string) => { setFilePath(path); setLocalPreviewUrl((prev) => { if (prev?.startsWith('blob:')) URL.revokeObjectURL(prev); return previewUrl; }); }; const drop = useFileDropZone({ onDropPaths: (paths) => { const picked = pickFirstMaterialImagePath(paths); if (!picked) return; setPreviewFromPathAndUrl(picked, ''); }, filterPaths: filterMaterialImagePaths, }); const trimmed = name.trim(); const nameOk = trimmed.length >= 1; const nameDup = nameOk && existingNames.some( (n) => normalizeName(n) === normalizeName(trimmed) && normalizeName(n) !== normalizeName(initial?.name ?? ''), ); const hasImage = Boolean(filePath) || Boolean(initial?.assetId); const canSave = nameOk && !nameDup && hasImage && !saving; const previewSrc = localPreviewUrl || existingUrl; if (!open) return null; return createPortal( <>
{t('materials.name')}
{!nameOk ?
{t('materials.nameRequired')}
: null} {nameDup ?
{t('materials.nameDup')}
: null}
{t('materials.image')}
{ drop.onDrop(e); const entries = getDroppedFileEntries(e); const files = e.dataTransfer?.files; for (let i = 0; i < entries.length; i += 1) { const entry = entries[i]!; if (!pickFirstMaterialImagePath([entry.path])) continue; const file = files?.[i]; if (file) { setPreviewFromPathAndUrl(entry.path, URL.createObjectURL(file)); return; } setPreviewFromPathAndUrl(entry.path, ''); return; } }} > {drop.dragOver ?
{t('materials.dropHint')}
: null} {previewSrc ? ( ) : (
{t('materials.imageEmpty')}
)}
{!hasImage ?
{t('materials.imageRequired')}
: null}
{error ?
{error}
: null}
, document.body, ); } type MaterialsManagerModalProps = { open: boolean; materials: ProjectMaterial[]; onClose: () => void; onAdd: () => void; onEdit: (material: ProjectMaterial) => void; onDelete: (materialId: MaterialId) => Promise; onReorder: (materialIds: MaterialId[]) => Promise; onRotate: (materialId: MaterialId, rotationDeg: 0 | 90 | 180 | 270) => void; onLegendChange?: (materialId: MaterialId, legend: MaterialLegend) => Promise; }; export function MaterialsManagerModal({ open, materials, onClose, onAdd, onEdit, onDelete, onReorder, onRotate, onLegendChange, }: MaterialsManagerModalProps) { const { t } = useEditorI18n(); const [selectedId, setSelectedId] = useState(null); const onSelect = useCallback((id: MaterialId | null) => setSelectedId(id), []); useEffect(() => { if (!open) return; setSelectedId(materials[0]?.id ?? null); }, [open]); useEffect(() => { if (!open) return; const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [onClose, open]); if (!open) return null; return createPortal( <> , document.body, ); }