import React, { useState, useEffect, useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { searchLibraries, installLibrary, getInstalledLibraries, uninstallLibrary, } from '../../services/libraryService'; import type { ArduinoLibrary, InstalledLibrary } from '../../services/libraryService'; import { trackInstallLibrary } from '../../utils/analytics'; import { useLibraryManifestStore } from '../../store/useLibraryManifestStore'; import './LibraryManagerModal.css'; interface LibraryManagerModalProps { isOpen: boolean; onClose: () => void; } type Tab = 'project' | 'search' | 'installed'; /** Case/separator-insensitive name match, matching the backend's * _norm_lib_name (lowercased, alphanumerics only) so the UI's "in project" * state agrees with what the compiler scopes. */ const normLib = (s: string): string => (s || '').toLowerCase().replace(/[^a-z0-9]/g, ''); export const LibraryManagerModal: React.FC = ({ isOpen, onClose }) => { const { t } = useTranslation(); const [activeTab, setActiveTab] = useState('search'); // P2.4 — the project's declared library manifest (compile scope) lives here. const manifestLibs = useLibraryManifestStore((s) => s.libraries); const setLibraries = useLibraryManifestStore((s) => s.setLibraries); // Raw velxio.json editor draft + parse error (the Wokwi-style view). const [jsonDraft, setJsonDraft] = useState(''); const [jsonError, setJsonError] = useState(null); const [newLibName, setNewLibName] = useState(''); const [searchQuery, setSearchQuery] = useState(''); const [searchResults, setSearchResults] = useState([]); const [installedLibraries, setInstalledLibraries] = useState([]); const [loadingSearch, setLoadingSearch] = useState(false); const [loadingInstalled, setLoadingInstalled] = useState(false); const [installingLib, setInstallingLib] = useState(null); const [uninstallingLib, setUninstallingLib] = useState(null); /** Track user-selected version per library name */ const [selectedVersions, setSelectedVersions] = useState>({}); const [statusMsg, setStatusMsg] = useState<{ type: 'success' | 'error'; text: string } | null>( null, ); const debounceRef = useRef | null>(null); const fetchInstalled = useCallback(async () => { setLoadingInstalled(true); try { const libs = await getInstalledLibraries(); setInstalledLibraries(libs); } catch (e: unknown) { setStatusMsg({ type: 'error', text: e instanceof Error ? e.message : 'Failed to load installed libraries', }); } finally { setLoadingInstalled(false); } }, []); // Reset state when modal closes useEffect(() => { if (!isOpen) { setSearchQuery(''); setSearchResults([]); setStatusMsg(null); } }, [isOpen]); // Fetch installed list when modal opens or switching to installed tab useEffect(() => { if (isOpen && activeTab === 'installed') fetchInstalled(); }, [isOpen, activeTab, fetchInstalled]); useEffect(() => { if (isOpen) fetchInstalled(); }, [isOpen, fetchInstalled]); // Search: immediate on open (empty query), debounced when typing useEffect(() => { if (!isOpen) return; if (debounceRef.current) clearTimeout(debounceRef.current); const delay = searchQuery ? 400 : 0; debounceRef.current = setTimeout(async () => { setLoadingSearch(true); setStatusMsg(null); try { const results = await searchLibraries(searchQuery); setSearchResults(results); } catch (e: unknown) { setStatusMsg({ type: 'error', text: e instanceof Error ? e.message : 'Search failed' }); setSearchResults([]); } finally { setLoadingSearch(false); } }, delay); return () => { if (debounceRef.current) clearTimeout(debounceRef.current); }; }, [searchQuery, isOpen]); const handleInstall = async (libName: string) => { setInstallingLib(libName); setStatusMsg(null); try { const version = selectedVersions[libName]; const result = await installLibrary(libName, version); if (result.success) { trackInstallLibrary(libName); // P2.4 — installing a library declares it for THIS project (adds it to // velxio.json), so the compile is scoped to it and it never clashes // with another project's libs. The user can remove it in the Project tab. addToManifest(libName); if (result.fallback) { setStatusMsg({ type: 'success', text: `"${libName}" installed and added to this project (latest — requested @${result.requested_version} was not available)` }); } else { setStatusMsg({ type: 'success', text: `"${libName}${version ? ' @' + version : ''}" installed and added to this project!` }); } fetchInstalled(); } else { setStatusMsg({ type: 'error', text: result.error || `Failed to install "${libName}"` }); } } catch (e: unknown) { setStatusMsg({ type: 'error', text: e instanceof Error ? e.message : 'Installation failed' }); } finally { setInstallingLib(null); } }; const handleUninstall = async (libName: string) => { setUninstallingLib(libName); setStatusMsg(null); try { const result = await uninstallLibrary(libName); if (result.success) { setStatusMsg({ type: 'success', text: `"${libName}" uninstalled successfully!` }); fetchInstalled(); } else { setStatusMsg({ type: 'error', text: result.error || `Failed to uninstall "${libName}"` }); } } catch (e: unknown) { setStatusMsg({ type: 'error', text: e instanceof Error ? e.message : 'Uninstall failed' }); } finally { setUninstallingLib(null); } }; // ── Project manifest (velxio.json) editing ────────────────────────────── const declared = manifestLibs ?? []; const inManifest = useCallback( (name: string): boolean => declared.some((l) => normLib(l) === normLib(name)), [declared], ); const addToManifest = useCallback( (name: string) => { const clean = name.trim(); if (!clean) return; const cur = useLibraryManifestStore.getState().libraries ?? []; if (cur.some((l) => normLib(l) === normLib(clean))) return; setLibraries([...cur, clean]); }, [setLibraries], ); const removeFromManifest = useCallback( (name: string) => { const cur = useLibraryManifestStore.getState().libraries ?? []; const next = cur.filter((l) => normLib(l) !== normLib(name)); setLibraries(next.length ? next : null); }, [setLibraries], ); const applyJsonDraft = useCallback(() => { try { const parsed = JSON.parse(jsonDraft || '{}'); const libs = Array.isArray(parsed) ? parsed : parsed.libraries; if (!Array.isArray(libs) || !libs.every((x) => typeof x === 'string')) { setJsonError('Expected {"libraries": ["Name", ...]}'); return; } setJsonError(null); setLibraries(libs.length ? (libs as string[]) : null); } catch (e) { setJsonError(e instanceof Error ? e.message : 'Invalid JSON'); } }, [jsonDraft, setLibraries]); // Open the Project (velxio.json) tab when launched from the explorer entry. useEffect(() => { const toProject = () => setActiveTab('project'); window.addEventListener('velxio-open-library-manager', toProject); return () => window.removeEventListener('velxio-open-library-manager', toProject); }, []); // Keep the raw velxio.json draft in sync with the manifest while not editing. useEffect(() => { setJsonDraft(JSON.stringify({ libraries: manifestLibs ?? [] }, null, 2)); setJsonError(null); }, [manifestLibs, isOpen]); const handleClose = () => { onClose(); }; if (!isOpen) return null; const isInstalled = (libName: string): boolean => installedLibraries.some( (il) => (il.library?.name || il.name || '').toLowerCase() === libName.toLowerCase(), ); const getLibName = (lib: ArduinoLibrary): string => lib.name || 'Unknown'; const getLibVersion = (lib: ArduinoLibrary): string => lib.latest?.version || lib.version || ''; const getLibAuthor = (lib: ArduinoLibrary): string => lib.latest?.author || lib.author || ''; const getLibDesc = (lib: ArduinoLibrary): string => lib.latest?.sentence || lib.sentence || ''; const getInstalledName = (lib: InstalledLibrary): string => lib.library?.name || lib.name || 'Unknown'; const getInstalledVersion = (lib: InstalledLibrary): string => lib.library?.version || lib.version || ''; const getInstalledAuthor = (lib: InstalledLibrary): string => lib.library?.author || lib.author || ''; const getInstalledDesc = (lib: InstalledLibrary): string => lib.library?.sentence || lib.sentence || ''; return (
e.stopPropagation()}> {/* Header */}
{t('editor.libraryManager.title')}
{/* Tabs */}
{/* Status bar */} {statusMsg && (
{statusMsg.type === 'success' ? ( ) : ( )} {statusMsg.text}
)} {/* Project Tab — velxio.json: the libraries THIS project declares. These (plus the arduino-esp32 core) are the ESP32 compile scope, so the project never picks up another project's or user's libraries. */} {activeTab === 'project' && (
Libraries this project uses (its velxio.json). They become the compile scope so libraries never clash between projects. Installing a library from the Search tab adds it here automatically; you can also add or remove them below.
{/* Declared libraries as removable rows */}
{declared.length === 0 && (

No libraries declared.

Core libraries (WiFi, Wire, SPI, WebServer…) are always available. Add external libraries from the Search tab or below.

)} {declared.map((name, i) => (
{name}
))}
{/* Quick add by name */}
setNewLibName(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter' && newLibName.trim()) { addToManifest(newLibName); setNewLibName(''); } }} />
{/* Raw velxio.json editor (Wokwi-style) for power users */}
Edit velxio.json directly