fix: toolbar buttons clickable, add node delete

Fixes:
- SVG mousedown handler now ignores clicks outside SVG (toolbar works)
- Right-click node -> Destroy Node to delete virtual devices
- POST /api/destroy-node {node_id} endpoint (uses pw-cli destroy)

Node context menu shows node name and destroy button.
This commit is contained in:
joren
2026-03-30 00:02:53 +02:00
parent 9dc685acda
commit 444fc43c9c
2 changed files with 90 additions and 1 deletions

View File

@@ -26,6 +26,7 @@
let hoveredPort = $state<number | null>(null);
let selectedEdge = $state<string | null>(null);
let contextMenu = $state<{ x: number; y: number; linkId: number; outputPortId: number; inputPortId: number; pinned: boolean } | null>(null);
let nodeContextMenu = $state<{ x: number; y: number; nodeId: number; nodeName: string } | null>(null);
// Filters
let showAudio = $state(true);
@@ -234,6 +235,9 @@
// Mouse handlers
function onMouseDown(e: MouseEvent) {
// Ignore clicks outside the SVG (toolbar, dialogs, etc.)
if (svgEl && !svgEl.contains(e.target as Node)) return;
contextMenu = null;
if (e.button === 2) return;
const pt = svgPoint(e);
@@ -352,6 +356,8 @@
function onContextMenu(e: MouseEvent) {
e.preventDefault();
const target = e.target as HTMLElement;
// Right-click on edge
if (target.classList.contains('edge-path')) {
const edgeId = target.dataset.edgeId;
const link = $links.find(l => String(l.id) === edgeId);
@@ -365,7 +371,40 @@
pinned: $patchbay.pinned_connections.includes(link.id),
};
}
return;
}
// Right-click on node
const nodeGroup = target.closest('.node-group') as HTMLElement;
if (nodeGroup) {
const nodeId = Number(nodeGroup.dataset.nodeId);
const nd = $nodes.find(n => n.id === nodeId);
if (nd) {
nodeContextMenu = { x: e.clientX, y: e.clientY, nodeId, nodeName: nd.name };
}
}
}
function unloadNodeModule() {
if (!nodeContextMenu) return;
// Try to find and unload the module that created this node
// Use pactl to unload by looking up the module
fetch('/api/load-module', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ module: 'module-null-sink', args: '' }),
}).catch(() => {});
// Actually, just use the destroy approach via pactl
// We need to find the module ID. Let's use a different endpoint.
// For now, use pw-cli to destroy the node
const name = nodeContextMenu.nodeName;
fetch('/api/destroy-node', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ node_id: nodeContextMenu.nodeId }),
}).catch(() => {});
nodeContextMenu = null;
}
function onKey(e: KeyboardEvent) {
@@ -379,7 +418,7 @@
onDestroy(() => { destroyGraph(); });
</script>
<svelte:window onkeydown={onKey} onclick={() => { contextMenu = null; showVirtualMenu = false; }} />
<svelte:window onkeydown={onKey} onclick={() => { contextMenu = null; nodeContextMenu = null; showVirtualMenu = false; }} />
<div class="wrap">
<div class="toolbar">
@@ -584,6 +623,20 @@
</div>
{/if}
{#if nodeContextMenu}
<div class="ctx" style="left:{nodeContextMenu.x}px;top:{nodeContextMenu.y}px" role="menu">
<div class="ctx-title">{nodeContextMenu.nodeName}</div>
<button onclick={() => {
fetch('/api/destroy-node', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ node_id: nodeContextMenu!.nodeId }),
}).catch(() => {});
nodeContextMenu = null;
}}>Destroy Node</button>
</div>
{/if}
<!-- Hide Nodes Dialog -->
{#if showHideDialog}
<div class="dialog">
@@ -749,6 +802,11 @@
font-size: 12px; cursor: pointer; text-align: left; font-family: monospace;
}
.ctx button:hover { background: #444; }
.ctx-title {
padding: 4px 16px; font-size: 9px; color: #666;
border-bottom: 1px solid #444; font-family: monospace;
max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Dialogs */
.dialog {