561 lines
20 KiB
JavaScript
561 lines
20 KiB
JavaScript
// Preset Controls Component
|
|
|
|
class PresetControls extends Component {
|
|
constructor(container, viewModel, eventBus) {
|
|
super(container, viewModel, eventBus);
|
|
this.presets = {};
|
|
this.currentPreset = null;
|
|
this.presetControls = new Map();
|
|
this.selectedNode = null; // Track which node is currently selected
|
|
this.nodeParameters = new Map(); // Store parameters per node: nodeIp -> { presetName, parameters }
|
|
}
|
|
|
|
mount() {
|
|
super.mount();
|
|
this.setupEventListeners();
|
|
this.setupViewModelListeners();
|
|
this.loadPresets();
|
|
}
|
|
|
|
setupEventListeners() {
|
|
// FPS slider
|
|
const fpsSlider = this.findElement('#fps-slider');
|
|
const fpsValue = this.findElement('#fps-value');
|
|
if (fpsSlider && fpsValue) {
|
|
this.addEventListener(fpsSlider, 'input', (e) => {
|
|
const fps = parseInt(e.target.value);
|
|
fpsValue.textContent = fps;
|
|
this.updateFrameRate(fps);
|
|
});
|
|
}
|
|
|
|
// Preset selection - immediate switching
|
|
const presetSelect = this.findElement('#preset-select');
|
|
if (presetSelect) {
|
|
this.addEventListener(presetSelect, 'change', (e) => {
|
|
const presetName = e.target.value;
|
|
this.selectPreset(presetName);
|
|
|
|
// If currently streaming, automatically restart with new preset
|
|
const toggleBtn = this.findElement('#toggle-stream-btn');
|
|
if (toggleBtn && toggleBtn.dataset.streaming === 'true' && presetName) {
|
|
this.startStreaming();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Apply matrix config button
|
|
const applyMatrixBtn = this.findElement('#apply-matrix-btn');
|
|
if (applyMatrixBtn) {
|
|
this.addEventListener(applyMatrixBtn, 'click', () => {
|
|
this.applyMatrixConfig();
|
|
});
|
|
}
|
|
|
|
// Toggle stream button
|
|
const toggleStreamBtn = this.findElement('#toggle-stream-btn');
|
|
if (toggleStreamBtn) {
|
|
this.addEventListener(toggleStreamBtn, 'click', () => {
|
|
const isStreaming = toggleStreamBtn.dataset.streaming === 'true';
|
|
if (isStreaming) {
|
|
this.stopStreaming();
|
|
} else {
|
|
this.startStreaming();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Test and clear buttons
|
|
const sendTestBtn = this.findElement('#send-test-btn');
|
|
if (sendTestBtn) {
|
|
this.addEventListener(sendTestBtn, 'click', () => {
|
|
this.sendTestFrame();
|
|
});
|
|
}
|
|
|
|
const clearMatrixBtn = this.findElement('#clear-matrix-btn');
|
|
if (clearMatrixBtn) {
|
|
this.addEventListener(clearMatrixBtn, 'click', () => {
|
|
this.clearMatrix();
|
|
});
|
|
}
|
|
}
|
|
|
|
setupViewModelListeners() {
|
|
// Listen for node selection
|
|
this.subscribeToEvent('selectNode', (data) => {
|
|
this.selectedNode = data.nodeIp;
|
|
this.loadNodeSettings();
|
|
});
|
|
|
|
this.subscribeToEvent('streamingStarted', (data) => {
|
|
this.updateStreamingState(true, data.preset, data.nodeIp);
|
|
});
|
|
|
|
this.subscribeToEvent('streamingStopped', (data) => {
|
|
this.updateStreamingState(false, null, data.nodeIp);
|
|
});
|
|
|
|
this.subscribeToEvent('presetParameterUpdated', (data) => {
|
|
// Only update if this is for the currently selected node
|
|
if (data.nodeIp === this.selectedNode) {
|
|
const control = this.presetControls.get(data.parameter);
|
|
if (control) {
|
|
if (control.type === 'range') {
|
|
control.value = data.value;
|
|
const valueDisplay = control.parentElement.querySelector('.preset-value');
|
|
if (valueDisplay) {
|
|
valueDisplay.textContent = parseFloat(data.value).toFixed(2);
|
|
}
|
|
} else if (control.type === 'color') {
|
|
control.value = this.hexToColorValue(data.value);
|
|
} else {
|
|
control.value = data.value;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
this.subscribeToEvent('status', (data) => {
|
|
// Update UI to reflect current server state
|
|
const isStreaming = data.data.streaming;
|
|
const currentPreset = data.data.currentPreset;
|
|
const presetParameters = data.data.presetParameters;
|
|
const fps = data.data.fps;
|
|
|
|
this.updateStreamingState(isStreaming, currentPreset ? { name: currentPreset } : null);
|
|
|
|
// Update FPS display
|
|
if (fps !== undefined) {
|
|
const fpsSlider = this.findElement('#fps-slider');
|
|
const fpsValue = this.findElement('#fps-value');
|
|
if (fpsSlider && fpsValue) {
|
|
fpsSlider.value = fps;
|
|
fpsValue.textContent = fps;
|
|
}
|
|
}
|
|
|
|
// Only select preset if it's different from current (avoid recreating controls)
|
|
const presetSelect = this.findElement('#preset-select');
|
|
if (currentPreset && presetSelect && presetSelect.value !== currentPreset) {
|
|
this.selectPreset(currentPreset);
|
|
}
|
|
|
|
if (presetParameters && this.currentPreset) {
|
|
// Update parameter controls with current values without triggering events
|
|
Object.entries(presetParameters).forEach(([param, value]) => {
|
|
const control = this.presetControls.get(param);
|
|
if (control) {
|
|
if (control.type === 'range') {
|
|
control.value = value;
|
|
const valueDisplay = control.parentElement.querySelector('.preset-value');
|
|
if (valueDisplay) {
|
|
valueDisplay.textContent = parseFloat(value).toFixed(2);
|
|
}
|
|
} else if (control.type === 'color') {
|
|
control.value = this.hexToColorValue(value);
|
|
} else {
|
|
control.value = value;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
this.subscribeToEvent('frameRateUpdated', (data) => {
|
|
const fpsSlider = this.findElement('#fps-slider');
|
|
const fpsValue = this.findElement('#fps-value');
|
|
if (fpsSlider && fpsValue && data.fps) {
|
|
fpsSlider.value = data.fps;
|
|
fpsValue.textContent = data.fps;
|
|
}
|
|
});
|
|
}
|
|
|
|
async loadPresets() {
|
|
try {
|
|
const response = await fetch('/api/presets');
|
|
const data = await response.json();
|
|
|
|
this.presets = data.presets;
|
|
this.populatePresetSelect();
|
|
|
|
} catch (error) {
|
|
console.error('Error loading presets:', error);
|
|
}
|
|
}
|
|
|
|
populatePresetSelect() {
|
|
const presetSelect = this.findElement('#preset-select');
|
|
if (!presetSelect) return;
|
|
|
|
// Clear existing options (except the first one)
|
|
while (presetSelect.children.length > 1) {
|
|
presetSelect.removeChild(presetSelect.lastChild);
|
|
}
|
|
|
|
// Add preset options
|
|
Object.entries(this.presets).forEach(([name, metadata]) => {
|
|
const option = document.createElement('option');
|
|
option.value = name;
|
|
option.textContent = metadata.name;
|
|
presetSelect.appendChild(option);
|
|
});
|
|
}
|
|
|
|
selectPreset(presetName) {
|
|
if (!presetName || !this.presets[presetName]) {
|
|
this.currentPreset = null;
|
|
this.clearPresetControls();
|
|
return;
|
|
}
|
|
|
|
this.currentPreset = this.presets[presetName];
|
|
|
|
// Store preset selection for current node
|
|
if (this.selectedNode) {
|
|
const nodeParams = this.nodeParameters.get(this.selectedNode) || {};
|
|
nodeParams.presetName = presetName;
|
|
this.nodeParameters.set(this.selectedNode, nodeParams);
|
|
}
|
|
|
|
this.createPresetControls();
|
|
}
|
|
|
|
loadNodeSettings() {
|
|
if (!this.selectedNode) return;
|
|
|
|
// Get stored parameters for this node
|
|
const nodeParams = this.nodeParameters.get(this.selectedNode);
|
|
|
|
if (nodeParams && nodeParams.presetName) {
|
|
// Load the preset and restore parameters
|
|
const presetSelect = this.findElement('#preset-select');
|
|
if (presetSelect && presetSelect.value !== nodeParams.presetName) {
|
|
presetSelect.value = nodeParams.presetName;
|
|
this.selectPreset(nodeParams.presetName);
|
|
}
|
|
|
|
// Restore parameter values
|
|
if (nodeParams.parameters) {
|
|
Object.entries(nodeParams.parameters).forEach(([param, value]) => {
|
|
const control = this.presetControls.get(param);
|
|
if (control) {
|
|
if (control.type === 'range') {
|
|
control.value = value;
|
|
const valueDisplay = control.parentElement.querySelector('.preset-value');
|
|
if (valueDisplay) {
|
|
valueDisplay.textContent = parseFloat(value).toFixed(2);
|
|
}
|
|
} else if (control.type === 'color') {
|
|
control.value = this.hexToColorValue(value);
|
|
} else {
|
|
control.value = value;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
} else {
|
|
// Reset to default
|
|
const presetSelect = this.findElement('#preset-select');
|
|
if (presetSelect) {
|
|
presetSelect.value = '';
|
|
this.clearPresetControls();
|
|
}
|
|
}
|
|
}
|
|
|
|
createPresetControls() {
|
|
const controlsContainer = this.findElement('#preset-controls');
|
|
if (!controlsContainer) return;
|
|
|
|
// Clear existing controls
|
|
controlsContainer.innerHTML = '';
|
|
|
|
if (!this.currentPreset || !this.currentPreset.parameters) {
|
|
return;
|
|
}
|
|
|
|
// Create controls for each parameter
|
|
Object.entries(this.currentPreset.parameters).forEach(([paramName, paramConfig]) => {
|
|
const controlDiv = document.createElement('div');
|
|
controlDiv.className = 'preset-control';
|
|
|
|
const label = document.createElement('label');
|
|
label.className = 'preset-label';
|
|
label.textContent = this.formatParameterName(paramName);
|
|
controlDiv.appendChild(label);
|
|
|
|
const input = this.createParameterInput(paramName, paramConfig);
|
|
controlDiv.appendChild(input);
|
|
|
|
controlsContainer.appendChild(controlDiv);
|
|
this.presetControls.set(paramName, input);
|
|
});
|
|
}
|
|
|
|
createParameterInput(paramName, paramConfig) {
|
|
const { type, min, max, step, default: defaultValue } = paramConfig;
|
|
|
|
switch (type) {
|
|
case 'range':
|
|
const sliderInput = document.createElement('input');
|
|
sliderInput.type = 'range';
|
|
sliderInput.className = 'preset-slider';
|
|
sliderInput.min = min;
|
|
sliderInput.max = max;
|
|
sliderInput.step = step || 0.1;
|
|
sliderInput.value = defaultValue;
|
|
|
|
// Add value display
|
|
const valueDisplay = document.createElement('span');
|
|
valueDisplay.className = 'preset-value';
|
|
valueDisplay.textContent = defaultValue;
|
|
|
|
sliderInput.addEventListener('input', (e) => {
|
|
const value = parseFloat(e.target.value);
|
|
valueDisplay.textContent = value.toFixed(2);
|
|
this.updatePresetParameter(paramName, value);
|
|
|
|
// Visual feedback for real-time update
|
|
valueDisplay.style.color = 'var(--accent-primary)';
|
|
setTimeout(() => {
|
|
valueDisplay.style.color = '';
|
|
}, 200);
|
|
});
|
|
|
|
const container = document.createElement('div');
|
|
container.style.display = 'flex';
|
|
container.style.alignItems = 'center';
|
|
container.style.gap = '0.5rem';
|
|
container.appendChild(sliderInput);
|
|
container.appendChild(valueDisplay);
|
|
|
|
return container;
|
|
|
|
case 'color':
|
|
const colorInput = document.createElement('input');
|
|
colorInput.type = 'color';
|
|
colorInput.className = 'preset-input';
|
|
colorInput.value = this.hexToColorValue(defaultValue);
|
|
|
|
colorInput.addEventListener('input', (e) => {
|
|
const hexValue = this.colorValueToHex(e.target.value);
|
|
this.updatePresetParameter(paramName, hexValue);
|
|
|
|
// Visual feedback for real-time update
|
|
colorInput.style.borderColor = 'var(--accent-primary)';
|
|
setTimeout(() => {
|
|
colorInput.style.borderColor = '';
|
|
}, 200);
|
|
});
|
|
|
|
return colorInput;
|
|
|
|
default:
|
|
const textInput = document.createElement('input');
|
|
textInput.type = 'text';
|
|
textInput.className = 'preset-input';
|
|
textInput.value = defaultValue;
|
|
|
|
textInput.addEventListener('input', (e) => {
|
|
this.updatePresetParameter(paramName, e.target.value);
|
|
|
|
// Visual feedback for real-time update
|
|
textInput.style.borderColor = 'var(--accent-primary)';
|
|
setTimeout(() => {
|
|
textInput.style.borderColor = '';
|
|
}, 200);
|
|
});
|
|
|
|
return textInput;
|
|
}
|
|
}
|
|
|
|
updatePresetParameter(parameter, value) {
|
|
// Store parameter for current node
|
|
if (this.selectedNode) {
|
|
const nodeParams = this.nodeParameters.get(this.selectedNode) || {};
|
|
if (!nodeParams.parameters) {
|
|
nodeParams.parameters = {};
|
|
}
|
|
nodeParams.parameters[parameter] = value;
|
|
this.nodeParameters.set(this.selectedNode, nodeParams);
|
|
}
|
|
|
|
// Send parameter update to server immediately (real-time)
|
|
this.viewModel.publish('updatePresetParameter', {
|
|
parameter,
|
|
value,
|
|
nodeIp: this.selectedNode
|
|
});
|
|
|
|
console.log(`Parameter updated for ${this.selectedNode}: ${parameter} = ${value}`);
|
|
}
|
|
|
|
clearPresetControls() {
|
|
const controlsContainer = this.findElement('#preset-controls');
|
|
if (controlsContainer) {
|
|
controlsContainer.innerHTML = '';
|
|
}
|
|
this.presetControls.clear();
|
|
}
|
|
|
|
formatParameterName(name) {
|
|
return name
|
|
.split(/(?=[A-Z])/)
|
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
.join(' ');
|
|
}
|
|
|
|
hexToColorValue(hex) {
|
|
// Convert hex (rrggbb) to color value (#rrggbb)
|
|
if (hex.startsWith('#')) return hex;
|
|
return `#${hex}`;
|
|
}
|
|
|
|
colorValueToHex(colorValue) {
|
|
// Convert color value (#rrggbb) to hex (rrggbb)
|
|
return colorValue.replace('#', '');
|
|
}
|
|
|
|
startStreaming() {
|
|
const presetSelect = this.findElement('#preset-select');
|
|
if (!presetSelect || !presetSelect.value) {
|
|
alert('Please select a preset first');
|
|
return;
|
|
}
|
|
|
|
if (!this.selectedNode) {
|
|
alert('Please select a node first');
|
|
return;
|
|
}
|
|
|
|
const width = parseInt(this.findElement('#matrix-width')?.value) || 16;
|
|
const height = parseInt(this.findElement('#matrix-height')?.value) || 16;
|
|
|
|
// Get current parameters for this node
|
|
const nodeParams = this.nodeParameters.get(this.selectedNode);
|
|
const parameters = nodeParams?.parameters || {};
|
|
|
|
this.viewModel.publish('startPreset', {
|
|
presetName: presetSelect.value,
|
|
width,
|
|
height,
|
|
nodeIp: this.selectedNode,
|
|
parameters
|
|
});
|
|
}
|
|
|
|
stopStreaming() {
|
|
this.viewModel.publish('stopStreaming', {
|
|
nodeIp: this.selectedNode
|
|
});
|
|
}
|
|
|
|
sendTestFrame() {
|
|
if (!this.selectedNode) {
|
|
alert('Please select a node first');
|
|
return;
|
|
}
|
|
|
|
// Create a test frame with a simple pattern in serpentine order
|
|
const width = parseInt(this.findElement('#matrix-width')?.value) || 16;
|
|
const height = parseInt(this.findElement('#matrix-height')?.value) || 16;
|
|
|
|
let frameData = 'RAW:';
|
|
for (let row = 0; row < height; row++) {
|
|
for (let col = 0; col < width; col++) {
|
|
// Calculate serpentine index manually
|
|
const hardwareIndex = (row % 2 === 0) ?
|
|
(row * width + col) :
|
|
(row * width + (width - 1 - col));
|
|
// Create a checkerboard pattern
|
|
if ((row + col) % 2 === 0) {
|
|
frameData += '00ff00'; // Green
|
|
} else {
|
|
frameData += '000000'; // Black
|
|
}
|
|
}
|
|
}
|
|
|
|
this.viewModel.publish('sendToNode', {
|
|
nodeIp: this.selectedNode,
|
|
message: frameData
|
|
});
|
|
}
|
|
|
|
clearMatrix() {
|
|
if (!this.selectedNode) {
|
|
alert('Please select a node first');
|
|
return;
|
|
}
|
|
|
|
// Send a frame with all black pixels in serpentine order
|
|
const width = parseInt(this.findElement('#matrix-width')?.value) || 16;
|
|
const height = parseInt(this.findElement('#matrix-height')?.value) || 16;
|
|
|
|
let frameData = 'RAW:';
|
|
for (let row = 0; row < height; row++) {
|
|
for (let col = 0; col < width; col++) {
|
|
// Calculate serpentine index manually
|
|
const hardwareIndex = (row % 2 === 0) ?
|
|
(row * width + col) :
|
|
(row * width + (width - 1 - col));
|
|
frameData += '000000';
|
|
}
|
|
}
|
|
|
|
this.viewModel.publish('sendToNode', {
|
|
nodeIp: this.selectedNode,
|
|
message: frameData
|
|
});
|
|
}
|
|
|
|
applyMatrixConfig() {
|
|
const width = parseInt(this.findElement('#matrix-width')?.value);
|
|
const height = parseInt(this.findElement('#matrix-height')?.value);
|
|
|
|
if (width && height) {
|
|
this.viewModel.publish('setMatrixSize', { width, height });
|
|
}
|
|
}
|
|
|
|
updateStreamingState(isStreaming, preset, nodeIp) {
|
|
// Only update UI if this is for the currently selected node
|
|
if (nodeIp !== this.selectedNode && nodeIp !== null) {
|
|
return;
|
|
}
|
|
|
|
const toggleBtn = this.findElement('#toggle-stream-btn');
|
|
const btnIcon = toggleBtn?.querySelector('.btn-icon');
|
|
const btnText = toggleBtn?.querySelector('.btn-text');
|
|
|
|
if (toggleBtn) {
|
|
toggleBtn.dataset.streaming = isStreaming ? 'true' : 'false';
|
|
|
|
if (isStreaming) {
|
|
if (btnIcon) btnIcon.textContent = '⏸';
|
|
if (btnText) btnText.textContent = 'Stop Streaming';
|
|
toggleBtn.classList.remove('btn-primary');
|
|
toggleBtn.classList.add('btn-stop');
|
|
} else {
|
|
if (btnIcon) btnIcon.textContent = '▶';
|
|
if (btnText) btnText.textContent = 'Start Streaming';
|
|
toggleBtn.classList.remove('btn-stop');
|
|
toggleBtn.classList.add('btn-primary');
|
|
}
|
|
}
|
|
}
|
|
|
|
updateFrameRate(fps) {
|
|
this.viewModel.publish('updateFrameRate', { fps });
|
|
}
|
|
}
|
|
|
|
// Export for use in other modules
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = PresetControls;
|
|
}
|