project saving on device and browser
parent
3fdf4b1573
commit
1e6fe41ddd
48
index.html
48
index.html
|
|
@ -34,11 +34,8 @@
|
|||
<button id="btn-save" title="Save code to device as main.py" disabled>
|
||||
<span class="icon">💾</span> Save
|
||||
</button>
|
||||
<button id="btn-save-workspace" title="Save Blockly workspace to device" disabled>
|
||||
<span class="icon">💾</span> Save WS
|
||||
</button>
|
||||
<button id="btn-load-workspace" title="Load Blockly workspace from device" disabled>
|
||||
<span class="icon">📂</span> Load WS
|
||||
<button id="btn-projects" title="Open/save projects (browser or device)">
|
||||
<span class="icon">📂</span> Projects
|
||||
</button>
|
||||
<span id="connection-status" class="status-disconnected">Disconnected</span>
|
||||
</div>
|
||||
|
|
@ -68,6 +65,47 @@
|
|||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Projects dialog -->
|
||||
<div id="projects-overlay" class="hidden">
|
||||
<div id="projects-modal">
|
||||
<div class="projects-header">
|
||||
<h3>Projects</h3>
|
||||
<button id="projects-close">×</button>
|
||||
</div>
|
||||
<div class="projects-columns">
|
||||
<div class="projects-col">
|
||||
<h4>Browser</h4>
|
||||
<ul class="projects-list" id="browser-list"></ul>
|
||||
<div class="projects-actions">
|
||||
<div class="projects-name-row">
|
||||
<input type="text" id="browser-save-name" placeholder="Project name..." />
|
||||
<button id="browser-save-btn">Save</button>
|
||||
</div>
|
||||
<div class="projects-btn-row">
|
||||
<button id="browser-load-btn" disabled>Load</button>
|
||||
<button id="browser-delete-btn" class="btn-danger" disabled>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="projects-col">
|
||||
<h4>Device</h4>
|
||||
<ul class="projects-list" id="device-list"></ul>
|
||||
<div class="projects-actions">
|
||||
<div class="projects-name-row">
|
||||
<input type="text" id="device-save-name" placeholder="Project name..." />
|
||||
<button id="device-save-btn">Save</button>
|
||||
</div>
|
||||
<div class="projects-btn-row">
|
||||
<button id="device-load-btn" disabled>Load</button>
|
||||
<button id="device-delete-btn" class="btn-danger" disabled>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="device-status" class="projects-note"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Flash progress overlay -->
|
||||
<div id="flash-overlay" class="hidden">
|
||||
<div id="flash-modal">
|
||||
|
|
|
|||
146
src/main.js
146
src/main.js
|
|
@ -14,6 +14,7 @@ import { executeCode, stopExecution, saveToDevice, writeFileToDevice } from './s
|
|||
import { flashFirmware } from './serial/flasher.js';
|
||||
import { appendToTerminal, clearTerminal } from './ui/terminal.js';
|
||||
import { initResizablePanels } from './ui/panels.js';
|
||||
import { initProjectsDialog, open as openProjects } from './ui/projectsDialog.js';
|
||||
import './style.css';
|
||||
|
||||
// ─── Blockly Workspace ───────────────────────────────────
|
||||
|
|
@ -93,8 +94,7 @@ const btnFlash = document.getElementById('btn-flash');
|
|||
const btnRun = document.getElementById('btn-run');
|
||||
const btnStop = document.getElementById('btn-stop');
|
||||
const btnSave = document.getElementById('btn-save');
|
||||
const btnSaveWorkspace = document.getElementById('btn-save-workspace');
|
||||
const btnLoadWorkspace = document.getElementById('btn-load-workspace');
|
||||
const btnProjects = document.getElementById('btn-projects');
|
||||
const statusEl = document.getElementById('connection-status');
|
||||
const terminalInput = document.getElementById('terminal-input');
|
||||
|
||||
|
|
@ -118,36 +118,32 @@ function setConnectedUI(connected) {
|
|||
btnRun.disabled = !connected;
|
||||
btnStop.disabled = !connected;
|
||||
btnSave.disabled = !connected;
|
||||
btnSaveWorkspace.disabled = !connected;
|
||||
btnLoadWorkspace.disabled = !connected;
|
||||
terminalInput.disabled = !connected;
|
||||
statusEl.textContent = connected ? 'Connected' : 'Disconnected';
|
||||
statusEl.className = connected ? 'status-connected' : 'status-disconnected';
|
||||
}
|
||||
|
||||
// ─── Serial Event Listeners ──────────────────────────────
|
||||
// ─── Serial Capture (reusable promise-based) ─────────────
|
||||
|
||||
// Workspace loading state
|
||||
let workspaceCaptureState = null;
|
||||
let captureState = null;
|
||||
|
||||
onData((text) => {
|
||||
if (!workspaceCaptureState) {
|
||||
if (!captureState) {
|
||||
appendToTerminal(text);
|
||||
return;
|
||||
}
|
||||
|
||||
const { startMarker, endMarker } = workspaceCaptureState;
|
||||
workspaceCaptureState.raw += text;
|
||||
const raw = workspaceCaptureState.raw;
|
||||
const { startMarker, endMarker } = captureState;
|
||||
captureState.raw += text;
|
||||
const raw = captureState.raw;
|
||||
|
||||
const startIdx = raw.indexOf(startMarker);
|
||||
|
||||
if (startIdx === -1) {
|
||||
// No start marker yet — flush text that can't be part of the marker
|
||||
const keep = startMarker.length - 1;
|
||||
if (raw.length > keep) {
|
||||
appendToTerminal(raw.substring(0, raw.length - keep));
|
||||
workspaceCaptureState.raw = raw.substring(raw.length - keep);
|
||||
captureState.raw = raw.substring(raw.length - keep);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -156,35 +152,56 @@ onData((text) => {
|
|||
const endIdx = raw.indexOf(endMarker, contentStart);
|
||||
|
||||
if (endIdx === -1) {
|
||||
// Have start but no end yet — show text before start marker once
|
||||
if (!workspaceCaptureState.flushedPre && startIdx > 0) {
|
||||
if (!captureState.flushedPre && startIdx > 0) {
|
||||
appendToTerminal(raw.substring(0, startIdx));
|
||||
workspaceCaptureState.flushedPre = true;
|
||||
captureState.flushedPre = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Both markers found — extract content and load
|
||||
const jsonContent = raw.substring(contentStart, endIdx).trim();
|
||||
const beforeStart = startIdx > 0 && !workspaceCaptureState.flushedPre
|
||||
const content = raw.substring(contentStart, endIdx);
|
||||
const beforeStart = startIdx > 0 && !captureState.flushedPre
|
||||
? raw.substring(0, startIdx) : '';
|
||||
const afterEnd = raw.substring(endIdx + endMarker.length);
|
||||
const resolve = captureState.resolve;
|
||||
|
||||
workspaceCaptureState = null;
|
||||
captureState = null;
|
||||
|
||||
if (beforeStart) appendToTerminal(beforeStart);
|
||||
|
||||
try {
|
||||
const state = JSON.parse(jsonContent);
|
||||
Blockly.serialization.workspaces.load(state, workspace);
|
||||
appendToTerminal('Workspace loaded successfully!\n');
|
||||
} catch (parseErr) {
|
||||
appendToTerminal(`\nParse error: ${parseErr.message}\n`);
|
||||
}
|
||||
|
||||
if (afterEnd.trim()) appendToTerminal(afterEnd);
|
||||
|
||||
resolve(content);
|
||||
});
|
||||
|
||||
function captureDeviceOutput(script) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ts = Date.now();
|
||||
const sm = `__CAP_S_${ts}__`;
|
||||
const em = `__CAP_E_${ts}__`;
|
||||
|
||||
captureState = {
|
||||
startMarker: sm,
|
||||
endMarker: em,
|
||||
raw: '',
|
||||
flushedPre: false,
|
||||
resolve,
|
||||
};
|
||||
|
||||
const wrapped = `print('${sm}',end='')\n${script}\nprint('${em}',end='')`;
|
||||
executeCode(wrapped).catch(err => {
|
||||
captureState = null;
|
||||
reject(err);
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
if (captureState?.resolve === resolve) {
|
||||
captureState = null;
|
||||
reject(new Error('Timeout waiting for device response'));
|
||||
}
|
||||
}, 10000);
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Toolbar Buttons ─────────────────────────────────────
|
||||
|
||||
btnConnect.addEventListener('click', async () => {
|
||||
|
|
@ -303,72 +320,17 @@ btnSave.addEventListener('click', async () => {
|
|||
}
|
||||
});
|
||||
|
||||
// ─── Workspace Save/Load ───────────────────────────────────
|
||||
// ─── Projects Dialog ────────────────────────────────────
|
||||
|
||||
async function saveWorkspaceToDevice() {
|
||||
try {
|
||||
const state = Blockly.serialization.workspaces.save(workspace);
|
||||
const json = JSON.stringify(state);
|
||||
|
||||
appendToTerminal('\nSaving workspace to device...\n');
|
||||
await writeFileToDevice(json, 'workspace.json');
|
||||
appendToTerminal('Workspace saved to workspace.json\n');
|
||||
} catch (err) {
|
||||
appendToTerminal(`\nSave workspace error: ${err.message}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadWorkspaceFromDevice() {
|
||||
try {
|
||||
appendToTerminal('\nLoading workspace from device...\n');
|
||||
|
||||
// Use unique markers to identify workspace content
|
||||
const timestamp = Date.now();
|
||||
const startMarker = `__WS_START_${timestamp}__`;
|
||||
const endMarker = `__WS_END_${timestamp}__`;
|
||||
|
||||
workspaceCaptureState = {
|
||||
startMarker,
|
||||
endMarker,
|
||||
raw: '',
|
||||
flushedPre: false,
|
||||
};
|
||||
|
||||
const script = [
|
||||
`try:`,
|
||||
` f = open('workspace.json', 'r')`,
|
||||
` data = f.read()`,
|
||||
` f.close()`,
|
||||
` print('${startMarker}')`,
|
||||
` print(data, end='')`,
|
||||
` print('${endMarker}')`,
|
||||
`except Exception as e:`,
|
||||
` print('Error reading workspace.json: ' + str(e))`,
|
||||
].join('\n');
|
||||
|
||||
await executeCode(script);
|
||||
|
||||
// Clean up capture state after timeout
|
||||
setTimeout(() => {
|
||||
if (workspaceCaptureState) {
|
||||
appendToTerminal('\nTimeout waiting for workspace data\n');
|
||||
workspaceCaptureState = null;
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
} catch (err) {
|
||||
appendToTerminal(`\nLoad workspace error: ${err.message}\n`);
|
||||
workspaceCaptureState = null;
|
||||
}
|
||||
}
|
||||
|
||||
btnSaveWorkspace.addEventListener('click', async () => {
|
||||
await saveWorkspaceToDevice();
|
||||
initProjectsDialog({
|
||||
workspace,
|
||||
captureDeviceOutput,
|
||||
executeCode,
|
||||
writeFileToDevice,
|
||||
isConnected,
|
||||
});
|
||||
|
||||
btnLoadWorkspace.addEventListener('click', async () => {
|
||||
await loadWorkspaceFromDevice();
|
||||
});
|
||||
btnProjects.addEventListener('click', () => openProjects());
|
||||
|
||||
terminalInput.addEventListener('keydown', async (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
|
|
|
|||
179
src/style.css
179
src/style.css
|
|
@ -291,6 +291,185 @@ html, body {
|
|||
background: var(--text-muted);
|
||||
}
|
||||
|
||||
/* --- Projects Dialog --- */
|
||||
|
||||
#projects-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
#projects-modal {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 24px 28px;
|
||||
width: 680px;
|
||||
max-width: 95vw;
|
||||
max-height: 85vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.projects-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.projects-header h3 {
|
||||
margin: 0;
|
||||
color: var(--accent);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#projects-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
font-size: 22px;
|
||||
cursor: pointer;
|
||||
padding: 0 4px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
#projects-close:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.projects-columns {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.projects-col {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.projects-col h4 {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.6px;
|
||||
}
|
||||
|
||||
.projects-list {
|
||||
list-style: none;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
min-height: 180px;
|
||||
max-height: 260px;
|
||||
overflow-y: auto;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.projects-list li {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.projects-list li:hover {
|
||||
background: var(--bg-surface);
|
||||
}
|
||||
|
||||
.projects-list li.selected {
|
||||
background: var(--accent);
|
||||
color: var(--bg-toolbar);
|
||||
}
|
||||
|
||||
.projects-list .empty-msg {
|
||||
padding: 8px 12px;
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
font-style: italic;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.projects-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.projects-name-row {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.projects-name-row input {
|
||||
flex: 1;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 6px 10px;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.projects-name-row input:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.projects-btn-row {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.projects-actions button {
|
||||
background: var(--bg-surface);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 6px 14px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.projects-actions button:hover:not(:disabled) {
|
||||
background: var(--accent);
|
||||
color: var(--bg-toolbar);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.projects-actions button:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.projects-actions button.btn-danger:hover:not(:disabled) {
|
||||
background: var(--red);
|
||||
border-color: var(--red);
|
||||
}
|
||||
|
||||
.projects-note {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
min-height: 18px;
|
||||
}
|
||||
|
||||
/* --- Flash Progress Overlay --- */
|
||||
.hidden { display: none !important; }
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,242 @@
|
|||
import * as Blockly from 'blockly';
|
||||
|
||||
const BROWSER_STORAGE_KEY = 'esp32block_projects';
|
||||
|
||||
let workspace = null;
|
||||
let captureOutput = null;
|
||||
let execCode = null;
|
||||
let writeFile = null;
|
||||
let checkConnected = null;
|
||||
|
||||
// DOM refs (cached on first open)
|
||||
let overlay, browserList, deviceList;
|
||||
let browserNameInput, deviceNameInput;
|
||||
let browserSaveBtn, browserLoadBtn, browserDeleteBtn;
|
||||
let deviceSaveBtn, deviceLoadBtn, deviceDeleteBtn;
|
||||
let deviceStatus;
|
||||
|
||||
let browserSelected = null;
|
||||
let deviceSelected = null;
|
||||
|
||||
export function initProjectsDialog(deps) {
|
||||
workspace = deps.workspace;
|
||||
captureOutput = deps.captureDeviceOutput;
|
||||
execCode = deps.executeCode;
|
||||
writeFile = deps.writeFileToDevice;
|
||||
checkConnected = deps.isConnected;
|
||||
|
||||
overlay = document.getElementById('projects-overlay');
|
||||
browserList = document.getElementById('browser-list');
|
||||
deviceList = document.getElementById('device-list');
|
||||
browserNameInput = document.getElementById('browser-save-name');
|
||||
deviceNameInput = document.getElementById('device-save-name');
|
||||
browserSaveBtn = document.getElementById('browser-save-btn');
|
||||
browserLoadBtn = document.getElementById('browser-load-btn');
|
||||
browserDeleteBtn = document.getElementById('browser-delete-btn');
|
||||
deviceSaveBtn = document.getElementById('device-save-btn');
|
||||
deviceLoadBtn = document.getElementById('device-load-btn');
|
||||
deviceDeleteBtn = document.getElementById('device-delete-btn');
|
||||
deviceStatus = document.getElementById('device-status');
|
||||
|
||||
document.getElementById('projects-close').addEventListener('click', close);
|
||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
|
||||
|
||||
browserSaveBtn.addEventListener('click', saveBrowser);
|
||||
browserLoadBtn.addEventListener('click', loadBrowser);
|
||||
browserDeleteBtn.addEventListener('click', deleteBrowser);
|
||||
deviceSaveBtn.addEventListener('click', saveDevice);
|
||||
deviceLoadBtn.addEventListener('click', loadDevice);
|
||||
deviceDeleteBtn.addEventListener('click', deleteDevice);
|
||||
}
|
||||
|
||||
export function open() {
|
||||
browserSelected = null;
|
||||
deviceSelected = null;
|
||||
overlay.classList.remove('hidden');
|
||||
refreshBrowserList();
|
||||
refreshDeviceList();
|
||||
}
|
||||
|
||||
function close() {
|
||||
overlay.classList.add('hidden');
|
||||
}
|
||||
|
||||
// ─── Browser column ──────────────────────────────────────
|
||||
|
||||
function getBrowserProjects() {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(BROWSER_STORAGE_KEY) || '{}');
|
||||
} catch { return {}; }
|
||||
}
|
||||
|
||||
function setBrowserProjects(projects) {
|
||||
localStorage.setItem(BROWSER_STORAGE_KEY, JSON.stringify(projects));
|
||||
}
|
||||
|
||||
function refreshBrowserList() {
|
||||
const projects = getBrowserProjects();
|
||||
const names = Object.keys(projects).sort();
|
||||
browserList.innerHTML = '';
|
||||
browserSelected = null;
|
||||
updateBrowserButtons();
|
||||
|
||||
if (names.length === 0) {
|
||||
browserList.innerHTML = '<li class="empty-msg">No saved projects</li>';
|
||||
return;
|
||||
}
|
||||
|
||||
for (const name of names) {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = name;
|
||||
li.addEventListener('click', () => selectBrowserItem(name, li));
|
||||
li.addEventListener('dblclick', () => { selectBrowserItem(name, li); loadBrowser(); });
|
||||
browserList.appendChild(li);
|
||||
}
|
||||
}
|
||||
|
||||
function selectBrowserItem(name, li) {
|
||||
browserList.querySelectorAll('li').forEach(el => el.classList.remove('selected'));
|
||||
li.classList.add('selected');
|
||||
browserSelected = name;
|
||||
browserNameInput.value = name;
|
||||
updateBrowserButtons();
|
||||
}
|
||||
|
||||
function updateBrowserButtons() {
|
||||
browserLoadBtn.disabled = !browserSelected;
|
||||
browserDeleteBtn.disabled = !browserSelected;
|
||||
}
|
||||
|
||||
function saveBrowser() {
|
||||
const name = browserNameInput.value.trim();
|
||||
if (!name) return;
|
||||
const projects = getBrowserProjects();
|
||||
projects[name] = Blockly.serialization.workspaces.save(workspace);
|
||||
setBrowserProjects(projects);
|
||||
browserNameInput.value = '';
|
||||
refreshBrowserList();
|
||||
}
|
||||
|
||||
function loadBrowser() {
|
||||
if (!browserSelected) return;
|
||||
const projects = getBrowserProjects();
|
||||
const state = projects[browserSelected];
|
||||
if (!state) return;
|
||||
Blockly.serialization.workspaces.load(state, workspace);
|
||||
close();
|
||||
}
|
||||
|
||||
function deleteBrowser() {
|
||||
if (!browserSelected) return;
|
||||
const projects = getBrowserProjects();
|
||||
delete projects[browserSelected];
|
||||
setBrowserProjects(projects);
|
||||
refreshBrowserList();
|
||||
}
|
||||
|
||||
// ─── Device column ───────────────────────────────────────
|
||||
|
||||
async function refreshDeviceList() {
|
||||
deviceList.innerHTML = '';
|
||||
deviceSelected = null;
|
||||
updateDeviceButtons();
|
||||
|
||||
if (!checkConnected()) {
|
||||
deviceStatus.textContent = 'Connect a device to see its projects';
|
||||
deviceSaveBtn.disabled = true;
|
||||
deviceList.innerHTML = '<li class="empty-msg">Not connected</li>';
|
||||
return;
|
||||
}
|
||||
|
||||
deviceStatus.textContent = 'Loading...';
|
||||
deviceSaveBtn.disabled = false;
|
||||
|
||||
try {
|
||||
const raw = await captureOutput(
|
||||
"import os\n" +
|
||||
"for f in os.listdir('/'):\n" +
|
||||
" if f.endswith('.blk'): print(f)"
|
||||
);
|
||||
const files = raw.trim().split('\n').filter(Boolean).map(f => f.trim());
|
||||
deviceList.innerHTML = '';
|
||||
|
||||
if (files.length === 0) {
|
||||
deviceList.innerHTML = '<li class="empty-msg">No saved projects</li>';
|
||||
deviceStatus.textContent = '';
|
||||
return;
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const displayName = file.replace(/\.blk$/, '');
|
||||
const li = document.createElement('li');
|
||||
li.textContent = displayName;
|
||||
li.addEventListener('click', () => selectDeviceItem(displayName, file, li));
|
||||
li.addEventListener('dblclick', () => { selectDeviceItem(displayName, file, li); loadDevice(); });
|
||||
deviceList.appendChild(li);
|
||||
}
|
||||
deviceStatus.textContent = '';
|
||||
} catch {
|
||||
deviceList.innerHTML = '<li class="empty-msg">Error reading device</li>';
|
||||
deviceStatus.textContent = 'Could not list files';
|
||||
}
|
||||
}
|
||||
|
||||
function selectDeviceItem(displayName, filename, li) {
|
||||
deviceList.querySelectorAll('li').forEach(el => el.classList.remove('selected'));
|
||||
li.classList.add('selected');
|
||||
deviceSelected = filename;
|
||||
deviceNameInput.value = displayName;
|
||||
updateDeviceButtons();
|
||||
}
|
||||
|
||||
function updateDeviceButtons() {
|
||||
const connected = checkConnected();
|
||||
deviceSaveBtn.disabled = !connected;
|
||||
deviceLoadBtn.disabled = !deviceSelected || !connected;
|
||||
deviceDeleteBtn.disabled = !deviceSelected || !connected;
|
||||
}
|
||||
|
||||
async function saveDevice() {
|
||||
const name = deviceNameInput.value.trim();
|
||||
if (!name || !checkConnected()) return;
|
||||
const filename = name.endsWith('.blk') ? name : name + '.blk';
|
||||
const state = Blockly.serialization.workspaces.save(workspace);
|
||||
const json = JSON.stringify(state);
|
||||
|
||||
deviceStatus.textContent = 'Saving...';
|
||||
deviceSaveBtn.disabled = true;
|
||||
try {
|
||||
await writeFile(json, filename);
|
||||
deviceNameInput.value = '';
|
||||
await refreshDeviceList();
|
||||
} catch {
|
||||
deviceStatus.textContent = 'Save failed';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDevice() {
|
||||
if (!deviceSelected || !checkConnected()) return;
|
||||
deviceStatus.textContent = 'Loading...';
|
||||
try {
|
||||
const raw = await captureOutput(
|
||||
`f=open('${deviceSelected}','r')\nprint(f.read(),end='')\nf.close()`
|
||||
);
|
||||
const state = JSON.parse(raw.trim());
|
||||
Blockly.serialization.workspaces.load(state, workspace);
|
||||
close();
|
||||
} catch {
|
||||
deviceStatus.textContent = 'Load failed';
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteDevice() {
|
||||
if (!deviceSelected || !checkConnected()) return;
|
||||
deviceStatus.textContent = 'Deleting...';
|
||||
try {
|
||||
await execCode(`import os\nos.remove('${deviceSelected}')`);
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
await refreshDeviceList();
|
||||
} catch {
|
||||
deviceStatus.textContent = 'Delete failed';
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue