feat: create virtual devices (+ Add Device dropdown)
Backend:
- POST /api/create-null-sink {name} - loads null-sink module
- POST /api/create-loopback {name} - loads loopback module
- POST /api/unload-module {module_id} - unloads a module
- Fixed double-proxy-destroy crash in GraphEngine
- Graceful failure when module not available (no crash)
Frontend:
- + Add Device button in toolbar with dropdown menu
- Null Sink option (creates virtual audio output)
- Loopback Device option (creates paired input+output)
- Dropdown closes on outside click
Note: null-sink requires libpipewire-module-null-sink to be installed.
Loopback works on all PipeWire installations.
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
setAutoPin, setAutoDisconnect,
|
||||
saveProfile, loadProfile, deleteProfile,
|
||||
setNodeVolume, setNodeMute,
|
||||
createNullSink, createLoopback,
|
||||
} from '../lib/stores';
|
||||
import type { Node, Port, Link } from '../lib/types';
|
||||
|
||||
@@ -37,6 +38,7 @@
|
||||
let showMergeDialog = $state(false);
|
||||
let showProfileDialog = $state(false);
|
||||
let showRuleDialog = $state(false);
|
||||
let showVirtualMenu = $state(false);
|
||||
let newHideRule = $state('');
|
||||
let newMergeRule = $state('');
|
||||
let newProfileName = $state('');
|
||||
@@ -377,7 +379,7 @@
|
||||
onDestroy(() => { destroyGraph(); });
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={onKey} />
|
||||
<svelte:window onkeydown={onKey} onclick={() => { contextMenu = null; showVirtualMenu = false; }} />
|
||||
|
||||
<div class="wrap">
|
||||
<div class="toolbar">
|
||||
@@ -412,10 +414,20 @@
|
||||
<button onclick={() => { showMergeDialog = !showMergeDialog; showHideDialog = false; showProfileDialog = false; showRuleDialog = false; }} title="Node merging rules">Merge Nodes</button>
|
||||
<button onclick={() => { showRuleDialog = !showRuleDialog; showHideDialog = false; showMergeDialog = false; showProfileDialog = false; }} title="Manage patchbay rules">Rules</button>
|
||||
<button onclick={() => { showProfileDialog = !showProfileDialog; showHideDialog = false; showMergeDialog = false; showRuleDialog = false; }} title="Save/load profiles">Profiles</button>
|
||||
<span class="sep"></span>
|
||||
<button class="add-btn" onclick={() => { showVirtualMenu = !showVirtualMenu; }} title="Add virtual device">+ Add Device</button>
|
||||
|
||||
<span class="stats">{$nodes.length}N {$ports.length}P {$links.length}L {#if $patchbay.pinned_connections.length > 0}{$patchbay.pinned_connections.length}p{/if}</span>
|
||||
</div>
|
||||
|
||||
<!-- Virtual device dropdown -->
|
||||
{#if showVirtualMenu}
|
||||
<div class="virt-menu">
|
||||
<button onclick={async () => { showVirtualMenu = false; await createNullSink('pwweb-null-' + Date.now().toString(36)); }}>Null Sink (Virtual Output)</button>
|
||||
<button onclick={async () => { showVirtualMenu = false; await createLoopback('pwweb-loop-' + Date.now().toString(36)); }}>Loopback Device</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<svg
|
||||
bind:this={svgEl}
|
||||
@@ -802,5 +814,20 @@
|
||||
.vol-bar { pointer-events: none; }
|
||||
.vol-hitarea { cursor: pointer; }
|
||||
.vol-handle { cursor: ew-resize; pointer-events: none; }
|
||||
.vol-handle:hover { filter: brightness(1.3); }
|
||||
.vol-handle:hover { filter: brightness(1.3); } .add-btn { background: #1a3a2a !important; border-color: #4a9 !important; color: #6c9 !important; }
|
||||
|
||||
.virt-menu {
|
||||
position: absolute; top: 32px; z-index: 20;
|
||||
left: 50%; transform: translateX(-50%);
|
||||
background: #2a2a3e; border: 1px solid #555;
|
||||
border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.6);
|
||||
}
|
||||
.virt-menu button {
|
||||
display: block; width: 100%; padding: 6px 16px;
|
||||
background: none; border: none; color: #ccc;
|
||||
font-size: 11px; cursor: pointer; text-align: left; font-family: monospace;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.virt-menu button:hover { background: #444; }
|
||||
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user