Add EntitySelect/IconSelect UI improvements across modals

- Portal IconSelect popups to document.body with position:fixed to prevent
  clipping by modal overflow-y:auto
- Replace custom scene selectors in automation editor with EntitySelect
  command-palette pickers (main scene + fallback scene)
- Add IconSelect grid for automation deactivation mode (none/revert/fallback)
- Add IconSelect grid for automation condition type and match type
- Replace mapped zone source dropdowns with EntitySelect pickers
- Replace scene target selector with EntityPalette.pick() pattern
- Remove effect palette preview bar from CSS editor
- Remove sensitivity badge from audio color strip source cards
- Clean up unused scene-selector CSS and scene-target-add-row CSS
- Add locale keys for all new UI elements across en/ru/zh

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 16:00:30 +03:00
parent 186940124c
commit 2712c6682e
32 changed files with 1204 additions and 391 deletions

View File

@@ -5,7 +5,8 @@ from some input, encapsulating everything needed to drive a physical LED strip:
calibration, color correction, smoothing, and FPS.
Current types:
PictureColorStripSource — derives LED colors from a PictureSource (screen capture)
PictureColorStripSource — derives LED colors from a single PictureSource (simple 4-edge calibration)
AdvancedPictureColorStripSource — line-based calibration across multiple PictureSources
StaticColorStripSource — constant solid color fills all LEDs
GradientColorStripSource — linear gradient across all LEDs from user-defined color stops
ColorCycleColorStripSource — smoothly cycles through a user-defined list of colors
@@ -240,11 +241,8 @@ class ColorStripSource:
os_listener=bool(data.get("os_listener", False)),
)
# Default: "picture" type
return PictureColorStripSource(
id=sid, name=name, source_type=source_type,
created_at=created_at, updated_at=updated_at, description=description,
clock_id=clock_id, picture_source_id=data.get("picture_source_id") or "",
# Shared picture-type field extraction
_picture_kwargs = dict(
fps=data.get("fps") or 30,
brightness=data["brightness"] if data.get("brightness") is not None else 1.0,
saturation=data["saturation"] if data.get("saturation") is not None else 1.0,
@@ -256,10 +254,39 @@ class ColorStripSource:
frame_interpolation=bool(data.get("frame_interpolation", False)),
)
if source_type == "picture_advanced":
return AdvancedPictureColorStripSource(
id=sid, name=name, source_type="picture_advanced",
created_at=created_at, updated_at=updated_at, description=description,
clock_id=clock_id, **_picture_kwargs,
)
# Default: "picture" type (simple 4-edge calibration)
return PictureColorStripSource(
id=sid, name=name, source_type=source_type,
created_at=created_at, updated_at=updated_at, description=description,
clock_id=clock_id, picture_source_id=data.get("picture_source_id") or "",
**_picture_kwargs,
)
def _picture_base_to_dict(source, d: dict) -> dict:
"""Populate dict with fields common to both picture source types."""
d["fps"] = source.fps
d["brightness"] = source.brightness
d["saturation"] = source.saturation
d["gamma"] = source.gamma
d["smoothing"] = source.smoothing
d["interpolation_mode"] = source.interpolation_mode
d["calibration"] = calibration_to_dict(source.calibration)
d["led_count"] = source.led_count
d["frame_interpolation"] = source.frame_interpolation
return d
@dataclass
class PictureColorStripSource(ColorStripSource):
"""Color strip source driven by a PictureSource (screen capture / static image).
"""Color strip source driven by a single PictureSource (simple 4-edge calibration).
Contains everything required to produce LED color arrays from a picture stream:
calibration (LED positions), color correction, smoothing, FPS target.
@@ -286,16 +313,38 @@ class PictureColorStripSource(ColorStripSource):
def to_dict(self) -> dict:
d = super().to_dict()
d["picture_source_id"] = self.picture_source_id
d["fps"] = self.fps
d["brightness"] = self.brightness
d["saturation"] = self.saturation
d["gamma"] = self.gamma
d["smoothing"] = self.smoothing
d["interpolation_mode"] = self.interpolation_mode
d["calibration"] = calibration_to_dict(self.calibration)
d["led_count"] = self.led_count
d["frame_interpolation"] = self.frame_interpolation
return d
return _picture_base_to_dict(self, d)
@dataclass
class AdvancedPictureColorStripSource(ColorStripSource):
"""Color strip source with line-based calibration across multiple picture sources.
Each calibration line references its own picture source and edge, enabling
LED strips that span multiple monitors. No single picture_source_id — the
picture sources are defined per-line in the calibration config.
"""
@property
def sharable(self) -> bool:
"""Picture streams are expensive (screen capture) and safe to share."""
return True
fps: int = 30
brightness: float = 1.0
saturation: float = 1.0
gamma: float = 1.0
smoothing: float = 0.3
interpolation_mode: str = "average"
calibration: CalibrationConfig = field(
default_factory=lambda: CalibrationConfig(mode="advanced")
)
led_count: int = 0
frame_interpolation: bool = False
def to_dict(self) -> dict:
d = super().to_dict()
return _picture_base_to_dict(self, d)
@dataclass

View File

@@ -8,6 +8,7 @@ from typing import Dict, List, Optional
from wled_controller.core.capture.calibration import CalibrationConfig, calibration_to_dict
from wled_controller.storage.color_strip_source import (
AdvancedPictureColorStripSource,
ApiInputColorStripSource,
AudioColorStripSource,
ColorCycleColorStripSource,
@@ -280,6 +281,27 @@ class ColorStripStore:
app_filter_list=app_filter_list if isinstance(app_filter_list, list) else [],
os_listener=bool(os_listener) if os_listener is not None else False,
)
elif source_type == "picture_advanced":
if calibration is None:
calibration = CalibrationConfig(mode="advanced")
source = AdvancedPictureColorStripSource(
id=source_id,
name=name,
source_type="picture_advanced",
created_at=now,
updated_at=now,
description=description,
clock_id=clock_id,
fps=fps,
brightness=brightness,
saturation=saturation,
gamma=gamma,
smoothing=smoothing,
interpolation_mode=interpolation_mode,
calibration=calibration,
led_count=led_count,
frame_interpolation=frame_interpolation,
)
else:
if calibration is None:
calibration = CalibrationConfig(layout="clockwise", start_position="bottom_left")
@@ -372,8 +394,8 @@ class ColorStripStore:
if clock_id is not None:
source.clock_id = clock_id if clock_id else None
if isinstance(source, PictureColorStripSource):
if picture_source_id is not None:
if isinstance(source, (PictureColorStripSource, AdvancedPictureColorStripSource)):
if picture_source_id is not None and isinstance(source, PictureColorStripSource):
source.picture_source_id = picture_source_id
if fps is not None:
source.fps = fps

View File

@@ -84,6 +84,7 @@ class ScenePresetStore:
name: Optional[str] = None,
description: Optional[str] = None,
order: Optional[int] = None,
targets: Optional[List[TargetSnapshot]] = None,
) -> ScenePreset:
if preset_id not in self._presets:
raise ValueError(f"Scene preset not found: {preset_id}")
@@ -99,6 +100,8 @@ class ScenePresetStore:
preset.description = description
if order is not None:
preset.order = order
if targets is not None:
preset.targets = targets
preset.updated_at = datetime.utcnow()
self._save()