Merge feature/property-inspector into master

This commit is contained in:
joren
2026-03-30 00:33:46 +02:00
5 changed files with 104 additions and 1 deletions

View File

@@ -27,6 +27,7 @@
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);
let showPropsDialog = $state<number | null>(null); // node ID or null
// Filters
let showAudio = $state(true);
@@ -658,6 +659,7 @@
{#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={() => { showPropsDialog = nodeContextMenu!.nodeId; nodeContextMenu = null; }}>Properties</button>
<button onclick={() => {
fetch('/api/destroy-node', {
method: 'POST',
@@ -669,6 +671,41 @@
</div>
{/if}
<!-- Properties dialog -->
{#if showPropsDialog !== null}
{@const nd = $nodes.find(n => n.id === showPropsDialog)}
{#if nd}
<div class="dialog">
<div class="dialog-header">
<span>Properties: {nd.nick || nd.name}</span>
<button class="close" onclick={() => { showPropsDialog = null; }}>X</button>
</div>
<div class="dialog-body">
<table class="props-table">
<tbody>
<tr><td class="pk">ID</td><td>{nd.id}</td></tr>
<tr><td class="pk">Name</td><td>{nd.name}</td></tr>
{#if nd.nick}<tr><td class="pk">Nick</td><td>{nd.nick}</td></tr>{/if}
{#if nd.media_name}<tr><td class="pk">Media Name</td><td>{nd.media_name}</td></tr>{/if}
<tr><td class="pk">Class</td><td>{nd.mode} / {nd.node_type}</td></tr>
<tr><td class="pk">Volume</td><td>{Math.round(nd.volume * 100)}% {nd.mute ? '(muted)' : ''}</td></tr>
<tr><td class="pk">Ports</td><td>{nd.port_ids.length}</td></tr>
{#if nd.sample_rate > 0}<tr><td class="pk">Sample Rate</td><td>{nd.sample_rate} Hz</td></tr>{/if}
{#if nd.channels > 0}<tr><td class="pk">Channels</td><td>{nd.channels}</td></tr>{/if}
{#if nd.format}<tr><td class="pk">Format</td><td>{nd.format}</td></tr>{/if}
{#if nd.quantum > 0}<tr><td class="pk">Quantum</td><td>{nd.quantum}</td></tr>{/if}
{#if nd.rate > 0}<tr><td class="pk">Rate</td><td>{nd.rate}</td></tr>{/if}
{#if nd.device_name}<tr><td class="pk">Device</td><td>{nd.device_name}</td></tr>{/if}
{#if nd.device_bus}<tr><td class="pk">Bus</td><td>{nd.device_bus}</td></tr>{/if}
{#if nd.api}<tr><td class="pk">API</td><td>{nd.api}</td></tr>{/if}
{#if nd.priority > 0}<tr><td class="pk">Priority</td><td>{nd.priority}</td></tr>{/if}
</tbody>
</table>
</div>
</div>
{/if}
{/if}
<!-- Hide Nodes Dialog -->
{#if showHideDialog}
<div class="dialog">
@@ -897,6 +934,11 @@
.empty { font-size: 11px; color: #555; padding: 8px 0; text-align: center; }
.props-table { width: 100%; border-collapse: collapse; font-size: 11px; }
.props-table td { padding: 2px 6px; border-bottom: 1px solid #2a2a3e; }
.props-table td:first-child { color: #888; white-space: nowrap; width: 100px; }
.props-table td:last-child { color: #ccc; word-break: break-all; }
.port-circle { cursor: crosshair; }
.port-circle:hover { filter: brightness(1.5); }
.edge-path { cursor: pointer; pointer-events: stroke; }

View File

@@ -18,6 +18,15 @@ export interface Node {
node_type: string;
volume: number;
mute: boolean;
sample_rate: number;
channels: number;
quantum: number;
rate: number;
format: string;
device_name: string;
device_bus: string;
api: string;
priority: number;
port_ids: number[];
}

View File

@@ -97,6 +97,36 @@ static void on_node_info(void *data, const struct pw_node_info *info) {
if (mute_str) {
nobj->node.mute = pw_properties_parse_bool(mute_str);
}
// Read additional properties
const char *str;
str = spa_dict_lookup(info->props, "default.clock.rate");
if (str) nobj->node.sample_rate = (uint32_t)atoi(str);
str = spa_dict_lookup(info->props, "default.clock.quantum");
if (str) nobj->node.quantum = (uint32_t)atoi(str);
str = spa_dict_lookup(info->props, "audio.channels");
if (str) nobj->node.channels = (uint32_t)atoi(str);
str = spa_dict_lookup(info->props, "audio.format");
if (str) nobj->node.format = str;
str = spa_dict_lookup(info->props, "device.name");
if (str) nobj->node.device_name = str;
str = spa_dict_lookup(info->props, "device.bus");
if (str) nobj->node.device_bus = str;
str = spa_dict_lookup(info->props, "api.alsa.path");
if (str) nobj->node.api = str;
str = spa_dict_lookup(info->props, "priority.driver");
if (str) nobj->node.priority = atoi(str);
str = spa_dict_lookup(info->props, "node.rate");
if (str) nobj->node.rate = (uint32_t)atoi(str);
}
}

View File

@@ -67,11 +67,24 @@ struct Node {
float volume; // 0.0 - 1.0 (linear)
bool mute;
// Properties
uint32_t sample_rate;
uint32_t channels;
std::string format;
uint32_t quantum;
uint32_t rate;
std::string device_name;
std::string device_bus;
std::string api;
int priority;
std::vector<uint32_t> port_ids; // child port IDs
Node() : id(0), mode(PortMode::None), node_type(NodeType::None),
mode2(PortMode::None), ready(false), changed(false),
volume(1.0f), mute(false) {}
volume(1.0f), mute(false),
sample_rate(0), channels(0), quantum(0), rate(0),
priority(0) {}
};
struct Link;

View File

@@ -111,6 +111,15 @@ std::string WebServer::buildGraphJson() const {
<< ",\"node_type\":\"" << nodeTypeStr(n.node_type) << "\""
<< ",\"volume\":" << n.volume
<< ",\"mute\":" << (n.mute ? "true" : "false")
<< ",\"sample_rate\":" << n.sample_rate
<< ",\"channels\":" << n.channels
<< ",\"quantum\":" << n.quantum
<< ",\"rate\":" << n.rate
<< ",\"format\":\"" << escapeJson(n.format) << "\""
<< ",\"device_name\":\"" << escapeJson(n.device_name) << "\""
<< ",\"device_bus\":\"" << escapeJson(n.device_bus) << "\""
<< ",\"api\":\"" << escapeJson(n.api) << "\""
<< ",\"priority\":" << n.priority
<< ",\"port_ids\":[";
bool first_p = true;
for (uint32_t pid : n.port_ids) {