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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user