This commit is contained in:
Martino Ferrari
2026-04-25 22:56:09 +02:00
parent 8b548ba1c2
commit 986f6cd6d8
85 changed files with 11479 additions and 5050 deletions
+177
View File
@@ -0,0 +1,177 @@
import { h } from 'preact';
import { useState, useCallback } from 'preact/hooks';
import type { Interface, Widget } from './lib/types';
import { serializeInterface, parseInterface } from './lib/xml';
import SignalTree from './SignalTree';
import EditCanvas from './EditCanvas';
import PropertiesPane from './PropertiesPane';
interface Props {
initial: Interface | null;
onDone: () => void;
}
let _newId = Date.now();
function newIfaceId() { return `iface-${_newId++}`; }
function blankInterface(): Interface {
return {
id: newIfaceId(),
name: 'New Interface',
version: 1,
w: 1200,
h: 800,
widgets: [],
};
}
export default function EditMode({ initial, onDone }: Props) {
const [iface, setIface] = useState<Interface>(initial ?? blankInterface());
const [selectedId, setSelectedId] = useState<string | null>(null);
const [dirty, setDirty] = useState(false);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const selectedWidget = iface.widgets.find(w => w.id === selectedId) ?? null;
const handleWidgetsChange = useCallback((widgets: Widget[]) => {
setIface(f => ({ ...f, widgets }));
setDirty(true);
}, []);
const handleWidgetChange = useCallback((updated: Widget) => {
setIface(f => ({ ...f, widgets: f.widgets.map(w => w.id === updated.id ? updated : w) }));
setDirty(true);
}, []);
const handleIfaceChange = useCallback((updated: Interface) => {
setIface(updated);
setDirty(true);
}, []);
async function handleSave() {
setSaving(true);
setError(null);
try {
const xml = serializeInterface(iface);
const url = iface.id
? `/api/v1/interfaces/${encodeURIComponent(iface.id)}`
: '/api/v1/interfaces';
const method = iface.id ? 'PUT' : 'POST';
const res = await fetch(url, {
method,
headers: { 'Content-Type': 'application/xml' },
body: xml,
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Save failed (${res.status}): ${text}`);
}
if (method === 'POST') {
const json = await res.json();
setIface(f => ({ ...f, id: json.id }));
}
setDirty(false);
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
} finally {
setSaving(false);
}
}
function handleExport() {
const xml = serializeInterface(iface);
const blob = new Blob([xml], { type: 'application/xml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${iface.name.replace(/[^a-z0-9]/gi, '_')}.xml`;
a.click();
URL.revokeObjectURL(url);
}
function handleImport() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.xml,application/xml,text/xml';
input.onchange = async () => {
const file = input.files?.[0];
if (!file) return;
try {
const xml = await file.text();
const parsed = parseInterface(xml);
setIface(parsed);
setSelectedId(null);
setDirty(true);
} catch (err) {
setError(`Import failed: ${err instanceof Error ? err.message : err}`);
}
};
input.click();
}
function handleLeave() {
if (dirty && !confirm('You have unsaved changes. Leave without saving?')) return;
onDone();
}
function handleDeleteSelected() {
if (!selectedId) return;
setIface(f => ({ ...f, widgets: f.widgets.filter(w => w.id !== selectedId) }));
setSelectedId(null);
setDirty(true);
}
function handleKeyDown(e: KeyboardEvent) {
if ((e.key === 'Delete' || e.key === 'Backspace') && selectedId &&
!(e.target instanceof HTMLInputElement) && !(e.target instanceof HTMLTextAreaElement)) {
handleDeleteSelected();
}
}
return (
<div class="edit-mode-layout" onKeyDown={handleKeyDown} tabIndex={-1}>
{/* Toolbar */}
<header class="toolbar">
<div class="toolbar-left">
<span class="app-name">uopi</span>
<span class="edit-badge">Edit</span>
<input
class="iface-name-input"
value={iface.name}
onInput={(e) => handleIfaceChange({ ...iface, name: (e.target as HTMLInputElement).value })}
/>
{dirty && <span class="dirty-dot" title="Unsaved changes"></span>}
</div>
<div class="toolbar-center">
{error && <span class="parse-error" title={error}>Save error check console</span>}
</div>
<div class="toolbar-right">
<button class="toolbar-btn" onClick={handleImport} title="Import XML file">Import</button>
<button class="toolbar-btn" onClick={handleExport} title="Export XML file">Export</button>
<button class="toolbar-btn toolbar-btn-primary" onClick={handleSave} disabled={saving}>
{saving ? 'Saving…' : 'Save'}
</button>
<button class="toolbar-btn" onClick={handleLeave} title="Back to view mode"> Close</button>
</div>
</header>
{/* Three-panel body */}
<div class="edit-body">
<SignalTree />
<EditCanvas
iface={iface}
selectedId={selectedId}
onSelect={setSelectedId}
onChange={handleWidgetsChange}
/>
<PropertiesPane
selected={selectedWidget}
iface={iface}
onChange={handleWidgetChange}
onIfaceChange={handleIfaceChange}
/>
</div>
</div>
);
}