diff --git a/server/src/wled_controller/api/routes.py b/server/src/wled_controller/api/routes.py index 9ecc691..f6caaa0 100644 --- a/server/src/wled_controller/api/routes.py +++ b/server/src/wled_controller/api/routes.py @@ -304,6 +304,7 @@ async def create_device( border_width=device.settings.border_width, interpolation_mode=device.settings.interpolation_mode, brightness=device.settings.brightness, + smoothing=device.settings.smoothing, state_check_interval=device.settings.state_check_interval, ), calibration=CalibrationSchema(**calibration_to_dict(device.calibration)), @@ -339,7 +340,9 @@ async def list_devices( display_index=device.settings.display_index, fps=device.settings.fps, border_width=device.settings.border_width, + interpolation_mode=device.settings.interpolation_mode, brightness=device.settings.brightness, + smoothing=device.settings.smoothing, state_check_interval=device.settings.state_check_interval, ), calibration=CalibrationSchema(**calibration_to_dict(device.calibration)), @@ -384,7 +387,9 @@ async def get_device( display_index=device.settings.display_index, fps=device.settings.fps, border_width=device.settings.border_width, + interpolation_mode=device.settings.interpolation_mode, brightness=device.settings.brightness, + smoothing=device.settings.smoothing, state_check_interval=device.settings.state_check_interval, ), calibration=CalibrationSchema(**calibration_to_dict(device.calibration)), @@ -474,6 +479,7 @@ async def update_device( border_width=device.settings.border_width, interpolation_mode=device.settings.interpolation_mode, brightness=device.settings.brightness, + smoothing=device.settings.smoothing, state_check_interval=device.settings.state_check_interval, ), calibration=CalibrationSchema(**calibration_to_dict(device.calibration)), @@ -619,6 +625,7 @@ async def update_settings( """Update processing settings for a device. Merges with existing settings so callers can send partial updates. + Only fields explicitly included in the request body are applied. """ try: # Get existing device to merge settings @@ -627,20 +634,31 @@ async def update_settings( raise HTTPException(status_code=404, detail=f"Device {device_id} not found") existing = device.settings + sent = settings.model_fields_set # fields the client actually sent - # Merge: use new values where provided, keep existing otherwise + # Merge: only override fields the client explicitly provided new_settings = ProcessingSettings( - display_index=settings.display_index, - fps=settings.fps, - border_width=settings.border_width, - interpolation_mode=settings.interpolation_mode, - brightness=settings.color_correction.brightness if settings.color_correction else existing.brightness, - gamma=settings.color_correction.gamma if settings.color_correction else existing.gamma, - saturation=settings.color_correction.saturation if settings.color_correction else existing.saturation, - smoothing=settings.smoothing, - state_check_interval=settings.state_check_interval, + display_index=settings.display_index if 'display_index' in sent else existing.display_index, + fps=settings.fps if 'fps' in sent else existing.fps, + border_width=settings.border_width if 'border_width' in sent else existing.border_width, + interpolation_mode=settings.interpolation_mode if 'interpolation_mode' in sent else existing.interpolation_mode, + brightness=settings.brightness if 'brightness' in sent else existing.brightness, + gamma=existing.gamma, + saturation=existing.saturation, + smoothing=settings.smoothing if 'smoothing' in sent else existing.smoothing, + state_check_interval=settings.state_check_interval if 'state_check_interval' in sent else existing.state_check_interval, ) + # Apply color_correction fields if explicitly sent + if 'color_correction' in sent and settings.color_correction: + cc_sent = settings.color_correction.model_fields_set + if 'brightness' in cc_sent: + new_settings.brightness = settings.color_correction.brightness + if 'gamma' in cc_sent: + new_settings.gamma = settings.color_correction.gamma + if 'saturation' in cc_sent: + new_settings.saturation = settings.color_correction.saturation + # Update in storage device = store.update_device(device_id, settings=new_settings) @@ -657,6 +675,7 @@ async def update_settings( border_width=device.settings.border_width, interpolation_mode=device.settings.interpolation_mode, brightness=device.settings.brightness, + smoothing=device.settings.smoothing, state_check_interval=device.settings.state_check_interval, ) diff --git a/server/src/wled_controller/core/processor_manager.py b/server/src/wled_controller/core/processor_manager.py index c962540..16a96f3 100644 --- a/server/src/wled_controller/core/processor_manager.py +++ b/server/src/wled_controller/core/processor_manager.py @@ -613,6 +613,8 @@ class ProcessorManager: ) # Send to WLED with device brightness + if not state.is_running or state.wled_client is None: + break brightness_value = int(wled_brightness * 255) await state.wled_client.send_pixels(led_colors, brightness=brightness_value) diff --git a/server/src/wled_controller/core/screen_capture.py b/server/src/wled_controller/core/screen_capture.py index ef8fd40..8ff8046 100644 --- a/server/src/wled_controller/core/screen_capture.py +++ b/server/src/wled_controller/core/screen_capture.py @@ -229,18 +229,16 @@ def get_edge_segments( divide_axis = 0 # Height edge_length = edge_pixels.shape[0] - if segment_count > edge_length: - raise ValueError( - f"segment_count {segment_count} is larger than edge length {edge_length}" - ) - - # Calculate segment size - segment_size = edge_length // segment_count + # Use float stepping so multiple LEDs can share pixels when + # segment_count > edge_length (e.g. after downscaling). + step = edge_length / segment_count segments = [] for i in range(segment_count): - start = i * segment_size - end = start + segment_size if i < segment_count - 1 else edge_length + start = int(i * step) + end = max(start + 1, int((i + 1) * step)) + # Clamp to edge bounds + end = min(end, edge_length) if divide_axis == 1: segment = edge_pixels[:, start:end, :] diff --git a/server/src/wled_controller/static/app.js b/server/src/wled_controller/static/app.js index 8d8f05b..5562efd 100644 --- a/server/src/wled_controller/static/app.js +++ b/server/src/wled_controller/static/app.js @@ -47,16 +47,18 @@ const EDGE_TEST_COLORS = { left: [255, 255, 0] }; -// Modal body lock helpers - prevent layout jump when scrollbar disappears +// Modal body lock helpers — uses position:fixed to freeze scroll without removing scrollbar function lockBody() { - const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth; - document.body.style.paddingRight = scrollbarWidth + 'px'; + const scrollY = window.scrollY; + document.body.style.top = `-${scrollY}px`; document.body.classList.add('modal-open'); } function unlockBody() { + const scrollY = parseInt(document.body.style.top || '0', 10) * -1; document.body.classList.remove('modal-open'); - document.body.style.paddingRight = ''; + document.body.style.top = ''; + window.scrollTo(0, scrollY); } // Image lightbox @@ -764,12 +766,13 @@ function createDeviceCard(device) { ` : ''} -
+
+ title="${_deviceBrightnessCache[device.id] != null ? Math.round(_deviceBrightnessCache[device.id] / 255 * 100) + '%' : '...'}" + ${_deviceBrightnessCache[device.id] == null ? 'disabled' : ''}>
${isProcessing ? ` @@ -1231,7 +1234,10 @@ async function fetchDeviceBrightness(deviceId) { if (slider) { slider.value = data.brightness; slider.title = Math.round(data.brightness / 255 * 100) + '%'; + slider.disabled = false; } + const wrap = document.querySelector(`[data-brightness-wrap="${deviceId}"]`); + if (wrap) wrap.classList.remove('brightness-loading'); } catch (err) { // Silently fail — device may be offline } @@ -4279,7 +4285,6 @@ async function updateStreamSelectorInfo(streamId) { ${typeIcon} ${typeName} ${propsHtml}
- ${stream.description ? `
${escapeHtml(stream.description)}
` : ''} `; infoPanel.style.display = ''; } catch { diff --git a/server/src/wled_controller/static/style.css b/server/src/wled_controller/static/style.css index e27841c..dadda5b 100644 --- a/server/src/wled_controller/static/style.css +++ b/server/src/wled_controller/static/style.css @@ -48,7 +48,8 @@ body { } body.modal-open { - overflow: hidden; + position: fixed; + width: 100%; } .container { @@ -595,6 +596,11 @@ section { width: 100%; } +.brightness-loading .brightness-slider { + opacity: 0.3; + pointer-events: none; +} + .section-header { display: flex; align-items: center;