2026-06-08 20:51:34 +07:00
|
|
|
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
2026-08-15 13:19:30 +07:00
|
|
|
import { createPortal } from 'react-dom';
|
2026-05-09 21:29:07 +07:00
|
|
|
import { useTranslation } from 'react-i18next';
|
2026-04-22 02:45:45 +07:00
|
|
|
import {
|
|
|
|
|
searchLibraries,
|
|
|
|
|
installLibrary,
|
|
|
|
|
getInstalledLibraries,
|
2026-06-08 00:58:13 +07:00
|
|
|
getCustomLibraries,
|
|
|
|
|
deleteCustomLibrary,
|
2026-04-22 02:45:45 +07:00
|
|
|
} from '../../services/libraryService';
|
2026-03-05 08:05:23 +07:00
|
|
|
import type { ArduinoLibrary, InstalledLibrary } from '../../services/libraryService';
|
2026-03-25 13:06:38 +07:00
|
|
|
import { trackInstallLibrary } from '../../utils/analytics';
|
feat(micropython): install libraries from the micropython-lib index (#214)
A MicroPython library in Velxio IS a .py file in the project, copied onto
the board's filesystem before main.py runs. That mechanism worked, but
getting the file meant hunting GitHub by hand — the gap issue #214 is
about. Now the Library Manager fills it, in both halves:
Backend: /api/micropython-libs proxies the official micropython-lib
package index (the same one `mip` uses on real hardware). /search filters
the index; /fetch resolves a package plus its declared deps into .py
files. The proxy is what makes this safe to expose: only the official
index is reachable (the name must exist in index.json, files come from its
content-addressed store), source only (.py, never .mpy bytecode), path-
safe, and capped at 24 files / 256 KB per install. In-process TTL cache
over the index.
Frontend: the same Library Manager modal, mode-aware. On a MicroPython
board the search hits the index and "Add to project" writes the package's
files into the board's workspace (upsert — re-adding is the upgrade path),
visible in the explorer and travelling with the project. Before typing it
shows featured picks (ssd1306, umqtt.*, urequests) and the modules already
frozen into the firmware (neopixel, dht, onewire, ds18x20, machine,
network) labeled "built into the firmware", so searching for one answers
"just import it" instead of an empty list.
The file-materialization prelude now creates package directories: umqtt
ships as umqtt/simple.py, and MicroPython's open() does not mkdir, so the
prelude died with ENOENT on any packaged module.
Also ships a gallery example ("ESP32 + MicroPython: use an external
library") that documents the file-is-the-library mechanism with the
ssd1306 driver on the wired OLED.
2026-08-19 02:50:02 +07:00
|
|
|
import {
|
|
|
|
|
BUILTIN_MPY_MODULES,
|
|
|
|
|
FEATURED_MPY_PACKAGES,
|
|
|
|
|
fetchMpyPackage,
|
|
|
|
|
packageFilesInGroup,
|
|
|
|
|
searchMpyPackages,
|
|
|
|
|
writeFilesIntoGroup,
|
|
|
|
|
} from '../../services/micropythonLibs';
|
|
|
|
|
import type { MpyPackage } from '../../services/micropythonLibs';
|
2026-06-07 11:07:48 +07:00
|
|
|
import { useSimulatorStore } from '../../store/useSimulatorStore';
|
|
|
|
|
import { boardDisplayName } from '../../types/board';
|
2026-03-05 08:05:23 +07:00
|
|
|
import './LibraryManagerModal.css';
|
|
|
|
|
|
|
|
|
|
interface LibraryManagerModalProps {
|
2026-04-22 02:45:45 +07:00
|
|
|
isOpen: boolean;
|
|
|
|
|
onClose: () => void;
|
2026-03-05 08:05:23 +07:00
|
|
|
}
|
|
|
|
|
|
2026-06-08 20:51:34 +07:00
|
|
|
/** Case/separator-insensitive name match, matching the backend's _norm_lib_name
|
|
|
|
|
* 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, '');
|
|
|
|
|
|
|
|
|
|
/** One row of the single unified list. A search result and an installed/custom
|
|
|
|
|
* library both normalise to this so they render identically. */
|
|
|
|
|
interface LibRow {
|
|
|
|
|
name: string;
|
|
|
|
|
version: string;
|
|
|
|
|
author: string;
|
|
|
|
|
desc: string;
|
|
|
|
|
installed: boolean;
|
|
|
|
|
custom: boolean;
|
|
|
|
|
releases?: Record<string, unknown>;
|
|
|
|
|
}
|
2026-03-05 08:05:23 +07:00
|
|
|
|
2026-06-08 20:51:34 +07:00
|
|
|
/**
|
|
|
|
|
* Library Manager — ONE list, no tabs. Each row is state-aware:
|
|
|
|
|
* + Add to project (installs if needed, then declares it on this board)
|
|
|
|
|
* In project ✓ (click to remove from this board's libraries.json)
|
|
|
|
|
* Uninstall / Remove (free the cache / remove your custom upload)
|
|
|
|
|
*
|
|
|
|
|
* The per-board manifest (board.libraries) IS the compile scope and is what the
|
|
|
|
|
* read-only `libraries.json` file in the explorer shows. This modal is the only
|
|
|
|
|
* place that edits it.
|
|
|
|
|
*/
|
2026-03-05 08:05:23 +07:00
|
|
|
export const LibraryManagerModal: React.FC<LibraryManagerModalProps> = ({ isOpen, onClose }) => {
|
2026-05-09 21:29:07 +07:00
|
|
|
const { t } = useTranslation();
|
2026-06-08 20:51:34 +07:00
|
|
|
|
|
|
|
|
// Per-board manifest: the libraries.json edited here belongs to the ACTIVE
|
|
|
|
|
// board, so two boards in one project can scope to different libraries.
|
2026-06-07 11:07:48 +07:00
|
|
|
const boards = useSimulatorStore((s) => s.boards);
|
|
|
|
|
const activeBoardId = useSimulatorStore((s) => s.activeBoardId);
|
|
|
|
|
const updateBoard = useSimulatorStore((s) => s.updateBoard);
|
|
|
|
|
const activeBoard = boards.find((b) => b.id === activeBoardId) ?? boards[0];
|
|
|
|
|
const manifestLibs = activeBoard?.libraries ?? null;
|
2026-06-08 20:51:34 +07:00
|
|
|
const declared = manifestLibs ?? [];
|
|
|
|
|
|
feat(micropython): install libraries from the micropython-lib index (#214)
A MicroPython library in Velxio IS a .py file in the project, copied onto
the board's filesystem before main.py runs. That mechanism worked, but
getting the file meant hunting GitHub by hand — the gap issue #214 is
about. Now the Library Manager fills it, in both halves:
Backend: /api/micropython-libs proxies the official micropython-lib
package index (the same one `mip` uses on real hardware). /search filters
the index; /fetch resolves a package plus its declared deps into .py
files. The proxy is what makes this safe to expose: only the official
index is reachable (the name must exist in index.json, files come from its
content-addressed store), source only (.py, never .mpy bytecode), path-
safe, and capped at 24 files / 256 KB per install. In-process TTL cache
over the index.
Frontend: the same Library Manager modal, mode-aware. On a MicroPython
board the search hits the index and "Add to project" writes the package's
files into the board's workspace (upsert — re-adding is the upgrade path),
visible in the explorer and travelling with the project. Before typing it
shows featured picks (ssd1306, umqtt.*, urequests) and the modules already
frozen into the firmware (neopixel, dht, onewire, ds18x20, machine,
network) labeled "built into the firmware", so searching for one answers
"just import it" instead of an empty list.
The file-materialization prelude now creates package directories: umqtt
ships as umqtt/simple.py, and MicroPython's open() does not mkdir, so the
prelude died with ENOENT on any packaged module.
Also ships a gallery example ("ESP32 + MicroPython: use an external
library") that documents the file-is-the-library mechanism with the
ssd1306 driver on the wired OLED.
2026-08-19 02:50:02 +07:00
|
|
|
// ── MicroPython mode (issue #214) ─────────────────────────────────────────
|
|
|
|
|
// Same modal, different backing: a MicroPython library is a .py file in
|
|
|
|
|
// the project, so search hits the micropython-lib index (backend proxy)
|
|
|
|
|
// and "Add to project" writes the package's files into this board's file
|
|
|
|
|
// group — no libraries.json, no arduino-cli.
|
|
|
|
|
const isMpy = activeBoard?.languageMode === 'micropython';
|
|
|
|
|
const [mpyResults, setMpyResults] = useState<MpyPackage[]>([]);
|
|
|
|
|
// Bumped after add/remove so the "In project" pills re-derive from the
|
|
|
|
|
// file group (which lives outside React state).
|
|
|
|
|
const [mpyTick, setMpyTick] = useState(0);
|
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
|
|
|
const [searchResults, setSearchResults] = useState<ArduinoLibrary[]>([]);
|
|
|
|
|
const [installedLibraries, setInstalledLibraries] = useState<InstalledLibrary[]>([]);
|
|
|
|
|
const [loadingSearch, setLoadingSearch] = useState(false);
|
|
|
|
|
const [loadingInstalled, setLoadingInstalled] = useState(false);
|
2026-06-08 20:51:34 +07:00
|
|
|
const [busyLib, setBusyLib] = useState<string | null>(null); // install/uninstall in flight
|
2026-05-02 14:24:09 +07:00
|
|
|
const [selectedVersions, setSelectedVersions] = useState<Record<string, string>>({});
|
2026-04-22 02:45:45 +07:00
|
|
|
const [statusMsg, setStatusMsg] = useState<{ type: 'success' | 'error'; text: string } | null>(
|
|
|
|
|
null,
|
|
|
|
|
);
|
|
|
|
|
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
2026-03-05 08:05:23 +07:00
|
|
|
|
2026-06-08 20:51:34 +07:00
|
|
|
const setLibraries = useCallback(
|
|
|
|
|
(libs: string[] | null) => {
|
|
|
|
|
if (!activeBoard) return;
|
|
|
|
|
updateBoard(activeBoard.id, { libraries: libs && libs.length ? libs : undefined });
|
|
|
|
|
},
|
|
|
|
|
[activeBoard, updateBoard],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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 = manifestLibs ?? [];
|
|
|
|
|
if (cur.some((l) => normLib(l) === normLib(clean))) return;
|
|
|
|
|
setLibraries([...cur, clean]);
|
|
|
|
|
},
|
|
|
|
|
[manifestLibs, setLibraries],
|
|
|
|
|
);
|
|
|
|
|
const removeFromManifest = useCallback(
|
|
|
|
|
(name: string) => {
|
|
|
|
|
const cur = manifestLibs ?? [];
|
|
|
|
|
const next = cur.filter((l) => normLib(l) !== normLib(name));
|
|
|
|
|
setLibraries(next.length ? next : null);
|
|
|
|
|
},
|
|
|
|
|
[manifestLibs, setLibraries],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const isInstalled = useCallback(
|
|
|
|
|
(name: string): boolean =>
|
|
|
|
|
installedLibraries.some((il) => normLib(il.library?.name || il.name || '') === normLib(name)),
|
|
|
|
|
[installedLibraries],
|
|
|
|
|
);
|
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
const fetchInstalled = useCallback(async () => {
|
|
|
|
|
setLoadingInstalled(true);
|
|
|
|
|
try {
|
2026-06-08 20:51:34 +07:00
|
|
|
// The shared index libs PLUS the user's per-user custom uploads (which
|
|
|
|
|
// live in their per-user store). Custom first; de-duped by name.
|
2026-06-08 00:58:13 +07:00
|
|
|
const [libs, custom] = await Promise.all([getInstalledLibraries(), getCustomLibraries()]);
|
|
|
|
|
const customNames = new Set(custom.map((c) => (c.name || '').toLowerCase()));
|
2026-06-08 20:51:34 +07:00
|
|
|
setInstalledLibraries([
|
2026-06-08 00:58:13 +07:00
|
|
|
...custom,
|
|
|
|
|
...libs.filter((l) => !customNames.has((l.library?.name || l.name || '').toLowerCase())),
|
2026-06-08 20:51:34 +07:00
|
|
|
]);
|
2026-04-22 02:45:45 +07:00
|
|
|
} catch (e: unknown) {
|
|
|
|
|
setStatusMsg({
|
|
|
|
|
type: 'error',
|
|
|
|
|
text: e instanceof Error ? e.message : 'Failed to load installed libraries',
|
|
|
|
|
});
|
|
|
|
|
} finally {
|
|
|
|
|
setLoadingInstalled(false);
|
|
|
|
|
}
|
|
|
|
|
}, []);
|
2026-03-05 08:05:23 +07:00
|
|
|
|
2026-06-08 20:51:34 +07:00
|
|
|
// Reset transient state when the modal closes.
|
2026-04-22 02:45:45 +07:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (!isOpen) {
|
|
|
|
|
setSearchQuery('');
|
|
|
|
|
setSearchResults([]);
|
|
|
|
|
setStatusMsg(null);
|
|
|
|
|
}
|
|
|
|
|
}, [isOpen]);
|
2026-03-05 11:23:59 +07:00
|
|
|
|
2026-06-08 20:51:34 +07:00
|
|
|
// Load the installed/custom list whenever the modal opens.
|
2026-04-22 02:45:45 +07:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (isOpen) fetchInstalled();
|
|
|
|
|
}, [isOpen, fetchInstalled]);
|
2026-03-06 07:07:10 +07:00
|
|
|
|
2026-08-03 08:01:54 +07:00
|
|
|
// Escape closes it, like any dialog. The overlay already closes on a
|
|
|
|
|
// backdrop click, but the toolbar button that OPENED this sits underneath
|
|
|
|
|
// the overlay — so someone who installs a library and reaches for it to get
|
|
|
|
|
// back to their sketch clicks a button that cannot be hit, and the editor
|
|
|
|
|
// reads as frozen. Escape is the exit everyone tries next.
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!isOpen) return;
|
|
|
|
|
const onKey = (e: KeyboardEvent) => {
|
|
|
|
|
if (e.key === 'Escape') onClose();
|
|
|
|
|
};
|
|
|
|
|
document.addEventListener('keydown', onKey);
|
|
|
|
|
return () => document.removeEventListener('keydown', onKey);
|
|
|
|
|
}, [isOpen, onClose]);
|
|
|
|
|
|
2026-06-08 20:51:34 +07:00
|
|
|
// Search the index (debounced). With an empty query we BROWSE the installed
|
|
|
|
|
// list instead, so don't fire a search.
|
2026-04-22 02:45:45 +07:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (!isOpen) return;
|
|
|
|
|
if (debounceRef.current) clearTimeout(debounceRef.current);
|
2026-06-08 20:51:34 +07:00
|
|
|
if (!searchQuery.trim()) {
|
|
|
|
|
setSearchResults([]);
|
|
|
|
|
setLoadingSearch(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-04-22 02:45:45 +07:00
|
|
|
debounceRef.current = setTimeout(async () => {
|
|
|
|
|
setLoadingSearch(true);
|
|
|
|
|
setStatusMsg(null);
|
|
|
|
|
try {
|
feat(micropython): install libraries from the micropython-lib index (#214)
A MicroPython library in Velxio IS a .py file in the project, copied onto
the board's filesystem before main.py runs. That mechanism worked, but
getting the file meant hunting GitHub by hand — the gap issue #214 is
about. Now the Library Manager fills it, in both halves:
Backend: /api/micropython-libs proxies the official micropython-lib
package index (the same one `mip` uses on real hardware). /search filters
the index; /fetch resolves a package plus its declared deps into .py
files. The proxy is what makes this safe to expose: only the official
index is reachable (the name must exist in index.json, files come from its
content-addressed store), source only (.py, never .mpy bytecode), path-
safe, and capped at 24 files / 256 KB per install. In-process TTL cache
over the index.
Frontend: the same Library Manager modal, mode-aware. On a MicroPython
board the search hits the index and "Add to project" writes the package's
files into the board's workspace (upsert — re-adding is the upgrade path),
visible in the explorer and travelling with the project. Before typing it
shows featured picks (ssd1306, umqtt.*, urequests) and the modules already
frozen into the firmware (neopixel, dht, onewire, ds18x20, machine,
network) labeled "built into the firmware", so searching for one answers
"just import it" instead of an empty list.
The file-materialization prelude now creates package directories: umqtt
ships as umqtt/simple.py, and MicroPython's open() does not mkdir, so the
prelude died with ENOENT on any packaged module.
Also ships a gallery example ("ESP32 + MicroPython: use an external
library") that documents the file-is-the-library mechanism with the
ssd1306 driver on the wired OLED.
2026-08-19 02:50:02 +07:00
|
|
|
if (isMpy) setMpyResults(await searchMpyPackages(searchQuery));
|
|
|
|
|
else setSearchResults(await searchLibraries(searchQuery));
|
2026-04-22 02:45:45 +07:00
|
|
|
} catch (e: unknown) {
|
|
|
|
|
setStatusMsg({ type: 'error', text: e instanceof Error ? e.message : 'Search failed' });
|
feat(micropython): install libraries from the micropython-lib index (#214)
A MicroPython library in Velxio IS a .py file in the project, copied onto
the board's filesystem before main.py runs. That mechanism worked, but
getting the file meant hunting GitHub by hand — the gap issue #214 is
about. Now the Library Manager fills it, in both halves:
Backend: /api/micropython-libs proxies the official micropython-lib
package index (the same one `mip` uses on real hardware). /search filters
the index; /fetch resolves a package plus its declared deps into .py
files. The proxy is what makes this safe to expose: only the official
index is reachable (the name must exist in index.json, files come from its
content-addressed store), source only (.py, never .mpy bytecode), path-
safe, and capped at 24 files / 256 KB per install. In-process TTL cache
over the index.
Frontend: the same Library Manager modal, mode-aware. On a MicroPython
board the search hits the index and "Add to project" writes the package's
files into the board's workspace (upsert — re-adding is the upgrade path),
visible in the explorer and travelling with the project. Before typing it
shows featured picks (ssd1306, umqtt.*, urequests) and the modules already
frozen into the firmware (neopixel, dht, onewire, ds18x20, machine,
network) labeled "built into the firmware", so searching for one answers
"just import it" instead of an empty list.
The file-materialization prelude now creates package directories: umqtt
ships as umqtt/simple.py, and MicroPython's open() does not mkdir, so the
prelude died with ENOENT on any packaged module.
Also ships a gallery example ("ESP32 + MicroPython: use an external
library") that documents the file-is-the-library mechanism with the
ssd1306 driver on the wired OLED.
2026-08-19 02:50:02 +07:00
|
|
|
if (isMpy) setMpyResults([]);
|
|
|
|
|
else setSearchResults([]);
|
2026-04-22 02:45:45 +07:00
|
|
|
} finally {
|
|
|
|
|
setLoadingSearch(false);
|
|
|
|
|
}
|
2026-06-08 20:51:34 +07:00
|
|
|
}, 400);
|
2026-04-22 02:45:45 +07:00
|
|
|
return () => {
|
|
|
|
|
if (debounceRef.current) clearTimeout(debounceRef.current);
|
2026-03-05 08:05:23 +07:00
|
|
|
};
|
feat(micropython): install libraries from the micropython-lib index (#214)
A MicroPython library in Velxio IS a .py file in the project, copied onto
the board's filesystem before main.py runs. That mechanism worked, but
getting the file meant hunting GitHub by hand — the gap issue #214 is
about. Now the Library Manager fills it, in both halves:
Backend: /api/micropython-libs proxies the official micropython-lib
package index (the same one `mip` uses on real hardware). /search filters
the index; /fetch resolves a package plus its declared deps into .py
files. The proxy is what makes this safe to expose: only the official
index is reachable (the name must exist in index.json, files come from its
content-addressed store), source only (.py, never .mpy bytecode), path-
safe, and capped at 24 files / 256 KB per install. In-process TTL cache
over the index.
Frontend: the same Library Manager modal, mode-aware. On a MicroPython
board the search hits the index and "Add to project" writes the package's
files into the board's workspace (upsert — re-adding is the upgrade path),
visible in the explorer and travelling with the project. Before typing it
shows featured picks (ssd1306, umqtt.*, urequests) and the modules already
frozen into the firmware (neopixel, dht, onewire, ds18x20, machine,
network) labeled "built into the firmware", so searching for one answers
"just import it" instead of an empty list.
The file-materialization prelude now creates package directories: umqtt
ships as umqtt/simple.py, and MicroPython's open() does not mkdir, so the
prelude died with ENOENT on any packaged module.
Also ships a gallery example ("ESP32 + MicroPython: use an external
library") that documents the file-is-the-library mechanism with the
ssd1306 driver on the wired OLED.
2026-08-19 02:50:02 +07:00
|
|
|
}, [searchQuery, isOpen, isMpy]);
|
2026-03-05 08:05:23 +07:00
|
|
|
|
2026-06-08 20:51:34 +07:00
|
|
|
// A custom .zip upload (pro) lands in the user's per-user store; auto-declare
|
|
|
|
|
// it on the active board and refresh the list. The upload BUTTON is injected
|
|
|
|
|
// into .lib-modal-header by the pro overlay (libraryUploadInjector).
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const onUploaded = (e: Event) => {
|
|
|
|
|
const name = (e as CustomEvent).detail?.library;
|
|
|
|
|
if (name) addToManifest(name);
|
|
|
|
|
fetchInstalled();
|
|
|
|
|
};
|
|
|
|
|
window.addEventListener('velxio-custom-library-installed', onUploaded);
|
|
|
|
|
return () => window.removeEventListener('velxio-custom-library-installed', onUploaded);
|
|
|
|
|
}, [addToManifest, fetchInstalled]);
|
2026-05-02 14:24:09 +07:00
|
|
|
|
2026-06-08 20:51:34 +07:00
|
|
|
// ── actions ────────────────────────────────────────────────────────────────
|
|
|
|
|
const install = useCallback(
|
|
|
|
|
async (name: string): Promise<boolean> => {
|
|
|
|
|
setBusyLib(name);
|
|
|
|
|
setStatusMsg(null);
|
|
|
|
|
try {
|
|
|
|
|
const result = await installLibrary(name, selectedVersions[name]);
|
|
|
|
|
if (result.success) {
|
|
|
|
|
trackInstallLibrary(name);
|
|
|
|
|
fetchInstalled();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
setStatusMsg({ type: 'error', text: result.error || `Failed to install "${name}"` });
|
|
|
|
|
return false;
|
|
|
|
|
} catch (e: unknown) {
|
|
|
|
|
setStatusMsg({ type: 'error', text: e instanceof Error ? e.message : 'Installation failed' });
|
|
|
|
|
return false;
|
|
|
|
|
} finally {
|
|
|
|
|
setBusyLib(null);
|
2026-06-08 00:58:13 +07:00
|
|
|
}
|
2026-06-08 20:51:34 +07:00
|
|
|
},
|
|
|
|
|
[selectedVersions, fetchInstalled],
|
2026-06-07 10:08:34 +07:00
|
|
|
);
|
|
|
|
|
|
2026-06-08 20:51:34 +07:00
|
|
|
// Primary action: install if needed, then declare on THIS board.
|
|
|
|
|
const addToProject = useCallback(
|
|
|
|
|
async (row: LibRow) => {
|
|
|
|
|
if (!row.installed) {
|
|
|
|
|
const ok = await install(row.name);
|
|
|
|
|
if (!ok) return;
|
|
|
|
|
}
|
|
|
|
|
addToManifest(row.name);
|
|
|
|
|
setStatusMsg({ type: 'success', text: `"${row.name}" added to ${activeBoard ? boardDisplayName(activeBoard) : 'this board'}.` });
|
2026-06-07 10:08:34 +07:00
|
|
|
},
|
2026-06-08 20:51:34 +07:00
|
|
|
[install, addToManifest, activeBoard],
|
2026-06-07 10:08:34 +07:00
|
|
|
);
|
|
|
|
|
|
2026-06-08 20:51:34 +07:00
|
|
|
// A CUSTOM lib lives in the user's per-user store, so removing it hits the
|
|
|
|
|
// per-user delete endpoint and also drops it from the manifest.
|
|
|
|
|
const removeCustom = useCallback(
|
|
|
|
|
async (name: string) => {
|
|
|
|
|
setBusyLib(name);
|
|
|
|
|
setStatusMsg(null);
|
2026-06-07 11:07:48 +07:00
|
|
|
try {
|
2026-06-08 20:51:34 +07:00
|
|
|
const result = await deleteCustomLibrary(name);
|
|
|
|
|
if (result.success) {
|
|
|
|
|
setStatusMsg({ type: 'success', text: `Removed your custom "${name}".` });
|
|
|
|
|
removeFromManifest(name);
|
|
|
|
|
fetchInstalled();
|
|
|
|
|
} else {
|
|
|
|
|
setStatusMsg({ type: 'error', text: result.error || `Failed to remove "${name}"` });
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
setBusyLib(null);
|
2026-06-07 11:07:48 +07:00
|
|
|
}
|
2026-06-08 20:51:34 +07:00
|
|
|
},
|
|
|
|
|
[removeFromManifest, fetchInstalled],
|
|
|
|
|
);
|
2026-06-07 11:07:48 +07:00
|
|
|
|
2026-06-08 20:51:34 +07:00
|
|
|
// ── unified rows: search results when typing, else the installed/custom list ─
|
|
|
|
|
const rows: LibRow[] = useMemo(() => {
|
|
|
|
|
if (searchQuery.trim()) {
|
|
|
|
|
return searchResults.map((lib) => ({
|
|
|
|
|
name: lib.name || 'Unknown',
|
|
|
|
|
version: lib.latest?.version || lib.version || '',
|
|
|
|
|
author: lib.latest?.author || lib.author || '',
|
|
|
|
|
desc: lib.latest?.sentence || lib.sentence || '',
|
|
|
|
|
installed: isInstalled(lib.name || ''),
|
|
|
|
|
custom: false,
|
|
|
|
|
releases: lib.releases,
|
|
|
|
|
}));
|
2026-06-07 10:08:34 +07:00
|
|
|
}
|
2026-06-08 20:51:34 +07:00
|
|
|
return installedLibraries.map((lib) => ({
|
|
|
|
|
name: lib.library?.name || lib.name || 'Unknown',
|
|
|
|
|
version: lib.library?.version || lib.version || '',
|
|
|
|
|
author: lib.library?.author || lib.author || '',
|
|
|
|
|
desc: lib.library?.sentence || lib.sentence || '',
|
|
|
|
|
installed: true,
|
|
|
|
|
custom: !!lib.custom,
|
|
|
|
|
}));
|
|
|
|
|
}, [searchQuery, searchResults, installedLibraries, isInstalled]);
|
2026-03-05 08:05:23 +07:00
|
|
|
|
feat(micropython): install libraries from the micropython-lib index (#214)
A MicroPython library in Velxio IS a .py file in the project, copied onto
the board's filesystem before main.py runs. That mechanism worked, but
getting the file meant hunting GitHub by hand — the gap issue #214 is
about. Now the Library Manager fills it, in both halves:
Backend: /api/micropython-libs proxies the official micropython-lib
package index (the same one `mip` uses on real hardware). /search filters
the index; /fetch resolves a package plus its declared deps into .py
files. The proxy is what makes this safe to expose: only the official
index is reachable (the name must exist in index.json, files come from its
content-addressed store), source only (.py, never .mpy bytecode), path-
safe, and capped at 24 files / 256 KB per install. In-process TTL cache
over the index.
Frontend: the same Library Manager modal, mode-aware. On a MicroPython
board the search hits the index and "Add to project" writes the package's
files into the board's workspace (upsert — re-adding is the upgrade path),
visible in the explorer and travelling with the project. Before typing it
shows featured picks (ssd1306, umqtt.*, urequests) and the modules already
frozen into the firmware (neopixel, dht, onewire, ds18x20, machine,
network) labeled "built into the firmware", so searching for one answers
"just import it" instead of an empty list.
The file-materialization prelude now creates package directories: umqtt
ships as umqtt/simple.py, and MicroPython's open() does not mkdir, so the
prelude died with ENOENT on any packaged module.
Also ships a gallery example ("ESP32 + MicroPython: use an external
library") that documents the file-is-the-library mechanism with the
ssd1306 driver on the wired OLED.
2026-08-19 02:50:02 +07:00
|
|
|
// ── MicroPython actions ───────────────────────────────────────────────────
|
|
|
|
|
const mpyGroupId = activeBoard?.activeFileGroupId ?? null;
|
|
|
|
|
const addMpyPackage = useCallback(
|
|
|
|
|
async (name: string) => {
|
|
|
|
|
if (!mpyGroupId) return;
|
|
|
|
|
setBusyLib(name);
|
|
|
|
|
setStatusMsg(null);
|
|
|
|
|
try {
|
|
|
|
|
const files = await fetchMpyPackage(name);
|
|
|
|
|
const written = writeFilesIntoGroup(mpyGroupId, files);
|
|
|
|
|
trackInstallLibrary(`mpy:${name}`);
|
|
|
|
|
setStatusMsg({
|
|
|
|
|
type: 'success',
|
|
|
|
|
text: `"${name}" added to the workspace: ${written.join(', ')}. main.py can import it now.`,
|
|
|
|
|
});
|
|
|
|
|
setMpyTick((n) => n + 1);
|
|
|
|
|
} catch (e: unknown) {
|
|
|
|
|
const detail =
|
|
|
|
|
(e as { response?: { data?: { detail?: string } } })?.response?.data?.detail;
|
|
|
|
|
setStatusMsg({
|
|
|
|
|
type: 'error',
|
|
|
|
|
text: detail || (e instanceof Error ? e.message : `Failed to add "${name}"`),
|
|
|
|
|
});
|
|
|
|
|
} finally {
|
|
|
|
|
setBusyLib(null);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[mpyGroupId],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
interface MpyRow {
|
|
|
|
|
name: string;
|
|
|
|
|
version: string;
|
|
|
|
|
desc: string;
|
|
|
|
|
builtin: boolean;
|
|
|
|
|
inProject: boolean;
|
|
|
|
|
}
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- mpyTick re-derives from the file group
|
|
|
|
|
const mpyRows: MpyRow[] = useMemo(() => {
|
|
|
|
|
if (!isMpy || !mpyGroupId) return [];
|
|
|
|
|
const inProj = (name: string) => packageFilesInGroup(mpyGroupId, name);
|
|
|
|
|
if (searchQuery.trim()) {
|
|
|
|
|
const needle = searchQuery.trim().toLowerCase();
|
|
|
|
|
const builtins = BUILTIN_MPY_MODULES.filter((b) => b.name.includes(needle)).map((b) => ({
|
|
|
|
|
name: b.name,
|
|
|
|
|
version: '',
|
|
|
|
|
desc: b.blurb,
|
|
|
|
|
builtin: true,
|
|
|
|
|
inProject: false,
|
|
|
|
|
}));
|
|
|
|
|
return builtins.concat(
|
|
|
|
|
mpyResults.map((p) => ({
|
|
|
|
|
name: p.name,
|
|
|
|
|
version: p.version,
|
|
|
|
|
desc: p.description,
|
|
|
|
|
builtin: false,
|
|
|
|
|
inProject: inProj(p.name),
|
|
|
|
|
})),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return FEATURED_MPY_PACKAGES.map((f) => ({
|
|
|
|
|
name: f.name,
|
|
|
|
|
version: '',
|
|
|
|
|
desc: f.blurb,
|
|
|
|
|
builtin: false,
|
|
|
|
|
inProject: inProj(f.name),
|
|
|
|
|
})).concat(
|
|
|
|
|
BUILTIN_MPY_MODULES.map((b) => ({
|
|
|
|
|
name: b.name,
|
|
|
|
|
version: '',
|
|
|
|
|
desc: b.blurb,
|
|
|
|
|
builtin: true,
|
|
|
|
|
inProject: false,
|
|
|
|
|
})),
|
|
|
|
|
);
|
|
|
|
|
}, [isMpy, mpyGroupId, searchQuery, mpyResults, mpyTick]);
|
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
if (!isOpen) return null;
|
2026-06-08 20:51:34 +07:00
|
|
|
const browsing = !searchQuery.trim();
|
2026-03-05 08:05:23 +07:00
|
|
|
|
2026-08-15 13:19:30 +07:00
|
|
|
// Portaled to <body>: the toolbar ancestor has container-type
|
|
|
|
|
// (container queries), which turns it into the containing block for
|
|
|
|
|
// position:fixed and shrank the overlay to the toolbar's box.
|
|
|
|
|
return createPortal(
|
2026-06-08 20:51:34 +07:00
|
|
|
<div className="lib-modal-overlay" onClick={onClose}>
|
2026-04-22 02:45:45 +07:00
|
|
|
<div className="lib-modal" onClick={(e) => e.stopPropagation()}>
|
2026-06-08 20:51:34 +07:00
|
|
|
{/* Header — the pro custom-upload button injects into .lib-modal-header */}
|
2026-04-22 02:45:45 +07:00
|
|
|
<div className="lib-modal-header">
|
|
|
|
|
<div className="lib-modal-title">
|
|
|
|
|
<svg
|
|
|
|
|
className="lib-icon"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
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>
|
2026-05-09 21:29:07 +07:00
|
|
|
<span>{t('editor.libraryManager.title')}</span>
|
2026-06-08 20:51:34 +07:00
|
|
|
{activeBoard && (
|
|
|
|
|
<span
|
feat(micropython): install libraries from the micropython-lib index (#214)
A MicroPython library in Velxio IS a .py file in the project, copied onto
the board's filesystem before main.py runs. That mechanism worked, but
getting the file meant hunting GitHub by hand — the gap issue #214 is
about. Now the Library Manager fills it, in both halves:
Backend: /api/micropython-libs proxies the official micropython-lib
package index (the same one `mip` uses on real hardware). /search filters
the index; /fetch resolves a package plus its declared deps into .py
files. The proxy is what makes this safe to expose: only the official
index is reachable (the name must exist in index.json, files come from its
content-addressed store), source only (.py, never .mpy bytecode), path-
safe, and capped at 24 files / 256 KB per install. In-process TTL cache
over the index.
Frontend: the same Library Manager modal, mode-aware. On a MicroPython
board the search hits the index and "Add to project" writes the package's
files into the board's workspace (upsert — re-adding is the upgrade path),
visible in the explorer and travelling with the project. Before typing it
shows featured picks (ssd1306, umqtt.*, urequests) and the modules already
frozen into the firmware (neopixel, dht, onewire, ds18x20, machine,
network) labeled "built into the firmware", so searching for one answers
"just import it" instead of an empty list.
The file-materialization prelude now creates package directories: umqtt
ships as umqtt/simple.py, and MicroPython's open() does not mkdir, so the
prelude died with ENOENT on any packaged module.
Also ships a gallery example ("ESP32 + MicroPython: use an external
library") that documents the file-is-the-library mechanism with the
ssd1306 driver on the wired OLED.
2026-08-19 02:50:02 +07:00
|
|
|
title={
|
|
|
|
|
isMpy
|
|
|
|
|
? 'MicroPython: a library is a .py file in this board\'s workspace'
|
|
|
|
|
: 'Libraries apply to this board (its libraries.json = compile scope)'
|
|
|
|
|
}
|
2026-06-08 20:51:34 +07:00
|
|
|
style={{
|
|
|
|
|
marginLeft: 8,
|
|
|
|
|
fontSize: 11,
|
|
|
|
|
color: '#a5d6a7',
|
|
|
|
|
background: '#1b3a1e',
|
|
|
|
|
border: '1px solid #2e7d32',
|
|
|
|
|
borderRadius: 10,
|
|
|
|
|
padding: '1px 8px',
|
|
|
|
|
}}
|
|
|
|
|
>
|
feat(micropython): install libraries from the micropython-lib index (#214)
A MicroPython library in Velxio IS a .py file in the project, copied onto
the board's filesystem before main.py runs. That mechanism worked, but
getting the file meant hunting GitHub by hand — the gap issue #214 is
about. Now the Library Manager fills it, in both halves:
Backend: /api/micropython-libs proxies the official micropython-lib
package index (the same one `mip` uses on real hardware). /search filters
the index; /fetch resolves a package plus its declared deps into .py
files. The proxy is what makes this safe to expose: only the official
index is reachable (the name must exist in index.json, files come from its
content-addressed store), source only (.py, never .mpy bytecode), path-
safe, and capped at 24 files / 256 KB per install. In-process TTL cache
over the index.
Frontend: the same Library Manager modal, mode-aware. On a MicroPython
board the search hits the index and "Add to project" writes the package's
files into the board's workspace (upsert — re-adding is the upgrade path),
visible in the explorer and travelling with the project. Before typing it
shows featured picks (ssd1306, umqtt.*, urequests) and the modules already
frozen into the firmware (neopixel, dht, onewire, ds18x20, machine,
network) labeled "built into the firmware", so searching for one answers
"just import it" instead of an empty list.
The file-materialization prelude now creates package directories: umqtt
ships as umqtt/simple.py, and MicroPython's open() does not mkdir, so the
prelude died with ENOENT on any packaged module.
Also ships a gallery example ("ESP32 + MicroPython: use an external
library") that documents the file-is-the-library mechanism with the
ssd1306 driver on the wired OLED.
2026-08-19 02:50:02 +07:00
|
|
|
{boardDisplayName(activeBoard)}
|
|
|
|
|
{isMpy ? ' · MicroPython' : ` · ${declared.length}`}
|
2026-06-08 20:51:34 +07:00
|
|
|
</span>
|
|
|
|
|
)}
|
2026-04-22 02:45:45 +07:00
|
|
|
</div>
|
2026-06-08 20:51:34 +07:00
|
|
|
<button className="lib-close-btn" onClick={onClose}>
|
|
|
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
|
2026-04-22 02:45:45 +07:00
|
|
|
<line x1="18" y1="6" x2="6" y2="18" />
|
|
|
|
|
<line x1="6" y1="6" x2="18" y2="18" />
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
2026-03-05 08:05:23 +07:00
|
|
|
|
2026-06-08 20:51:34 +07:00
|
|
|
{/* Search */}
|
|
|
|
|
<div className="lib-search-bar">
|
|
|
|
|
<svg
|
|
|
|
|
className="lib-search-icon"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
strokeWidth="2"
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
2026-06-07 10:08:34 +07:00
|
|
|
>
|
2026-06-08 20:51:34 +07:00
|
|
|
<circle cx="11" cy="11" r="8" />
|
|
|
|
|
<path d="m21 21-4.3-4.3" />
|
|
|
|
|
</svg>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder={t('editor.libraryManager.filterPlaceholder')}
|
|
|
|
|
value={searchQuery}
|
|
|
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
|
|
|
autoFocus
|
|
|
|
|
/>
|
|
|
|
|
{loadingSearch && (
|
|
|
|
|
<svg className="lib-spinner" 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>
|
|
|
|
|
)}
|
2026-04-22 02:45:45 +07:00
|
|
|
</div>
|
2026-03-05 08:05:23 +07:00
|
|
|
|
2026-06-08 20:51:34 +07:00
|
|
|
{/* Status */}
|
|
|
|
|
{statusMsg && <div className={`lib-status ${statusMsg.type}`}>{statusMsg.text}</div>}
|
2026-06-07 10:08:34 +07:00
|
|
|
|
2026-06-08 20:51:34 +07:00
|
|
|
{/* Single unified list */}
|
|
|
|
|
<div className="lib-content">
|
|
|
|
|
<div className="lib-list">
|
feat(micropython): install libraries from the micropython-lib index (#214)
A MicroPython library in Velxio IS a .py file in the project, copied onto
the board's filesystem before main.py runs. That mechanism worked, but
getting the file meant hunting GitHub by hand — the gap issue #214 is
about. Now the Library Manager fills it, in both halves:
Backend: /api/micropython-libs proxies the official micropython-lib
package index (the same one `mip` uses on real hardware). /search filters
the index; /fetch resolves a package plus its declared deps into .py
files. The proxy is what makes this safe to expose: only the official
index is reachable (the name must exist in index.json, files come from its
content-addressed store), source only (.py, never .mpy bytecode), path-
safe, and capped at 24 files / 256 KB per install. In-process TTL cache
over the index.
Frontend: the same Library Manager modal, mode-aware. On a MicroPython
board the search hits the index and "Add to project" writes the package's
files into the board's workspace (upsert — re-adding is the upgrade path),
visible in the explorer and travelling with the project. Before typing it
shows featured picks (ssd1306, umqtt.*, urequests) and the modules already
frozen into the firmware (neopixel, dht, onewire, ds18x20, machine,
network) labeled "built into the firmware", so searching for one answers
"just import it" instead of an empty list.
The file-materialization prelude now creates package directories: umqtt
ships as umqtt/simple.py, and MicroPython's open() does not mkdir, so the
prelude died with ENOENT on any packaged module.
Also ships a gallery example ("ESP32 + MicroPython: use an external
library") that documents the file-is-the-library mechanism with the
ssd1306 driver on the wired OLED.
2026-08-19 02:50:02 +07:00
|
|
|
{isMpy && browsing && (
|
|
|
|
|
<div className="lib-empty" style={{ padding: '10px 14px 4px' }}>
|
|
|
|
|
<p className="lib-empty-sub" style={{ margin: 0 }}>
|
|
|
|
|
A MicroPython library is a .py file in the project — adding one
|
|
|
|
|
writes it into this board's workspace, next to main.py. Search
|
|
|
|
|
the official micropython-lib index above.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{isMpy &&
|
|
|
|
|
mpyRows.map((row) => {
|
|
|
|
|
const busy = busyLib === row.name;
|
|
|
|
|
return (
|
|
|
|
|
<div key={`mpy-${row.name}`} className="lib-item">
|
|
|
|
|
<div className="lib-item-info">
|
|
|
|
|
<div className="lib-item-header">
|
|
|
|
|
<span className="lib-item-name">{row.name}</span>
|
|
|
|
|
{row.builtin && (
|
|
|
|
|
<span
|
|
|
|
|
style={{
|
|
|
|
|
fontSize: 10,
|
|
|
|
|
color: '#8ecae6',
|
|
|
|
|
background: '#12303f',
|
|
|
|
|
borderRadius: 6,
|
|
|
|
|
padding: '0 6px',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
built into the firmware
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
{row.desc && <p className="lib-item-desc">{row.desc}</p>}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="lib-item-actions">
|
|
|
|
|
{row.version && <span className="lib-item-version">{row.version}</span>}
|
|
|
|
|
{row.builtin ? (
|
|
|
|
|
<span className="lib-item-version" title="No install needed — just import it">
|
|
|
|
|
import {row.name}
|
|
|
|
|
</span>
|
|
|
|
|
) : row.inProject ? (
|
|
|
|
|
<span
|
|
|
|
|
className="lib-uninstall-btn"
|
|
|
|
|
style={{ color: '#a5d6a7', borderColor: '#2e7d32', cursor: 'default' }}
|
|
|
|
|
title="Its .py file is in the workspace — edit or delete it from the file explorer"
|
|
|
|
|
>
|
|
|
|
|
In project ✓
|
|
|
|
|
</span>
|
|
|
|
|
) : (
|
|
|
|
|
<button
|
|
|
|
|
className="lib-install-btn"
|
|
|
|
|
onClick={() => addMpyPackage(row.name)}
|
|
|
|
|
disabled={busy}
|
|
|
|
|
title="Write this package's .py files into the board's workspace"
|
|
|
|
|
>
|
|
|
|
|
{busy ? '…' : '+ Add to project'}
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
{isMpy && !browsing && !loadingSearch && mpyRows.length === 0 && (
|
|
|
|
|
<div className="lib-empty">
|
|
|
|
|
<p>{t('editor.libraryManager.noResultsFor', { query: searchQuery })}</p>
|
|
|
|
|
<p className="lib-empty-sub">
|
|
|
|
|
Not in micropython-lib? Paste the driver as a .py file in the
|
|
|
|
|
workspace — that is all an install does.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{!isMpy && browsing && loadingInstalled && (
|
2026-06-08 20:51:34 +07:00
|
|
|
<div className="lib-empty">
|
|
|
|
|
<p>{t('editor.libraryManager.loadingInstalled')}</p>
|
2026-06-07 11:07:48 +07:00
|
|
|
</div>
|
2026-06-08 20:51:34 +07:00
|
|
|
)}
|
feat(micropython): install libraries from the micropython-lib index (#214)
A MicroPython library in Velxio IS a .py file in the project, copied onto
the board's filesystem before main.py runs. That mechanism worked, but
getting the file meant hunting GitHub by hand — the gap issue #214 is
about. Now the Library Manager fills it, in both halves:
Backend: /api/micropython-libs proxies the official micropython-lib
package index (the same one `mip` uses on real hardware). /search filters
the index; /fetch resolves a package plus its declared deps into .py
files. The proxy is what makes this safe to expose: only the official
index is reachable (the name must exist in index.json, files come from its
content-addressed store), source only (.py, never .mpy bytecode), path-
safe, and capped at 24 files / 256 KB per install. In-process TTL cache
over the index.
Frontend: the same Library Manager modal, mode-aware. On a MicroPython
board the search hits the index and "Add to project" writes the package's
files into the board's workspace (upsert — re-adding is the upgrade path),
visible in the explorer and travelling with the project. Before typing it
shows featured picks (ssd1306, umqtt.*, urequests) and the modules already
frozen into the firmware (neopixel, dht, onewire, ds18x20, machine,
network) labeled "built into the firmware", so searching for one answers
"just import it" instead of an empty list.
The file-materialization prelude now creates package directories: umqtt
ships as umqtt/simple.py, and MicroPython's open() does not mkdir, so the
prelude died with ENOENT on any packaged module.
Also ships a gallery example ("ESP32 + MicroPython: use an external
library") that documents the file-is-the-library mechanism with the
ssd1306 driver on the wired OLED.
2026-08-19 02:50:02 +07:00
|
|
|
{!isMpy && !loadingSearch && rows.length === 0 && !(browsing && loadingInstalled) && (
|
2026-06-08 20:51:34 +07:00
|
|
|
<div className="lib-empty">
|
|
|
|
|
<p>
|
|
|
|
|
{browsing
|
|
|
|
|
? t('editor.libraryManager.noneInstalled')
|
|
|
|
|
: searchQuery
|
2026-05-09 21:29:07 +07:00
|
|
|
? t('editor.libraryManager.noResultsFor', { query: searchQuery })
|
|
|
|
|
: t('editor.libraryManager.noResults')}
|
2026-06-08 20:51:34 +07:00
|
|
|
</p>
|
|
|
|
|
{browsing && <p className="lib-empty-sub">Search above to find and add a library.</p>}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
feat(micropython): install libraries from the micropython-lib index (#214)
A MicroPython library in Velxio IS a .py file in the project, copied onto
the board's filesystem before main.py runs. That mechanism worked, but
getting the file meant hunting GitHub by hand — the gap issue #214 is
about. Now the Library Manager fills it, in both halves:
Backend: /api/micropython-libs proxies the official micropython-lib
package index (the same one `mip` uses on real hardware). /search filters
the index; /fetch resolves a package plus its declared deps into .py
files. The proxy is what makes this safe to expose: only the official
index is reachable (the name must exist in index.json, files come from its
content-addressed store), source only (.py, never .mpy bytecode), path-
safe, and capped at 24 files / 256 KB per install. In-process TTL cache
over the index.
Frontend: the same Library Manager modal, mode-aware. On a MicroPython
board the search hits the index and "Add to project" writes the package's
files into the board's workspace (upsert — re-adding is the upgrade path),
visible in the explorer and travelling with the project. Before typing it
shows featured picks (ssd1306, umqtt.*, urequests) and the modules already
frozen into the firmware (neopixel, dht, onewire, ds18x20, machine,
network) labeled "built into the firmware", so searching for one answers
"just import it" instead of an empty list.
The file-materialization prelude now creates package directories: umqtt
ships as umqtt/simple.py, and MicroPython's open() does not mkdir, so the
prelude died with ENOENT on any packaged module.
Also ships a gallery example ("ESP32 + MicroPython: use an external
library") that documents the file-is-the-library mechanism with the
ssd1306 driver on the wired OLED.
2026-08-19 02:50:02 +07:00
|
|
|
{!isMpy && rows.map((row, i) => {
|
2026-06-08 20:51:34 +07:00
|
|
|
const inProj = inManifest(row.name);
|
|
|
|
|
const busy = busyLib === row.name;
|
|
|
|
|
return (
|
|
|
|
|
<div key={`${row.name}-${i}`} className="lib-item">
|
2026-04-22 02:45:45 +07:00
|
|
|
<div className="lib-item-info">
|
|
|
|
|
<div className="lib-item-header">
|
2026-06-08 20:51:34 +07:00
|
|
|
<span className="lib-item-name">{row.name}</span>
|
|
|
|
|
{row.custom && (
|
|
|
|
|
<span
|
|
|
|
|
style={{
|
|
|
|
|
fontSize: 10,
|
|
|
|
|
color: '#ffd60a',
|
|
|
|
|
background: '#3a330f',
|
|
|
|
|
borderRadius: 6,
|
|
|
|
|
padding: '0 6px',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
custom
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
{row.author && (
|
2026-05-09 21:29:07 +07:00
|
|
|
<span className="lib-item-author">
|
2026-06-08 20:51:34 +07:00
|
|
|
{t('editor.libraryManager.byAuthor', { author: row.custom ? 'you' : row.author })}
|
2026-05-09 21:29:07 +07:00
|
|
|
</span>
|
2026-04-22 02:45:45 +07:00
|
|
|
)}
|
2026-03-05 08:05:23 +07:00
|
|
|
</div>
|
2026-06-08 20:51:34 +07:00
|
|
|
{row.desc && <p className="lib-item-desc">{row.desc}</p>}
|
2026-04-22 02:45:45 +07:00
|
|
|
</div>
|
|
|
|
|
<div className="lib-item-actions">
|
2026-06-08 20:51:34 +07:00
|
|
|
{row.version && <span className="lib-item-version">{row.version}</span>}
|
|
|
|
|
{!row.installed && row.releases && Object.keys(row.releases).length > 1 && (
|
|
|
|
|
<select
|
|
|
|
|
className="lib-version-select"
|
|
|
|
|
value={selectedVersions[row.name] ?? row.version}
|
|
|
|
|
onChange={(e) => setSelectedVersions((p) => ({ ...p, [row.name]: e.target.value }))}
|
|
|
|
|
>
|
|
|
|
|
{Object.keys(row.releases).map((ver) => (
|
|
|
|
|
<option key={ver} value={ver}>
|
|
|
|
|
{ver}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
2026-04-22 02:45:45 +07:00
|
|
|
)}
|
2026-06-08 20:51:34 +07:00
|
|
|
{inProj ? (
|
2026-06-07 10:08:34 +07:00
|
|
|
<button
|
|
|
|
|
className="lib-uninstall-btn"
|
2026-06-08 20:51:34 +07:00
|
|
|
style={{ color: '#a5d6a7', borderColor: '#2e7d32' }}
|
|
|
|
|
onClick={() => removeFromManifest(row.name)}
|
|
|
|
|
title="Remove from this board (libraries.json)"
|
2026-06-07 10:08:34 +07:00
|
|
|
>
|
|
|
|
|
In project ✓
|
|
|
|
|
</button>
|
|
|
|
|
) : (
|
|
|
|
|
<button
|
|
|
|
|
className="lib-install-btn"
|
2026-06-08 20:51:34 +07:00
|
|
|
onClick={() => addToProject(row)}
|
|
|
|
|
disabled={busy}
|
|
|
|
|
title="Install if needed and add to this board"
|
2026-06-07 10:08:34 +07:00
|
|
|
>
|
2026-06-08 20:51:34 +07:00
|
|
|
{busy ? '…' : '+ Add to project'}
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
2026-06-09 20:49:14 +07:00
|
|
|
{/* Only CUSTOM uploads can be removed (per-user store). Index
|
|
|
|
|
libraries live in the shared content-addressed cache — you
|
|
|
|
|
add/remove them from THIS project, but never "uninstall" a
|
|
|
|
|
copy everyone shares, so no Uninstall button for them. */}
|
|
|
|
|
{row.custom && (
|
2026-06-08 20:51:34 +07:00
|
|
|
<button
|
|
|
|
|
className="lib-uninstall-btn"
|
2026-06-09 20:49:14 +07:00
|
|
|
onClick={() => removeCustom(row.name)}
|
2026-06-08 20:51:34 +07:00
|
|
|
disabled={busy}
|
2026-06-09 20:49:14 +07:00
|
|
|
title="Remove your custom upload"
|
2026-06-08 20:51:34 +07:00
|
|
|
>
|
2026-06-09 20:49:14 +07:00
|
|
|
{busy ? '…' : 'Remove'}
|
2026-06-07 10:08:34 +07:00
|
|
|
</button>
|
|
|
|
|
)}
|
2026-04-22 02:45:45 +07:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-06-08 20:51:34 +07:00
|
|
|
);
|
|
|
|
|
})}
|
2026-04-22 02:45:45 +07:00
|
|
|
</div>
|
2026-06-08 20:51:34 +07:00
|
|
|
</div>
|
2026-04-22 02:45:45 +07:00
|
|
|
</div>
|
2026-08-15 13:19:30 +07:00
|
|
|
</div>,
|
|
|
|
|
document.body,
|
2026-04-22 02:45:45 +07:00
|
|
|
);
|
2026-03-05 08:05:23 +07:00
|
|
|
};
|