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>
|
<button id="btn-save" title="Save code to device as main.py" disabled>
|
||||||
<span class="icon">💾</span> Save
|
<span class="icon">💾</span> Save
|
||||||
</button>
|
</button>
|
||||||
<button id="btn-save-workspace" title="Save Blockly workspace to device" disabled>
|
<button id="btn-projects" title="Open/save projects (browser or device)">
|
||||||
<span class="icon">💾</span> Save WS
|
<span class="icon">📂</span> Projects
|
||||||
</button>
|
|
||||||
<button id="btn-load-workspace" title="Load Blockly workspace from device" disabled>
|
|
||||||
<span class="icon">📂</span> Load WS
|
|
||||||
</button>
|
</button>
|
||||||
<span id="connection-status" class="status-disconnected">Disconnected</span>
|
<span id="connection-status" class="status-disconnected">Disconnected</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -68,6 +65,47 @@
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</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 -->
|
<!-- Flash progress overlay -->
|
||||||
<div id="flash-overlay" class="hidden">
|
<div id="flash-overlay" class="hidden">
|
||||||
<div id="flash-modal">
|
<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 { flashFirmware } from './serial/flasher.js';
|
||||||
import { appendToTerminal, clearTerminal } from './ui/terminal.js';
|
import { appendToTerminal, clearTerminal } from './ui/terminal.js';
|
||||||
import { initResizablePanels } from './ui/panels.js';
|
import { initResizablePanels } from './ui/panels.js';
|
||||||
|
import { initProjectsDialog, open as openProjects } from './ui/projectsDialog.js';
|
||||||
import './style.css';
|
import './style.css';
|
||||||
|
|
||||||
// ─── Blockly Workspace ───────────────────────────────────
|
// ─── Blockly Workspace ───────────────────────────────────
|
||||||
|
|
@ -93,8 +94,7 @@ const btnFlash = document.getElementById('btn-flash');
|
||||||
const btnRun = document.getElementById('btn-run');
|
const btnRun = document.getElementById('btn-run');
|
||||||
const btnStop = document.getElementById('btn-stop');
|
const btnStop = document.getElementById('btn-stop');
|
||||||
const btnSave = document.getElementById('btn-save');
|
const btnSave = document.getElementById('btn-save');
|
||||||
const btnSaveWorkspace = document.getElementById('btn-save-workspace');
|
const btnProjects = document.getElementById('btn-projects');
|
||||||
const btnLoadWorkspace = document.getElementById('btn-load-workspace');
|
|
||||||
const statusEl = document.getElementById('connection-status');
|
const statusEl = document.getElementById('connection-status');
|
||||||
const terminalInput = document.getElementById('terminal-input');
|
const terminalInput = document.getElementById('terminal-input');
|
||||||
|
|
||||||
|
|
@ -118,36 +118,32 @@ function setConnectedUI(connected) {
|
||||||
btnRun.disabled = !connected;
|
btnRun.disabled = !connected;
|
||||||
btnStop.disabled = !connected;
|
btnStop.disabled = !connected;
|
||||||
btnSave.disabled = !connected;
|
btnSave.disabled = !connected;
|
||||||
btnSaveWorkspace.disabled = !connected;
|
|
||||||
btnLoadWorkspace.disabled = !connected;
|
|
||||||
terminalInput.disabled = !connected;
|
terminalInput.disabled = !connected;
|
||||||
statusEl.textContent = connected ? 'Connected' : 'Disconnected';
|
statusEl.textContent = connected ? 'Connected' : 'Disconnected';
|
||||||
statusEl.className = connected ? 'status-connected' : 'status-disconnected';
|
statusEl.className = connected ? 'status-connected' : 'status-disconnected';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Serial Event Listeners ──────────────────────────────
|
// ─── Serial Capture (reusable promise-based) ─────────────
|
||||||
|
|
||||||
// Workspace loading state
|
let captureState = null;
|
||||||
let workspaceCaptureState = null;
|
|
||||||
|
|
||||||
onData((text) => {
|
onData((text) => {
|
||||||
if (!workspaceCaptureState) {
|
if (!captureState) {
|
||||||
appendToTerminal(text);
|
appendToTerminal(text);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { startMarker, endMarker } = workspaceCaptureState;
|
const { startMarker, endMarker } = captureState;
|
||||||
workspaceCaptureState.raw += text;
|
captureState.raw += text;
|
||||||
const raw = workspaceCaptureState.raw;
|
const raw = captureState.raw;
|
||||||
|
|
||||||
const startIdx = raw.indexOf(startMarker);
|
const startIdx = raw.indexOf(startMarker);
|
||||||
|
|
||||||
if (startIdx === -1) {
|
if (startIdx === -1) {
|
||||||
// No start marker yet — flush text that can't be part of the marker
|
|
||||||
const keep = startMarker.length - 1;
|
const keep = startMarker.length - 1;
|
||||||
if (raw.length > keep) {
|
if (raw.length > keep) {
|
||||||
appendToTerminal(raw.substring(0, 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -156,35 +152,56 @@ onData((text) => {
|
||||||
const endIdx = raw.indexOf(endMarker, contentStart);
|
const endIdx = raw.indexOf(endMarker, contentStart);
|
||||||
|
|
||||||
if (endIdx === -1) {
|
if (endIdx === -1) {
|
||||||
// Have start but no end yet — show text before start marker once
|
if (!captureState.flushedPre && startIdx > 0) {
|
||||||
if (!workspaceCaptureState.flushedPre && startIdx > 0) {
|
|
||||||
appendToTerminal(raw.substring(0, startIdx));
|
appendToTerminal(raw.substring(0, startIdx));
|
||||||
workspaceCaptureState.flushedPre = true;
|
captureState.flushedPre = true;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Both markers found — extract content and load
|
const content = raw.substring(contentStart, endIdx);
|
||||||
const jsonContent = raw.substring(contentStart, endIdx).trim();
|
const beforeStart = startIdx > 0 && !captureState.flushedPre
|
||||||
const beforeStart = startIdx > 0 && !workspaceCaptureState.flushedPre
|
|
||||||
? raw.substring(0, startIdx) : '';
|
? raw.substring(0, startIdx) : '';
|
||||||
const afterEnd = raw.substring(endIdx + endMarker.length);
|
const afterEnd = raw.substring(endIdx + endMarker.length);
|
||||||
|
const resolve = captureState.resolve;
|
||||||
|
|
||||||
workspaceCaptureState = null;
|
captureState = null;
|
||||||
|
|
||||||
if (beforeStart) appendToTerminal(beforeStart);
|
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);
|
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 ─────────────────────────────────────
|
// ─── Toolbar Buttons ─────────────────────────────────────
|
||||||
|
|
||||||
btnConnect.addEventListener('click', async () => {
|
btnConnect.addEventListener('click', async () => {
|
||||||
|
|
@ -303,72 +320,17 @@ btnSave.addEventListener('click', async () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── Workspace Save/Load ───────────────────────────────────
|
// ─── Projects Dialog ────────────────────────────────────
|
||||||
|
|
||||||
async function saveWorkspaceToDevice() {
|
initProjectsDialog({
|
||||||
try {
|
workspace,
|
||||||
const state = Blockly.serialization.workspaces.save(workspace);
|
captureDeviceOutput,
|
||||||
const json = JSON.stringify(state);
|
executeCode,
|
||||||
|
writeFileToDevice,
|
||||||
appendToTerminal('\nSaving workspace to device...\n');
|
isConnected,
|
||||||
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();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
btnLoadWorkspace.addEventListener('click', async () => {
|
btnProjects.addEventListener('click', () => openProjects());
|
||||||
await loadWorkspaceFromDevice();
|
|
||||||
});
|
|
||||||
|
|
||||||
terminalInput.addEventListener('keydown', async (e) => {
|
terminalInput.addEventListener('keydown', async (e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
|
|
|
||||||
179
src/style.css
179
src/style.css
|
|
@ -291,6 +291,185 @@ html, body {
|
||||||
background: var(--text-muted);
|
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 --- */
|
/* --- Flash Progress Overlay --- */
|
||||||
.hidden { display: none !important; }
|
.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