{ contextMenu = null; nodeContextMenu = null; }} />
{$connected ? 'Connected' : 'Disconnected'} {$nodes.length}N {$ports.length}P {$links.length}L {#if $patchbay.pinned_connections.length > 0}{$patchbay.pinned_connections.length}p{/if}
{#if showVirtualMenu}
Add Virtual Device
{/if} {#if showNetworkDialog}
{showNetworkDialog.type === 'tunnel-sink' ? 'TCP Tunnel Sink' : 'TCP Tunnel Source'}
Host:
Port:
{/if} {#each graphLinks as link (link.id)} {@const outPos = getPortPos(link.output_port_id)} {@const inPos = getPortPos(link.input_port_id)} {#if outPos && inPos} {@const color = portColor(link.outPort.port_type)} {@const isSelected = selectedEdge === String(link.id)} {#if link.pinned} {/if} {/if} {/each} {#if connecting} {/if} {#each graphNodes as nd (nd.id)} {@const isSource = nd.mode === 'output'} {@const isSink = nd.mode === 'input'} {@const isDuplex = nd.mode === 'duplex'} {@const bg = isSource ? '#1a2a1a' : isSink ? '#2a1a1a' : isDuplex ? '#1a1a2a' : '#1e1e2e'} {@const border = isSource ? '#4a9' : isSink ? '#a44' : isDuplex ? '#49a' : '#555'} {@const headerBg = isSource ? '#263826' : isSink ? '#382626' : isDuplex ? '#262638' : '#262638'} {nd.nick || nd.name} [{nd.node_type}] {#each nd.inPorts as port, i (port.id)} {@const py = nd.y + 22 + i * 16 + 8} {#if isPortVisible(port.port_type)} { hoveredPort = port.id; }} onmouseleave={() => { hoveredPort = null; }} /> {shortName(port.name)} {:else} {/if} {/each} {#each nd.outPorts as port, i (port.id)} {@const py = nd.y + 22 + i * 16 + 8} {#if isPortVisible(port.port_type)} {shortName(port.name)} { hoveredPort = port.id; }} onmouseleave={() => { hoveredPort = null; }} /> {:else} {/if} {/each} {nd.mute ? 'M' : 'm'} {Math.round(Math.max(0, Math.min(1, nd.volume)) * 100)}% {/each} {#if contextMenu} {/if} {#if nodeContextMenu} {/if} {#if showPropsDialog !== null} {@const nd = $nodes.find(n => n.id === showPropsDialog)} {#if nd}
Properties: {nd.nick || nd.name}
{#if nd.nick}{/if} {#if nd.media_name}{/if} {#if nd.node_type === 'audio' || nd.channels > 0} {#if nd.quantum > 0}{/if} {/if} {#if nd.format}{/if} {#if nd.rate > 0}{/if} {#if nd.device_name}{/if} {#if nd.device_bus}{/if} {#if nd.api}{/if} {#if nd.priority > 0}{/if}
ID{nd.id}
Name{nd.name}
Nick{nd.nick}
Media Name{nd.media_name}
Class{nd.mode} / {nd.node_type}
Volume{Math.round(nd.volume * 100)}% {nd.mute ? '(muted)' : ''}
Ports{nd.port_ids.length}
Channels{nd.channels > 0 ? nd.channels : '-'}
Sample Rate{nd.sample_rate > 0 ? nd.sample_rate + ' Hz' : 'default'}
Latency{nd.quantum} samples{#if nd.sample_rate > 0} ({(nd.quantum / nd.sample_rate * 1000).toFixed(1)} ms){/if}
Format{nd.format}
Period Size{nd.rate}
Device{nd.device_name}
Bus{nd.device_bus}
API{nd.api}
Priority{nd.priority}
{/if} {/if} {#if showHideDialog}
Node Hiding Rules

Hide nodes matching pattern (regex or plain text).

{#each $patchbay.hide_rules as rule}
{rule}
{/each} {#if $patchbay.hide_rules.length === 0}
No hiding rules.
{/if}
{/if} {#if showMergeDialog}
Node Merging Rules

Merge nodes sharing a common name prefix into one block.

{#each $patchbay.merge_rules as rule}
{rule}
{/each} {#if $patchbay.merge_rules.length === 0}
No merge rules.
{/if}
{/if} {#if showProfileDialog}
Profile Management
{#each Object.entries($patchbay.profiles) as [name, profile]}
{name} ({profile.connections.length} rules)
{/each} {#if Object.keys($patchbay.profiles).length === 0}
No saved profiles.
{/if}
{/if} {#if showRuleDialog}
Rule Management
{#if $patchbay.active_profile && $patchbay.profiles[$patchbay.active_profile]} {@const profile = $patchbay.profiles[$patchbay.active_profile]}

Active profile: {profile.name} ({profile.connections.length} rules)

{#each profile.connections as rule, i}
{rule.output_node_name}:{shortName(rule.output_port_name)} => {rule.input_node_name}:{shortName(rule.input_port_name)} {rule.output_port_type} {#if rule.pinned}PIN{/if}
{/each}
{:else}
No active profile. Save a profile first via Profiles.
{/if}
{/if}