243 lines
7.5 KiB
JavaScript
243 lines
7.5 KiB
JavaScript
|
|
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';
|
||
|
|
}
|
||
|
|
}
|