Initial commit: pwweb - PipeWire WebUI

C++ backend with SSE streaming (reuses qpwgraph PipeWire callbacks).
Svelte frontend with custom SVG canvas for port-level connections.

Features:
- Live PipeWire graph via SSE
- Drag output->input port to connect
- Double-click or select+Delete to disconnect
- Node positions saved to localStorage
- Pan (drag bg) and zoom (scroll)
- Port type coloring (audio=green, midi=red, video=blue)
This commit is contained in:
joren
2026-03-29 22:40:07 +02:00
commit f8c57fbdd3
34 changed files with 24479 additions and 0 deletions

View File

@@ -0,0 +1,64 @@
import { writable, derived, get } from 'svelte/store';
import type { Node, Port, Link, GraphMessage } from './types';
import { subscribe, connectPorts, disconnectPorts } from './ws';
// Raw graph stores
export const nodes = writable<Node[]>([]);
export const ports = writable<Port[]>([]);
export const links = writable<Link[]>([]);
export const connected = writable(false);
// Port lookup map
export const portById = derived(ports, ($ports) => {
const map = new Map<number, Port>();
for (const p of $ports) map.set(p.id, p);
return map;
});
// Node lookup map
export const nodeById = derived(nodes, ($nodes) => {
const map = new Map<number, Node>();
for (const n of $nodes) map.set(n.id, n);
return map;
});
// Initialize: fetch via REST immediately, then subscribe to WS for updates
let unsubscribe: (() => void) | null = null;
function applyGraph(graph: GraphMessage) {
nodes.set(graph.nodes);
ports.set(graph.ports);
links.set(graph.links);
connected.set(true);
}
export async function initGraph() {
if (unsubscribe) return;
// 1. Fetch initial state via REST API (works immediately, no WS needed)
try {
const res = await fetch('/api/graph');
if (res.ok) {
const graph: GraphMessage = await res.json();
applyGraph(graph);
}
} catch (e) {
console.warn('[graph] REST fetch failed:', e);
}
// 2. Subscribe to WebSocket for live updates
unsubscribe = subscribe((graph: GraphMessage) => {
applyGraph(graph);
});
}
export function destroyGraph() {
if (unsubscribe) {
unsubscribe();
unsubscribe = null;
connected.set(false);
}
}
// Re-export connection functions
export { connectPorts, disconnectPorts };