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:
@@ -31,7 +31,7 @@ from wled_controller.core.capture.calibration import (
|
||||
)
|
||||
from wled_controller.core.capture.screen_capture import get_available_displays
|
||||
from wled_controller.core.processing.processor_manager import ProcessorManager
|
||||
from wled_controller.storage.color_strip_source import ApiInputColorStripSource, NotificationColorStripSource, PictureColorStripSource
|
||||
from wled_controller.storage.color_strip_source import AdvancedPictureColorStripSource, ApiInputColorStripSource, NotificationColorStripSource, PictureColorStripSource
|
||||
from wled_controller.storage.color_strip_store import ColorStripStore
|
||||
from wled_controller.storage.picture_source import ProcessedPictureSource, ScreenCapturePictureSource
|
||||
from wled_controller.storage.picture_source_store import PictureSourceStore
|
||||
@@ -47,7 +47,7 @@ router = APIRouter()
|
||||
def _css_to_response(source, overlay_active: bool = False) -> ColorStripSourceResponse:
|
||||
"""Convert a ColorStripSource to a ColorStripSourceResponse."""
|
||||
calibration = None
|
||||
if isinstance(source, PictureColorStripSource) and source.calibration:
|
||||
if isinstance(source, (PictureColorStripSource, AdvancedPictureColorStripSource)) and source.calibration:
|
||||
calibration = CalibrationSchema(**calibration_to_dict(source.calibration))
|
||||
|
||||
# Convert raw stop dicts to ColorStop schema objects for gradient sources
|
||||
@@ -376,7 +376,7 @@ async def test_css_calibration(
|
||||
if body.edges:
|
||||
try:
|
||||
source = store.get_source(source_id)
|
||||
if not isinstance(source, PictureColorStripSource):
|
||||
if not isinstance(source, (PictureColorStripSource, AdvancedPictureColorStripSource)):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Calibration test is only available for picture color strip sources",
|
||||
@@ -422,12 +422,13 @@ async def start_css_overlay(
|
||||
"""Start screen overlay visualization for a color strip source."""
|
||||
try:
|
||||
source = store.get_source(source_id)
|
||||
if not isinstance(source, PictureColorStripSource):
|
||||
if not isinstance(source, (PictureColorStripSource, AdvancedPictureColorStripSource)):
|
||||
raise HTTPException(status_code=400, detail="Overlay is only supported for picture color strip sources")
|
||||
if not source.calibration:
|
||||
raise HTTPException(status_code=400, detail="Color strip source has no calibration configured")
|
||||
|
||||
display_index = _resolve_display_index(source.picture_source_id, picture_source_store)
|
||||
ps_id = getattr(source, "picture_source_id", "") or ""
|
||||
display_index = _resolve_display_index(ps_id, picture_source_store)
|
||||
displays = get_available_displays()
|
||||
if not displays:
|
||||
raise HTTPException(status_code=409, detail="No displays available")
|
||||
|
||||
@@ -45,7 +45,7 @@ from wled_controller.core.capture.screen_capture import (
|
||||
get_available_displays,
|
||||
)
|
||||
from wled_controller.storage.color_strip_store import ColorStripStore
|
||||
from wled_controller.storage.color_strip_source import PictureColorStripSource
|
||||
from wled_controller.storage.color_strip_source import AdvancedPictureColorStripSource, PictureColorStripSource
|
||||
from wled_controller.storage import DeviceStore
|
||||
from wled_controller.storage.pattern_template_store import PatternTemplateStore
|
||||
from wled_controller.storage.picture_source import ScreenCapturePictureSource, StaticImagePictureSource
|
||||
@@ -820,11 +820,12 @@ async def start_target_overlay(
|
||||
if first_css_id:
|
||||
try:
|
||||
css = color_strip_store.get_source(first_css_id)
|
||||
if isinstance(css, PictureColorStripSource) and css.calibration:
|
||||
if isinstance(css, (PictureColorStripSource, AdvancedPictureColorStripSource)) and css.calibration:
|
||||
calibration = css.calibration
|
||||
# Resolve the display this CSS is capturing
|
||||
from wled_controller.api.routes.color_strip_sources import _resolve_display_index
|
||||
display_index = _resolve_display_index(css.picture_source_id, picture_source_store)
|
||||
ps_id = getattr(css, "picture_source_id", "") or ""
|
||||
display_index = _resolve_display_index(ps_id, picture_source_store)
|
||||
displays = get_available_displays()
|
||||
if displays:
|
||||
display_index = min(display_index, len(displays) - 1)
|
||||
|
||||
@@ -133,14 +133,42 @@ async def update_scene_preset(
|
||||
data: ScenePresetUpdate,
|
||||
_auth: AuthRequired,
|
||||
store: ScenePresetStore = Depends(get_scene_preset_store),
|
||||
target_store: OutputTargetStore = Depends(get_output_target_store),
|
||||
manager: ProcessorManager = Depends(get_processor_manager),
|
||||
):
|
||||
"""Update scene preset metadata."""
|
||||
"""Update scene preset metadata and optionally change targets."""
|
||||
# If target_ids changed, update the snapshot: keep state for existing targets,
|
||||
# capture fresh state for newly added targets, drop removed ones.
|
||||
new_targets = None
|
||||
if data.target_ids is not None:
|
||||
try:
|
||||
existing = store.get_preset(preset_id)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
existing_map = {t.target_id: t for t in existing.targets}
|
||||
new_target_ids = set(data.target_ids)
|
||||
|
||||
# Capture fresh state for newly added targets
|
||||
added_ids = new_target_ids - set(existing_map.keys())
|
||||
fresh = capture_current_snapshot(target_store, manager, added_ids) if added_ids else []
|
||||
fresh_map = {t.target_id: t for t in fresh}
|
||||
|
||||
# Build new target list preserving order from target_ids
|
||||
new_targets = []
|
||||
for tid in data.target_ids:
|
||||
if tid in existing_map:
|
||||
new_targets.append(existing_map[tid])
|
||||
elif tid in fresh_map:
|
||||
new_targets.append(fresh_map[tid])
|
||||
|
||||
try:
|
||||
preset = store.update_preset(
|
||||
preset_id,
|
||||
name=data.name,
|
||||
description=data.description,
|
||||
order=data.order,
|
||||
targets=new_targets,
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404 if "not found" in str(e).lower() else 400, detail=str(e))
|
||||
|
||||
@@ -49,7 +49,7 @@ class ColorStripSourceCreate(BaseModel):
|
||||
"""Request to create a color strip source."""
|
||||
|
||||
name: str = Field(description="Source name", min_length=1, max_length=100)
|
||||
source_type: Literal["picture", "static", "gradient", "color_cycle", "effect", "composite", "mapped", "audio", "api_input", "notification"] = Field(default="picture", description="Source type")
|
||||
source_type: Literal["picture", "picture_advanced", "static", "gradient", "color_cycle", "effect", "composite", "mapped", "audio", "api_input", "notification"] = Field(default="picture", description="Source type")
|
||||
# picture-type fields
|
||||
picture_source_id: str = Field(default="", description="Picture source ID (for picture type)")
|
||||
brightness: float = Field(default=1.0, description="Brightness multiplier (0.0-2.0)", ge=0.0, le=2.0)
|
||||
|
||||
@@ -34,9 +34,31 @@ class DeviceUpdate(BaseModel):
|
||||
zone_mode: Optional[str] = Field(None, description="OpenRGB zone mode: combined or separate")
|
||||
|
||||
|
||||
class CalibrationLineSchema(BaseModel):
|
||||
"""One LED line in advanced calibration."""
|
||||
|
||||
picture_source_id: str = Field(description="Picture source (monitor) to sample from")
|
||||
edge: Literal["top", "right", "bottom", "left"] = Field(description="Screen edge to sample")
|
||||
led_count: int = Field(ge=1, description="Number of LEDs in this line")
|
||||
span_start: float = Field(default=0.0, ge=0.0, le=1.0, description="Start fraction along edge")
|
||||
span_end: float = Field(default=1.0, ge=0.0, le=1.0, description="End fraction along edge")
|
||||
reverse: bool = Field(default=False, description="Reverse LED direction")
|
||||
border_width: int = Field(default=10, ge=1, le=100, description="Sampling depth in pixels")
|
||||
|
||||
|
||||
class Calibration(BaseModel):
|
||||
"""Calibration configuration for pixel-to-LED mapping."""
|
||||
|
||||
mode: Literal["simple", "advanced"] = Field(
|
||||
default="simple",
|
||||
description="Calibration mode: simple (4-edge) or advanced (multi-source lines)"
|
||||
)
|
||||
# Advanced mode: ordered list of lines
|
||||
lines: Optional[List[CalibrationLineSchema]] = Field(
|
||||
default=None,
|
||||
description="Line list for advanced mode (ignored in simple mode)"
|
||||
)
|
||||
# Simple mode fields
|
||||
layout: Literal["clockwise", "counterclockwise"] = Field(
|
||||
default="clockwise",
|
||||
description="LED strip layout direction"
|
||||
|
||||
@@ -23,11 +23,12 @@ class ScenePresetCreate(BaseModel):
|
||||
|
||||
|
||||
class ScenePresetUpdate(BaseModel):
|
||||
"""Update scene preset metadata (not snapshot data — use recapture for that)."""
|
||||
"""Update scene preset metadata and optionally change which targets are included."""
|
||||
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=100)
|
||||
description: Optional[str] = Field(None, max_length=500)
|
||||
order: Optional[int] = None
|
||||
target_ids: Optional[List[str]] = Field(None, description="Update target list: keep state for existing, capture fresh for new, drop removed")
|
||||
|
||||
|
||||
class ScenePresetResponse(BaseModel):
|
||||
|
||||
Reference in New Issue
Block a user