280 lines
9.0 KiB
JavaScript
280 lines
9.0 KiB
JavaScript
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 = '<li class="empty-msg">No saved projects</li>';
|
|
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 = '';
|
|
}
|
|
}
|