feat: improve UI
This commit is contained in:
@@ -201,6 +201,13 @@ class LEDLabApp {
|
||||
});
|
||||
});
|
||||
|
||||
this.eventBus.subscribe('updateFrameRate', (data) => {
|
||||
this.sendWebSocketMessage({
|
||||
type: 'updateFrameRate',
|
||||
...data
|
||||
});
|
||||
});
|
||||
|
||||
// Handle theme changes
|
||||
window.addEventListener('themeChanged', (event) => {
|
||||
console.log('Theme changed to:', event.detail.theme);
|
||||
|
||||
139
public/scripts/navigation.js
Normal file
139
public/scripts/navigation.js
Normal file
@@ -0,0 +1,139 @@
|
||||
// Navigation Manager for SPORE LEDLab
|
||||
|
||||
class NavigationManager {
|
||||
constructor() {
|
||||
this.currentView = 'stream';
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupNavigationListeners();
|
||||
// Sync settings values on init
|
||||
this.syncSettingsValues();
|
||||
}
|
||||
|
||||
setupNavigationListeners() {
|
||||
// Navigation tab listeners
|
||||
const navTabs = document.querySelectorAll('.nav-tab');
|
||||
navTabs.forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
const view = tab.dataset.view;
|
||||
this.switchView(view);
|
||||
});
|
||||
});
|
||||
|
||||
// Settings apply button
|
||||
const settingsApplyBtn = document.getElementById('settings-apply-matrix-btn');
|
||||
if (settingsApplyBtn) {
|
||||
settingsApplyBtn.addEventListener('click', () => {
|
||||
this.applySettingsFromView();
|
||||
});
|
||||
}
|
||||
|
||||
// Sync settings when stream view matrix config changes
|
||||
const streamWidthInput = document.getElementById('matrix-width');
|
||||
const streamHeightInput = document.getElementById('matrix-height');
|
||||
|
||||
if (streamWidthInput) {
|
||||
streamWidthInput.addEventListener('change', () => this.syncSettingsValues());
|
||||
}
|
||||
if (streamHeightInput) {
|
||||
streamHeightInput.addEventListener('change', () => this.syncSettingsValues());
|
||||
}
|
||||
}
|
||||
|
||||
switchView(viewName) {
|
||||
// Hide all views
|
||||
const allViews = document.querySelectorAll('.view-content');
|
||||
allViews.forEach(view => view.classList.remove('active'));
|
||||
|
||||
// Deactivate all tabs
|
||||
const allTabs = document.querySelectorAll('.nav-tab');
|
||||
allTabs.forEach(tab => tab.classList.remove('active'));
|
||||
|
||||
// Show selected view
|
||||
const targetView = document.getElementById(`${viewName}-view`);
|
||||
if (targetView) {
|
||||
targetView.classList.add('active');
|
||||
}
|
||||
|
||||
// Activate selected tab
|
||||
const targetTab = document.querySelector(`.nav-tab[data-view="${viewName}"]`);
|
||||
if (targetTab) {
|
||||
targetTab.classList.add('active');
|
||||
}
|
||||
|
||||
this.currentView = viewName;
|
||||
|
||||
// Sync values when switching to settings
|
||||
if (viewName === 'settings') {
|
||||
this.syncSettingsValues();
|
||||
}
|
||||
|
||||
console.log(`Switched to ${viewName} view`);
|
||||
}
|
||||
|
||||
syncSettingsValues() {
|
||||
// Sync matrix configuration values from stream to settings
|
||||
const streamWidth = document.getElementById('matrix-width');
|
||||
const streamHeight = document.getElementById('matrix-height');
|
||||
const settingsWidth = document.getElementById('settings-matrix-width');
|
||||
const settingsHeight = document.getElementById('settings-matrix-height');
|
||||
|
||||
if (streamWidth && settingsWidth) {
|
||||
settingsWidth.value = streamWidth.value;
|
||||
}
|
||||
if (streamHeight && settingsHeight) {
|
||||
settingsHeight.value = streamHeight.value;
|
||||
}
|
||||
}
|
||||
|
||||
applySettingsFromView() {
|
||||
const settingsWidth = document.getElementById('settings-matrix-width');
|
||||
const settingsHeight = document.getElementById('settings-matrix-height');
|
||||
const streamWidth = document.getElementById('matrix-width');
|
||||
const streamHeight = document.getElementById('matrix-height');
|
||||
|
||||
if (settingsWidth && streamWidth) {
|
||||
streamWidth.value = settingsWidth.value;
|
||||
}
|
||||
if (settingsHeight && streamHeight) {
|
||||
streamHeight.value = settingsHeight.value;
|
||||
}
|
||||
|
||||
// Trigger the apply button in the stream view
|
||||
const applyBtn = document.getElementById('apply-matrix-btn');
|
||||
if (applyBtn) {
|
||||
applyBtn.click();
|
||||
}
|
||||
|
||||
// Show success feedback
|
||||
const btn = document.getElementById('settings-apply-matrix-btn');
|
||||
if (btn) {
|
||||
const originalText = btn.textContent;
|
||||
btn.textContent = '✓ Applied!';
|
||||
btn.style.background = 'linear-gradient(135deg, var(--accent-success) 0%, #22c55e 100%)';
|
||||
|
||||
setTimeout(() => {
|
||||
btn.textContent = originalText;
|
||||
btn.style.background = '';
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
getCurrentView() {
|
||||
return this.currentView;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize navigation when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
window.navigationManager = new NavigationManager();
|
||||
console.log('Navigation manager initialized');
|
||||
});
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = NavigationManager;
|
||||
}
|
||||
|
||||
@@ -16,11 +16,29 @@ class PresetControls extends Component {
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Preset selection
|
||||
// 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) => {
|
||||
this.selectPreset(e.target.value);
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -32,18 +50,16 @@ class PresetControls extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
// Start/Stop buttons
|
||||
const startBtn = this.findElement('#start-btn');
|
||||
if (startBtn) {
|
||||
this.addEventListener(startBtn, 'click', () => {
|
||||
this.startStreaming();
|
||||
});
|
||||
}
|
||||
|
||||
const stopBtn = this.findElement('#stop-btn');
|
||||
if (stopBtn) {
|
||||
this.addEventListener(stopBtn, 'click', () => {
|
||||
this.stopStreaming();
|
||||
// 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -73,7 +89,21 @@ class PresetControls extends Component {
|
||||
});
|
||||
|
||||
this.subscribeToEvent('presetParameterUpdated', (data) => {
|
||||
this.updatePresetParameter(data.parameter, data.value);
|
||||
// Update control display without triggering another update
|
||||
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -82,15 +112,28 @@ class PresetControls extends Component {
|
||||
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);
|
||||
|
||||
if (currentPreset) {
|
||||
this.selectPreset(currentPreset.toLowerCase().replace('-preset', ''));
|
||||
// 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
|
||||
// Update parameter controls with current values without triggering events
|
||||
Object.entries(presetParameters).forEach(([param, value]) => {
|
||||
const control = this.presetControls.get(param);
|
||||
if (control) {
|
||||
@@ -109,6 +152,15 @@ class PresetControls extends Component {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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() {
|
||||
@@ -201,8 +253,15 @@ class PresetControls extends Component {
|
||||
valueDisplay.textContent = defaultValue;
|
||||
|
||||
sliderInput.addEventListener('input', (e) => {
|
||||
valueDisplay.textContent = parseFloat(e.target.value).toFixed(2);
|
||||
this.updatePresetParameter(paramName, parseFloat(e.target.value));
|
||||
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');
|
||||
@@ -223,6 +282,12 @@ class PresetControls extends Component {
|
||||
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;
|
||||
@@ -235,6 +300,12 @@ class PresetControls extends Component {
|
||||
|
||||
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;
|
||||
@@ -242,11 +313,13 @@ class PresetControls extends Component {
|
||||
}
|
||||
|
||||
updatePresetParameter(parameter, value) {
|
||||
// Send parameter update to server
|
||||
// Send parameter update to server immediately (real-time)
|
||||
this.viewModel.publish('updatePresetParameter', {
|
||||
parameter,
|
||||
value
|
||||
});
|
||||
|
||||
console.log(`Parameter updated: ${parameter} = ${value}`);
|
||||
}
|
||||
|
||||
clearPresetControls() {
|
||||
@@ -353,17 +426,30 @@ class PresetControls extends Component {
|
||||
}
|
||||
|
||||
updateStreamingState(isStreaming, preset) {
|
||||
const startBtn = this.findElement('#start-btn');
|
||||
const stopBtn = this.findElement('#stop-btn');
|
||||
const toggleBtn = this.findElement('#toggle-stream-btn');
|
||||
const btnIcon = toggleBtn?.querySelector('.btn-icon');
|
||||
const btnText = toggleBtn?.querySelector('.btn-text');
|
||||
|
||||
if (isStreaming) {
|
||||
startBtn.disabled = true;
|
||||
stopBtn.disabled = false;
|
||||
} else {
|
||||
startBtn.disabled = false;
|
||||
stopBtn.disabled = true;
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user