feat: add Install Libraries modal for managing library installations
parent
ccab31d301
commit
02774b383f
|
|
@ -3,6 +3,7 @@ import { useEditorStore } from '../../store/useEditorStore';
|
||||||
import { useSimulatorStore, BOARD_FQBN, BOARD_LABELS } from '../../store/useSimulatorStore';
|
import { useSimulatorStore, BOARD_FQBN, BOARD_LABELS } from '../../store/useSimulatorStore';
|
||||||
import { compileCode } from '../../services/compilation';
|
import { compileCode } from '../../services/compilation';
|
||||||
import { LibraryManagerModal } from '../simulator/LibraryManagerModal';
|
import { LibraryManagerModal } from '../simulator/LibraryManagerModal';
|
||||||
|
import { InstallLibrariesModal } from '../simulator/InstallLibrariesModal';
|
||||||
import { parseCompileResult } from '../../utils/compilationLogger';
|
import { parseCompileResult } from '../../utils/compilationLogger';
|
||||||
import type { CompilationLog } from '../../utils/compilationLogger';
|
import type { CompilationLog } from '../../utils/compilationLogger';
|
||||||
import { exportToWokwiZip, importFromWokwiZip } from '../../utils/wokwiZip';
|
import { exportToWokwiZip, importFromWokwiZip } from '../../utils/wokwiZip';
|
||||||
|
|
@ -30,6 +31,8 @@ export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compi
|
||||||
const [compiling, setCompiling] = useState(false);
|
const [compiling, setCompiling] = useState(false);
|
||||||
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
||||||
const [libManagerOpen, setLibManagerOpen] = useState(false);
|
const [libManagerOpen, setLibManagerOpen] = useState(false);
|
||||||
|
const [pendingLibraries, setPendingLibraries] = useState<string[]>([]);
|
||||||
|
const [installModalOpen, setInstallModalOpen] = useState(false);
|
||||||
const importInputRef = useRef<HTMLInputElement>(null);
|
const importInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const addLog = useCallback((log: CompilationLog) => {
|
const addLog = useCallback((log: CompilationLog) => {
|
||||||
|
|
@ -121,6 +124,10 @@ export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compi
|
||||||
setWires(result.wires);
|
setWires(result.wires);
|
||||||
if (result.files.length > 0) loadFiles(result.files);
|
if (result.files.length > 0) loadFiles(result.files);
|
||||||
setMessage({ type: 'success', text: `Imported ${file.name}` });
|
setMessage({ type: 'success', text: `Imported ${file.name}` });
|
||||||
|
if (result.libraries.length > 0) {
|
||||||
|
setPendingLibraries(result.libraries);
|
||||||
|
setInstallModalOpen(true);
|
||||||
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setMessage({ type: 'error', text: err?.message || 'Import failed.' });
|
setMessage({ type: 'error', text: err?.message || 'Import failed.' });
|
||||||
}
|
}
|
||||||
|
|
@ -275,6 +282,11 @@ export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compi
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<LibraryManagerModal isOpen={libManagerOpen} onClose={() => setLibManagerOpen(false)} />
|
<LibraryManagerModal isOpen={libManagerOpen} onClose={() => setLibManagerOpen(false)} />
|
||||||
|
<InstallLibrariesModal
|
||||||
|
isOpen={installModalOpen}
|
||||||
|
onClose={() => setInstallModalOpen(false)}
|
||||||
|
libraries={pendingLibraries}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,246 @@
|
||||||
|
.ilib-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.75);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1100;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
animation: ilib-fadeIn 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ilib-fadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.ilib-modal {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 8px;
|
||||||
|
width: 480px;
|
||||||
|
max-width: 95vw;
|
||||||
|
max-height: 80vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.6);
|
||||||
|
animation: ilib-slideIn 0.2s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ilib-slideIn {
|
||||||
|
from { transform: translateY(-16px); opacity: 0; }
|
||||||
|
to { transform: translateY(0); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.ilib-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 14px 18px;
|
||||||
|
background: #111;
|
||||||
|
border-bottom: 1px solid #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ilib-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
color: #ccc;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ilib-close-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #888;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.15s;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ilib-close-btn:hover:not(:disabled) {
|
||||||
|
background: #2a2a2a;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ilib-close-btn:disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Subtitle */
|
||||||
|
.ilib-subtitle {
|
||||||
|
padding: 10px 18px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #999;
|
||||||
|
border-bottom: 1px solid #222;
|
||||||
|
background: #161616;
|
||||||
|
min-height: 38px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ilib-subtitle-installing {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: #00b8d4;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ilib-subtitle-done {
|
||||||
|
color: #4ade80;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Library list */
|
||||||
|
.ilib-list {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ilib-list::-webkit-scrollbar { width: 6px; }
|
||||||
|
.ilib-list::-webkit-scrollbar-track { background: #111; }
|
||||||
|
.ilib-list::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
|
||||||
|
|
||||||
|
.ilib-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 18px;
|
||||||
|
border-bottom: 1px solid #1f1f1f;
|
||||||
|
gap: 12px;
|
||||||
|
transition: background 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ilib-item:hover { background: #1f1f1f; }
|
||||||
|
|
||||||
|
.ilib-item-name {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #ddd;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ilib-item--done .ilib-item-name { color: #888; }
|
||||||
|
.ilib-item--error .ilib-item-name { color: #888; }
|
||||||
|
|
||||||
|
.ilib-item-status {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status badges */
|
||||||
|
.ilib-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ilib-badge--pending {
|
||||||
|
color: #666;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ilib-badge--installing {
|
||||||
|
color: #00b8d4;
|
||||||
|
background: #0a1f24;
|
||||||
|
border: 1px solid #005f7a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ilib-badge--done {
|
||||||
|
color: #4ade80;
|
||||||
|
background: #0d2b1e;
|
||||||
|
border: 1px solid #1a4731;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ilib-badge--error {
|
||||||
|
color: #f87171;
|
||||||
|
background: #2b0d0d;
|
||||||
|
border: 1px solid #4a1a1a;
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Spinner */
|
||||||
|
.ilib-spinner {
|
||||||
|
flex-shrink: 0;
|
||||||
|
animation: ilib-spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ilib-spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
.ilib-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 14px 18px;
|
||||||
|
background: #111;
|
||||||
|
border-top: 1px solid #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ilib-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 8px 22px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ilib-btn:disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ilib-btn--primary {
|
||||||
|
background: #00b8d4;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ilib-btn--primary:hover:not(:disabled) {
|
||||||
|
background: #00d4f0;
|
||||||
|
box-shadow: 0 0 12px rgba(0, 184, 212, 0.4);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ilib-btn--ghost {
|
||||||
|
background: transparent;
|
||||||
|
color: #888;
|
||||||
|
border: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ilib-btn--ghost:hover:not(:disabled) {
|
||||||
|
background: #2a2a2a;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,197 @@
|
||||||
|
import React, { useState, useCallback } from 'react';
|
||||||
|
import { installLibrary } from '../../services/libraryService';
|
||||||
|
import './InstallLibrariesModal.css';
|
||||||
|
|
||||||
|
interface InstallLibrariesModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
libraries: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type ItemStatus = 'pending' | 'installing' | 'done' | 'error';
|
||||||
|
|
||||||
|
interface LibItem {
|
||||||
|
name: string;
|
||||||
|
status: ItemStatus;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Spinner: React.FC<{ size?: number }> = ({ size = 16 }) => (
|
||||||
|
<svg
|
||||||
|
className="ilib-spinner"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
>
|
||||||
|
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const InstallLibrariesModal: React.FC<InstallLibrariesModalProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
libraries,
|
||||||
|
}) => {
|
||||||
|
const [items, setItems] = useState<LibItem[]>(() =>
|
||||||
|
libraries.map((name) => ({ name, status: 'pending' })),
|
||||||
|
);
|
||||||
|
const [running, setRunning] = useState(false);
|
||||||
|
const [doneCount, setDoneCount] = useState(0);
|
||||||
|
|
||||||
|
// Sync items when the libraries prop changes (new import)
|
||||||
|
React.useEffect(() => {
|
||||||
|
setItems(libraries.map((name) => ({ name, status: 'pending' })));
|
||||||
|
setDoneCount(0);
|
||||||
|
setRunning(false);
|
||||||
|
}, [libraries]);
|
||||||
|
|
||||||
|
const setItemStatus = useCallback(
|
||||||
|
(name: string, status: ItemStatus, error?: string) => {
|
||||||
|
setItems((prev) =>
|
||||||
|
prev.map((it) => (it.name === name ? { ...it, status, error } : it)),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleInstallAll = useCallback(async () => {
|
||||||
|
setRunning(true);
|
||||||
|
let completed = 0;
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.status === 'done') { completed++; continue; }
|
||||||
|
setItemStatus(item.name, 'installing');
|
||||||
|
try {
|
||||||
|
const result = await installLibrary(item.name);
|
||||||
|
if (result.success) {
|
||||||
|
setItemStatus(item.name, 'done');
|
||||||
|
} else {
|
||||||
|
setItemStatus(item.name, 'error', result.error || 'Install failed');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setItemStatus(item.name, 'error', e instanceof Error ? e.message : 'Install failed');
|
||||||
|
}
|
||||||
|
completed++;
|
||||||
|
setDoneCount(completed);
|
||||||
|
}
|
||||||
|
setRunning(false);
|
||||||
|
}, [items, setItemStatus]);
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const pendingCount = items.filter((i) => i.status === 'pending').length;
|
||||||
|
const installedCount = items.filter((i) => i.status === 'done').length;
|
||||||
|
const allDone = items.length > 0 && pendingCount === 0 && !running;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="ilib-overlay" onClick={running ? undefined : onClose}>
|
||||||
|
<div className="ilib-modal" onClick={(e) => e.stopPropagation()}>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="ilib-header">
|
||||||
|
<div className="ilib-title">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#00b8d4" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z" />
|
||||||
|
<path d="m3.3 7 8.7 5 8.7-5" />
|
||||||
|
<path d="M12 22V12" />
|
||||||
|
</svg>
|
||||||
|
<span>REQUIRED LIBRARIES</span>
|
||||||
|
</div>
|
||||||
|
<button className="ilib-close-btn" onClick={onClose} disabled={running}>
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18" />
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Subtitle */}
|
||||||
|
<div className="ilib-subtitle">
|
||||||
|
{running ? (
|
||||||
|
<span className="ilib-subtitle-installing">
|
||||||
|
<Spinner size={13} />
|
||||||
|
Installing {doneCount + 1} of {items.length}…
|
||||||
|
</span>
|
||||||
|
) : allDone ? (
|
||||||
|
<span className="ilib-subtitle-done">All libraries installed successfully</span>
|
||||||
|
) : (
|
||||||
|
<span>
|
||||||
|
This project requires {items.length} {items.length === 1 ? 'library' : 'libraries'}.
|
||||||
|
Install them to compile correctly.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Library list */}
|
||||||
|
<div className="ilib-list">
|
||||||
|
{items.map((item) => (
|
||||||
|
<div key={item.name} className={`ilib-item ilib-item--${item.status}`}>
|
||||||
|
<span className="ilib-item-name">{item.name}</span>
|
||||||
|
<span className="ilib-item-status">
|
||||||
|
{item.status === 'pending' && <span className="ilib-badge ilib-badge--pending">pending</span>}
|
||||||
|
{item.status === 'installing' && (
|
||||||
|
<span className="ilib-badge ilib-badge--installing">
|
||||||
|
<Spinner size={12} />
|
||||||
|
installing…
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{item.status === 'done' && (
|
||||||
|
<span className="ilib-badge ilib-badge--done">
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<polyline points="20 6 9 17 4 12" />
|
||||||
|
</svg>
|
||||||
|
installed
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{item.status === 'error' && (
|
||||||
|
<span className="ilib-badge ilib-badge--error" title={item.error}>
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round">
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18" />
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18" />
|
||||||
|
</svg>
|
||||||
|
error
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="ilib-footer">
|
||||||
|
{allDone ? (
|
||||||
|
<button className="ilib-btn ilib-btn--primary" onClick={onClose}>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className="ilib-btn ilib-btn--ghost"
|
||||||
|
onClick={onClose}
|
||||||
|
disabled={running}
|
||||||
|
>
|
||||||
|
Skip
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="ilib-btn ilib-btn--primary"
|
||||||
|
onClick={handleInstallAll}
|
||||||
|
disabled={running || installedCount === items.length}
|
||||||
|
>
|
||||||
|
{running ? (
|
||||||
|
<>
|
||||||
|
<Spinner size={14} />
|
||||||
|
Installing…
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
`Install All (${pendingCount})`
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -48,6 +48,8 @@ export interface ImportResult {
|
||||||
components: VelxioComponent[];
|
components: VelxioComponent[];
|
||||||
wires: Wire[];
|
wires: Wire[];
|
||||||
files: Array<{ name: string; content: string }>;
|
files: Array<{ name: string; content: string }>;
|
||||||
|
/** Standard Arduino library names parsed from libraries.txt (Wokwi-only @wokwi: entries are excluded). */
|
||||||
|
libraries: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Board mappings ────────────────────────────────────────────────────────────
|
// ── Board mappings ────────────────────────────────────────────────────────────
|
||||||
|
|
@ -303,5 +305,17 @@ export async function importFromWokwiZip(file: File): Promise<ImportResult> {
|
||||||
return a.name.localeCompare(b.name);
|
return a.name.localeCompare(b.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
return { boardType, boardPosition, components, wires, files };
|
// Parse libraries.txt — skip blank lines, comments (#), and Wokwi-only entries (name@wokwi:hash)
|
||||||
|
const libraries: string[] = [];
|
||||||
|
const libEntry = zip.file('libraries.txt');
|
||||||
|
if (libEntry) {
|
||||||
|
const libText = await libEntry.async('string');
|
||||||
|
for (const raw of libText.split('\n')) {
|
||||||
|
const line = raw.trim();
|
||||||
|
if (!line || line.startsWith('#') || line.includes('@wokwi:')) continue;
|
||||||
|
libraries.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { boardType, boardPosition, components, wires, files, libraries };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue