Add capture template system with in-memory defaults and split device settings UI
Some checks failed
Validate / validate (push) Failing after 8s

- Generate default templates (MSS, DXcam, WGC) in memory from EngineRegistry at startup
- Only persist user-created templates to JSON, skip defaults on load/save
- Add capture_template_id to Device model and DeviceCreate schema
- Remember last used template in localStorage, use it for new devices with fallback
- Split Device Settings dialog into General Settings and Capture Settings
- Add capture settings button (🎬) to device card
- Separate default and custom templates with visual separator in Templates tab
- Add capture engine integration to ProcessorManager
- Add CLAUDE.md with git commit/push policy and server restart instructions
- Add en/ru localization for all new UI elements

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-10 02:43:49 +03:00
parent b5545d3198
commit 5370d80466
15 changed files with 772 additions and 106 deletions

View File

@@ -13,8 +13,9 @@ from wled_controller.core.calibration import (
PixelMapper,
create_default_calibration,
)
from wled_controller.core.capture_engines import CaptureEngine, EngineRegistry
from wled_controller.core.pixel_processor import apply_color_correction, smooth_colors
from wled_controller.core.screen_capture import capture_display, extract_border_pixels
from wled_controller.core.screen_capture import extract_border_pixels
from wled_controller.core.wled_client import WLEDClient
from wled_controller.utils import get_logger
@@ -87,8 +88,10 @@ class ProcessorState:
led_count: int
settings: ProcessingSettings
calibration: CalibrationConfig
capture_template_id: str = "tpl_mss_default" # NEW: template ID for capture engine
wled_client: Optional[WLEDClient] = None
pixel_mapper: Optional[PixelMapper] = None
capture_engine: Optional[CaptureEngine] = None # NEW: initialized capture engine
is_running: bool = False
task: Optional[asyncio.Task] = None
metrics: ProcessingMetrics = field(default_factory=ProcessingMetrics)
@@ -122,6 +125,7 @@ class ProcessorManager:
led_count: int,
settings: Optional[ProcessingSettings] = None,
calibration: Optional[CalibrationConfig] = None,
capture_template_id: str = "tpl_mss_default",
):
"""Add a device for processing.
@@ -131,6 +135,7 @@ class ProcessorManager:
led_count: Number of LEDs
settings: Processing settings (uses defaults if None)
calibration: Calibration config (creates default if None)
capture_template_id: Template ID for screen capture engine
"""
if device_id in self._processors:
raise ValueError(f"Device {device_id} already exists")
@@ -147,6 +152,7 @@ class ProcessorManager:
led_count=led_count,
settings=settings,
calibration=calibration,
capture_template_id=capture_template_id,
)
self._processors[device_id] = state
@@ -270,6 +276,21 @@ class ProcessorManager:
logger.error(f"Failed to connect to WLED device {device_id}: {e}")
raise RuntimeError(f"Failed to connect to WLED device: {e}")
# Initialize capture engine
# Phase 2: Use MSS engine for all devices (template integration in Phase 5)
try:
# For now, always use MSS engine (Phase 5 will load from template)
engine = EngineRegistry.create_engine("mss", {})
engine.initialize()
state.capture_engine = engine
logger.debug(f"Initialized capture engine for device {device_id}: mss")
except Exception as e:
logger.error(f"Failed to initialize capture engine for device {device_id}: {e}")
# Cleanup WLED client before raising
if state.wled_client:
await state.wled_client.disconnect()
raise RuntimeError(f"Failed to initialize capture engine: {e}")
# Initialize pixel mapper
state.pixel_mapper = PixelMapper(
state.calibration,
@@ -321,6 +342,11 @@ class ProcessorManager:
await state.wled_client.close()
state.wled_client = None
# Cleanup capture engine
if state.capture_engine:
state.capture_engine.cleanup()
state.capture_engine = None
logger.info(f"Stopped processing for device {device_id}")
async def _processing_loop(self, device_id: str):
@@ -351,8 +377,11 @@ class ProcessorManager:
try:
# Run blocking operations in thread pool to avoid blocking event loop
# Capture screen (blocking I/O)
capture = await asyncio.to_thread(capture_display, settings.display_index)
# Capture screen using engine (blocking I/O)
capture = await asyncio.to_thread(
state.capture_engine.capture_display,
settings.display_index
)
# Extract border pixels (CPU-intensive)
border_pixels = await asyncio.to_thread(extract_border_pixels, capture, settings.border_width)
@@ -725,3 +754,31 @@ class ProcessorManager:
"wled_led_type": h.wled_led_type,
"error": h.error,
}
def is_display_locked(self, display_index: int) -> bool:
"""Check if a display is currently being captured by any device.
Args:
display_index: Display index to check
Returns:
True if the display is actively being captured
"""
for state in self._processors.values():
if state.is_running and state.settings.display_index == display_index:
return True
return False
def get_display_lock_info(self, display_index: int) -> Optional[str]:
"""Get the device ID that is currently capturing from a display.
Args:
display_index: Display index to check
Returns:
Device ID if locked, None otherwise
"""
for device_id, state in self._processors.items():
if state.is_running and state.settings.display_index == display_index:
return device_id
return None