diff --git a/frontend/src/components/editor/EditorToolbar.tsx b/frontend/src/components/editor/EditorToolbar.tsx index 5ae83ef5..7da4f6e9 100644 --- a/frontend/src/components/editor/EditorToolbar.tsx +++ b/frontend/src/components/editor/EditorToolbar.tsx @@ -23,7 +23,8 @@ import { LibraryManagerModal } from '../simulator/LibraryManagerModal'; import { InstallLibrariesModal } from '../simulator/InstallLibrariesModal'; import { parseCompileResult } from '../../utils/compilationLogger'; import type { CompilationLog } from '../../utils/compilationLogger'; -import { exportToWokwiZip, importFromWokwiZip } from '../../utils/wokwiZip'; +import { exportToWokwiZip } from '../../utils/wokwiZip'; +import { importProjectFile, PROJECT_FILE_ACCEPT } from '../../utils/importProject'; import { readFirmwareFile } from '../../utils/firmwareLoader'; import { trackCompileCode, @@ -903,7 +904,14 @@ export const EditorToolbar = ({ importInputRef.current.value = ''; if (!file) return; try { - const result = await importFromWokwiZip(file); + const result = await importProjectFile(file); + if (result.kind === 'vlx') { + // importVlxFile already wrote into the stores. + setMessage({ type: 'success', text: `Imported ${file.name}` }); + return; + } + // .zip path: apply the parsed payload to the stores ourselves, then + // surface any missing libraries via the existing install modal. const { loadFiles } = useEditorStore.getState(); const { setComponents, setWires, setBoardType, setBoardPosition, stopSimulation } = useSimulatorStore.getState(); @@ -1110,11 +1118,13 @@ export const EditorToolbar = ({ {centerSlot &&
{centerSlot}
}
- {/* Hidden file input for import (always present) */} + {/* Hidden file input for project import. Accepts both .vlx + (Velxio native) and .zip (Wokwi bundle); the dispatcher in + utils/importProject.ts picks the right loader by extension. */} diff --git a/frontend/src/components/editor/FileExplorer.tsx b/frontend/src/components/editor/FileExplorer.tsx index 57e33901..641ff425 100644 --- a/frontend/src/components/editor/FileExplorer.tsx +++ b/frontend/src/components/editor/FileExplorer.tsx @@ -4,7 +4,7 @@ import { useEditorStore } from '../../store/useEditorStore'; import { useSimulatorStore } from '../../store/useSimulatorStore'; import type { BoardKind } from '../../types/board'; import { BOARD_KIND_LABELS } from '../../types/board'; -import { importVlxFile, VlxParseError } from '../../utils/vlxFile'; +import { importProjectFile, PROJECT_FILE_ACCEPT } from '../../utils/importProject'; import './FileExplorer.css'; // SVG icons — same style as EditorToolbar (stroke-based, 16x16) @@ -173,29 +173,54 @@ interface FileExplorerProps { export const FileExplorer: React.FC = ({ onSaveClick, onNewClick }) => { // Hidden we trigger via ref when the user clicks - // the Open .vlx button. Kept outside React state so the change event - // can fire repeatedly even if the user picks the same file twice. + // the Open project button. Accepts both .vlx (Velxio native) and .zip + // (Wokwi bundle); the dispatcher in utils/importProject.ts decides which + // loader to run based on the file extension. Kept outside React state so + // the change event still fires when the user picks the same file twice. const fileInputRef = useRef(null); - const handleOpenVlxClick = useCallback(() => { + const handleOpenProjectClick = useCallback(() => { fileInputRef.current?.click(); }, []); - const handleVlxFilePicked = useCallback(async (e: React.ChangeEvent) => { + const handleProjectFilePicked = useCallback(async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; // Reset so picking the SAME file again later still fires onchange. e.target.value = ''; if (!file) return; + const friendlyName = file.name.toLowerCase().endsWith('.zip') ? 'Wokwi .zip' : '.vlx'; if ( !window.confirm( - 'Load this .vlx file? Your current workspace will be replaced. This cannot be undone.', + `Load this ${friendlyName} project? Your current workspace will be replaced. ` + + `This cannot be undone.`, ) ) { return; } try { - await importVlxFile(file); + const result = await importProjectFile(file); + // .zip needs the caller to apply the payload to the stores (we keep + // that asymmetry so the toolbar's import flow can also pop the + // install-libraries modal afterwards). Here in the file explorer we + // don't have that modal, so we apply the payload silently and just + // warn in the console if the project references uninstalled libs. + if (result.kind === 'zip') { + const { loadFiles } = useEditorStore.getState(); + const { setComponents, setWires, setBoardType, setBoardPosition, stopSimulation } = + useSimulatorStore.getState(); + stopSimulation(); + if (result.boardType) setBoardType(result.boardType); + setBoardPosition(result.boardPosition); + setComponents(result.components); + setWires(result.wires); + if (result.files.length > 0) loadFiles(result.files); + if (result.libraries.length > 0) { + console.warn( + '[FileExplorer] Imported Wokwi zip references libraries you may need to install:', + result.libraries, + ); + } + } } catch (err) { - const msg = err instanceof VlxParseError ? err.message : (err as Error).message; - window.alert(`Could not load .vlx file:\n\n${msg}`); + window.alert((err as Error).message); } }, []); @@ -330,16 +355,16 @@ export const FileExplorer: React.FC = ({ onSaveClick, onNewCl