import * as Blockly from 'blockly'; import { getToken, onAuthChange } from '../auth/client.js'; const BROWSER_STORAGE_KEY = 'esp32block_projects'; let workspace = null; let captureOutput = null; let writeFile = null; let checkConnected = null; let browserList; let browserNameInput; let browserSaveBtn, browserLoadBtn, browserMoveCloudBtn, browserDownloadBtn, browserDeleteBtn; let browserUploadBtn, browserUploadInput; let browserSelected = null; let selectedEntry = null; let cloudProjects = []; export function initProjectsDialog(deps) { workspace = deps.workspace; captureOutput = deps.captureDeviceOutput; writeFile = deps.writeFileToDevice; checkConnected = deps.isConnected; browserList = document.getElementById('browser-list'); browserNameInput = document.getElementById('browser-save-name'); browserSaveBtn = document.getElementById('browser-save-btn'); browserLoadBtn = document.getElementById('browser-load-btn'); browserMoveCloudBtn = document.getElementById('browser-move-cloud-btn'); browserDownloadBtn = document.getElementById('browser-download-btn'); browserDeleteBtn = document.getElementById('browser-delete-btn'); browserUploadBtn = document.getElementById('browser-upload-btn'); browserUploadInput = document.getElementById('browser-upload-input'); browserSaveBtn.addEventListener('click', saveBrowser); browserLoadBtn.addEventListener('click', loadBrowser); browserMoveCloudBtn.addEventListener('click', moveBrowserToCloud); browserDownloadBtn.addEventListener('click', downloadBrowser); browserDeleteBtn.addEventListener('click', deleteBrowser); browserUploadBtn.addEventListener('click', openUploadPicker); browserUploadInput.addEventListener('change', uploadFromComputer); onAuthChange(() => { refreshBrowserList().catch(() => { /* ignore */ }); }); refreshBrowserList().catch(() => { /* ignore */ }); } export function refreshAll() { refreshBrowserList().catch(() => { /* ignore */ }); } export async function saveCurrentWorkspaceToDevice(preferredName = 'main') { if (!workspace || !writeFile || !checkConnected || !checkConnected()) return null; const filename = preferredName.endsWith('.blk') ? preferredName : preferredName + '.blk'; const state = Blockly.serialization.workspaces.save(workspace); const json = JSON.stringify(state); await writeFile(json, filename); return filename; } export async function loadWorkspaceFromDevice(preferredName = 'main') { if (!workspace || !captureOutput || !checkConnected || !checkConnected()) return null; const filename = preferredName.endsWith('.blk') ? preferredName : preferredName + '.blk'; const raw = await captureOutput( `f=open('${filename}','r')\nprint(f.read(),end='')\nf.close()` ); const state = JSON.parse(raw.trim()); Blockly.serialization.workspaces.load(state, workspace); return filename; } // ─── 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)); } async function fetchCloudProjects() { const token = getToken(); if (!token) return []; const res = await fetch(apiUrl('/api/projects'), { headers: { Authorization: `Bearer ${token}` }, }); if (!res.ok) throw new Error(`Failed to fetch cloud projects (HTTP ${res.status})`); const data = await res.json(); return Array.isArray(data?.projects) ? data.projects : []; } function apiUrl(path) { const base = import.meta.env.BASE_URL || '/'; const p = path.startsWith('/') ? path.slice(1) : path; return (base.endsWith('/') ? base : base + '/') + p; } function displayName(entry) { const tag = entry.source === 'cloud' ? '[cloud]' : '[browser]'; return `${entry.name} ${tag}`; } async function refreshBrowserList() { const loggedIn = !!getToken(); const projects = getBrowserProjects(); const names = Object.keys(projects).sort((a, b) => a.localeCompare(b)); try { cloudProjects = await fetchCloudProjects(); } catch { cloudProjects = []; } browserList.innerHTML = ''; browserSelected = null; // legacy selection key selectedEntry = null; updateBrowserButtons(); const entries = loggedIn ? [ ...names.map((name) => ({ source: 'browser', name })), ...cloudProjects.map((p) => ({ source: 'cloud', name: p.name })), ] : names.map((name) => ({ source: 'browser', name })); entries.sort((a, b) => { const byName = a.name.localeCompare(b.name); if (byName !== 0) return byName; return a.source.localeCompare(b.source); }); if (entries.length === 0) { browserList.innerHTML = '
  • No saved projects
  • '; return; } for (const entry of entries) { const li = document.createElement('li'); li.textContent = displayName(entry); li.addEventListener('click', () => selectBrowserItem(entry, li)); li.addEventListener('dblclick', () => { selectBrowserItem(entry, li); loadBrowser(); }); browserList.appendChild(li); } } function selectBrowserItem(entry, li) { browserList.querySelectorAll('li').forEach(el => el.classList.remove('selected')); li.classList.add('selected'); selectedEntry = entry; browserSelected = entry.name; browserNameInput.value = entry.name; updateBrowserButtons(); } function updateBrowserButtons() { const hasSelection = !!selectedEntry; const canMoveToCloud = !!selectedEntry && selectedEntry.source === 'browser' && !!getToken(); browserLoadBtn.disabled = !hasSelection; browserMoveCloudBtn.disabled = !canMoveToCloud; browserDownloadBtn.disabled = !hasSelection; browserDeleteBtn.disabled = !hasSelection; } async function saveCloud(name, state) { const token = getToken(); if (!token) return false; const res = await fetch(apiUrl('/api/projects'), { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, body: JSON.stringify({ name, state }), }); return res.ok; } async function saveBrowser() { const name = browserNameInput.value.trim(); if (!name) return; const state = Blockly.serialization.workspaces.save(workspace); if (getToken()) { await saveCloud(name, state).catch(() => { /* ignore */ }); } else { const projects = getBrowserProjects(); projects[name] = state; setBrowserProjects(projects); } browserNameInput.value = ''; await refreshBrowserList(); } async function loadBrowser() { if (!selectedEntry) return; if (selectedEntry.source === 'cloud') { const cloud = cloudProjects.find((p) => p.name === selectedEntry.name); if (!cloud?.state) return; Blockly.serialization.workspaces.load(cloud.state, workspace); return; } const projects = getBrowserProjects(); const state = projects[selectedEntry.name]; if (!state) return; Blockly.serialization.workspaces.load(state, workspace); } async function deleteCloud(name) { const token = getToken(); if (!token) return false; const res = await fetch(apiUrl(`/api/projects/${encodeURIComponent(name)}`), { method: 'DELETE', headers: { Authorization: `Bearer ${token}` }, }); return res.ok; } async function deleteBrowser() { if (!selectedEntry) return; if (selectedEntry.source === 'cloud') { await deleteCloud(selectedEntry.name).catch(() => { /* ignore */ }); } else { const projects = getBrowserProjects(); delete projects[selectedEntry.name]; setBrowserProjects(projects); } await refreshBrowserList(); } async function moveBrowserToCloud() { if (!selectedEntry || selectedEntry.source !== 'browser' || !getToken()) return; const projects = getBrowserProjects(); const state = projects[selectedEntry.name]; if (!state) return; const ok = await saveCloud(selectedEntry.name, state).catch(() => false); if (!ok) return; delete projects[selectedEntry.name]; setBrowserProjects(projects); await refreshBrowserList(); } function downloadBrowser() { if (!selectedEntry) return; const state = selectedEntry.source === 'cloud' ? cloudProjects.find((p) => p.name === selectedEntry.name)?.state : getBrowserProjects()[selectedEntry.name]; if (!state) return; const json = JSON.stringify(state, null, 2); const url = `data:application/json;charset=utf-8,${encodeURIComponent(json)}`; const a = document.createElement('a'); a.href = url; a.download = `${selectedEntry.name}.blk`; document.body.appendChild(a); a.click(); document.body.removeChild(a); } function openUploadPicker() { if (!browserUploadInput) return; browserUploadInput.click(); } async function uploadFromComputer(event) { const file = event?.target?.files?.[0]; if (!file) return; try { const raw = await file.text(); const parsed = JSON.parse(raw); if (!parsed || typeof parsed !== 'object') return; Blockly.serialization.workspaces.load(parsed, workspace); } catch { // ignore invalid file contents } finally { browserUploadInput.value = ''; } }