Add dynamic brightness value source support for KC targets, fix subtab selector collision

Extend value source brightness modulation to Key Colors targets (matching LED target support).
Also fix stream subtab CSS selector collision that broke target subtab selection, and use 🔢 emoji
for value source UI elements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 12:42:00 +03:00
parent ef474fe275
commit 8f79b77fe4
10 changed files with 131 additions and 13 deletions

View File

@@ -71,6 +71,7 @@ def _kc_settings_to_schema(settings: KeyColorsSettings) -> KeyColorsSettingsSche
smoothing=settings.smoothing, smoothing=settings.smoothing,
pattern_template_id=settings.pattern_template_id, pattern_template_id=settings.pattern_template_id,
brightness=settings.brightness, brightness=settings.brightness,
brightness_value_source_id=settings.brightness_value_source_id,
) )
@@ -82,6 +83,7 @@ def _kc_schema_to_settings(schema: KeyColorsSettingsSchema) -> KeyColorsSettings
smoothing=schema.smoothing, smoothing=schema.smoothing,
pattern_template_id=schema.pattern_template_id, pattern_template_id=schema.pattern_template_id,
brightness=schema.brightness, brightness=schema.brightness,
brightness_value_source_id=schema.brightness_value_source_id,
) )
@@ -254,6 +256,7 @@ async def update_target(
smoothing=incoming.get("smoothing", ex.smoothing), smoothing=incoming.get("smoothing", ex.smoothing),
pattern_template_id=incoming.get("pattern_template_id", ex.pattern_template_id), pattern_template_id=incoming.get("pattern_template_id", ex.pattern_template_id),
brightness=incoming.get("brightness", ex.brightness), brightness=incoming.get("brightness", ex.brightness),
brightness_value_source_id=incoming.get("brightness_value_source_id", ex.brightness_value_source_id),
) )
kc_settings = _kc_schema_to_settings(merged) kc_settings = _kc_schema_to_settings(merged)
else: else:
@@ -273,6 +276,13 @@ async def update_target(
description=data.description, description=data.description,
) )
# Detect KC brightness VS change (inside key_colors_settings)
kc_brightness_vs_changed = False
if data.key_colors_settings is not None:
kc_incoming = data.key_colors_settings.model_dump(exclude_unset=True)
if "brightness_value_source_id" in kc_incoming:
kc_brightness_vs_changed = True
# Sync processor manager # Sync processor manager
try: try:
target.sync_with_manager( target.sync_with_manager(
@@ -283,7 +293,7 @@ async def update_target(
data.key_colors_settings is not None), data.key_colors_settings is not None),
css_changed=data.color_strip_source_id is not None, css_changed=data.color_strip_source_id is not None,
device_changed=data.device_id is not None, device_changed=data.device_id is not None,
brightness_vs_changed=data.brightness_value_source_id is not None, brightness_vs_changed=(data.brightness_value_source_id is not None or kc_brightness_vs_changed),
) )
except ValueError: except ValueError:
pass pass

View File

@@ -26,6 +26,7 @@ class KeyColorsSettingsSchema(BaseModel):
smoothing: float = Field(default=0.3, description="Temporal smoothing (0.0-1.0)", ge=0.0, le=1.0) smoothing: float = Field(default=0.3, description="Temporal smoothing (0.0-1.0)", ge=0.0, le=1.0)
pattern_template_id: str = Field(default="", description="Pattern template ID for rectangle layout") pattern_template_id: str = Field(default="", description="Pattern template ID for rectangle layout")
brightness: float = Field(default=1.0, description="Output brightness (0.0-1.0)", ge=0.0, le=1.0) brightness: float = Field(default=1.0, description="Output brightness (0.0-1.0)", ge=0.0, le=1.0)
brightness_value_source_id: str = Field(default="", description="Brightness value source ID")
class ExtractedColorResponse(BaseModel): class ExtractedColorResponse(BaseModel):

View File

@@ -96,9 +96,11 @@ class KCTargetProcessor(TargetProcessor):
): ):
super().__init__(target_id, ctx, picture_source_id) super().__init__(target_id, ctx, picture_source_id)
self._settings = settings self._settings = settings
self._brightness_vs_id = settings.brightness_value_source_id if settings else ""
# Runtime state # Runtime state
self._live_stream: Optional[LiveStream] = None self._live_stream: Optional[LiveStream] = None
self._value_stream = None # active brightness value stream
self._previous_colors: Optional[Dict[str, Tuple[int, int, int]]] = None self._previous_colors: Optional[Dict[str, Tuple[int, int, int]]] = None
self._latest_colors: Optional[Dict[str, Tuple[int, int, int]]] = None self._latest_colors: Optional[Dict[str, Tuple[int, int, int]]] = None
self._ws_clients: List = [] self._ws_clients: List = []
@@ -156,6 +158,16 @@ class KCTargetProcessor(TargetProcessor):
logger.error(f"Failed to initialize live stream for KC target {self._target_id}: {e}") logger.error(f"Failed to initialize live stream for KC target {self._target_id}: {e}")
raise RuntimeError(f"Failed to initialize live stream: {e}") raise RuntimeError(f"Failed to initialize live stream: {e}")
# Acquire value stream for brightness modulation (if configured)
if self._brightness_vs_id and self._ctx.value_stream_manager:
try:
self._value_stream = self._ctx.value_stream_manager.acquire(
self._brightness_vs_id, self._target_id
)
except Exception as e:
logger.warning(f"Failed to acquire value stream {self._brightness_vs_id}: {e}")
self._value_stream = None
# Reset metrics # Reset metrics
self._metrics = ProcessingMetrics(start_time=datetime.utcnow()) self._metrics = ProcessingMetrics(start_time=datetime.utcnow())
self._previous_colors = None self._previous_colors = None
@@ -192,6 +204,14 @@ class KCTargetProcessor(TargetProcessor):
logger.warning(f"Error releasing live stream for KC target: {e}") logger.warning(f"Error releasing live stream for KC target: {e}")
self._live_stream = None self._live_stream = None
# Release value stream
if self._value_stream is not None and self._ctx.value_stream_manager:
try:
self._ctx.value_stream_manager.release(self._brightness_vs_id, self._target_id)
except Exception as e:
logger.warning(f"Error releasing value stream: {e}")
self._value_stream = None
logger.info(f"Stopped KC processing for target {self._target_id}") logger.info(f"Stopped KC processing for target {self._target_id}")
self._ctx.fire_event({"type": "state_change", "target_id": self._target_id, "processing": False}) self._ctx.fire_event({"type": "state_change", "target_id": self._target_id, "processing": False})
@@ -199,8 +219,37 @@ class KCTargetProcessor(TargetProcessor):
def update_settings(self, settings) -> None: def update_settings(self, settings) -> None:
self._settings = settings self._settings = settings
# Keep _brightness_vs_id in sync (hot-swap handled separately)
self._brightness_vs_id = settings.brightness_value_source_id if settings else ""
logger.info(f"Updated KC target settings: {self._target_id}") logger.info(f"Updated KC target settings: {self._target_id}")
def update_brightness_value_source(self, vs_id: str) -> None:
"""Hot-swap the brightness value source for a running KC target."""
old_vs_id = self._brightness_vs_id
self._brightness_vs_id = vs_id
vs_mgr = self._ctx.value_stream_manager
if not self._is_running or vs_mgr is None:
return
# Release old stream
if self._value_stream is not None and old_vs_id:
try:
vs_mgr.release(old_vs_id, self._target_id)
except Exception as e:
logger.warning(f"Error releasing old value stream {old_vs_id}: {e}")
self._value_stream = None
# Acquire new stream
if vs_id:
try:
self._value_stream = vs_mgr.acquire(vs_id, self._target_id)
except Exception as e:
logger.warning(f"Failed to acquire value stream {vs_id}: {e}")
self._value_stream = None
logger.info(f"Hot-swapped brightness VS for KC target {self._target_id}: {old_vs_id} -> {vs_id}")
# ----- State / Metrics ----- # ----- State / Metrics -----
def get_state(self) -> dict: def get_state(self) -> dict:
@@ -220,6 +269,7 @@ class KCTargetProcessor(TargetProcessor):
"timing_total_ms": round(metrics.timing_total_ms, 1) if self._is_running else None, "timing_total_ms": round(metrics.timing_total_ms, 1) if self._is_running else None,
"last_update": metrics.last_update, "last_update": metrics.last_update,
"errors": [metrics.last_error] if metrics.last_error else [], "errors": [metrics.last_error] if metrics.last_error else [],
"brightness_value_source_id": self._brightness_vs_id,
} }
def get_metrics(self) -> dict: def get_metrics(self) -> dict:
@@ -333,11 +383,17 @@ class KCTargetProcessor(TargetProcessor):
s = self._settings s = self._settings
calc_fn = calc_fns.get(s.interpolation_mode, calculate_average_color) calc_fn = calc_fns.get(s.interpolation_mode, calculate_average_color)
# Effective brightness: static setting * value stream
eff_brightness = s.brightness
vs = self._value_stream
if vs is not None:
eff_brightness *= vs.get_value()
# CPU-bound work in thread pool # CPU-bound work in thread pool
colors, colors_arr, frame_timing = await asyncio.to_thread( colors, colors_arr, frame_timing = await asyncio.to_thread(
_process_kc_frame, _process_kc_frame,
capture, rect_names, rect_bounds, calc_fn, capture, rect_names, rect_bounds, calc_fn,
prev_colors_arr, s.smoothing, s.brightness, prev_colors_arr, s.smoothing, eff_brightness,
) )
prev_colors_arr = colors_arr prev_colors_arr = colors_arr

View File

@@ -8,6 +8,7 @@ import {
_kcNameManuallyEdited, set_kcNameManuallyEdited, _kcNameManuallyEdited, set_kcNameManuallyEdited,
kcWebSockets, kcWebSockets,
PATTERN_RECT_BORDERS, PATTERN_RECT_BORDERS,
_cachedValueSources, set_cachedValueSources,
} from '../core/state.js'; } from '../core/state.js';
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml } from '../core/api.js'; import { API_BASE, getHeaders, fetchWithAuth, escapeHtml } from '../core/api.js';
import { t } from '../core/i18n.js'; import { t } from '../core/i18n.js';
@@ -27,6 +28,7 @@ class KCEditorModal extends Modal {
interpolation: document.getElementById('kc-editor-interpolation').value, interpolation: document.getElementById('kc-editor-interpolation').value,
smoothing: document.getElementById('kc-editor-smoothing').value, smoothing: document.getElementById('kc-editor-smoothing').value,
patternTemplateId: document.getElementById('kc-editor-pattern-template').value, patternTemplateId: document.getElementById('kc-editor-pattern-template').value,
brightness_vs: document.getElementById('kc-editor-brightness-vs').value,
}; };
} }
} }
@@ -348,15 +350,33 @@ function _autoGenerateKCName() {
document.getElementById('kc-editor-name').value = `${sourceName} \u00b7 ${patName} (${modeName})`; document.getElementById('kc-editor-name').value = `${sourceName} \u00b7 ${patName} (${modeName})`;
} }
function _populateKCBrightnessVsDropdown(selectedId = '') {
const sel = document.getElementById('kc-editor-brightness-vs');
// Keep the first "None" option, remove the rest
while (sel.options.length > 1) sel.remove(1);
_cachedValueSources.forEach(vs => {
const typeIcons = { static: '📊', animated: '🔄', audio: '🎵' };
const icon = typeIcons[vs.source_type] || '🔢';
const opt = document.createElement('option');
opt.value = vs.id;
opt.textContent = `${icon} ${vs.name}`;
sel.appendChild(opt);
});
sel.value = selectedId || '';
}
export async function showKCEditor(targetId = null, cloneData = null) { export async function showKCEditor(targetId = null, cloneData = null) {
try { try {
// Load sources and pattern templates in parallel // Load sources, pattern templates, and value sources in parallel
const [sourcesResp, patResp] = await Promise.all([ const [sourcesResp, patResp, vsResp] = await Promise.all([
fetchWithAuth('/picture-sources').catch(() => null), fetchWithAuth('/picture-sources').catch(() => null),
fetchWithAuth('/pattern-templates').catch(() => null), fetchWithAuth('/pattern-templates').catch(() => null),
fetchWithAuth('/value-sources').catch(() => null),
]); ]);
const sources = (sourcesResp && sourcesResp.ok) ? (await sourcesResp.json()).streams || [] : []; const sources = (sourcesResp && sourcesResp.ok) ? (await sourcesResp.json()).streams || [] : [];
const patTemplates = (patResp && patResp.ok) ? (await patResp.json()).templates || [] : []; const patTemplates = (patResp && patResp.ok) ? (await patResp.json()).templates || [] : [];
const valueSources = (vsResp && vsResp.ok) ? (await vsResp.json()).sources || [] : [];
set_cachedValueSources(valueSources);
// Populate source select // Populate source select
const sourceSelect = document.getElementById('kc-editor-source'); const sourceSelect = document.getElementById('kc-editor-source');
@@ -397,6 +417,7 @@ export async function showKCEditor(targetId = null, cloneData = null) {
document.getElementById('kc-editor-smoothing').value = kcSettings.smoothing ?? 0.3; document.getElementById('kc-editor-smoothing').value = kcSettings.smoothing ?? 0.3;
document.getElementById('kc-editor-smoothing-value').textContent = kcSettings.smoothing ?? 0.3; document.getElementById('kc-editor-smoothing-value').textContent = kcSettings.smoothing ?? 0.3;
patSelect.value = kcSettings.pattern_template_id || ''; patSelect.value = kcSettings.pattern_template_id || '';
_populateKCBrightnessVsDropdown(kcSettings.brightness_value_source_id || '');
document.getElementById('kc-editor-title').textContent = t('kc.edit'); document.getElementById('kc-editor-title').textContent = t('kc.edit');
} else if (cloneData) { } else if (cloneData) {
const kcSettings = cloneData.key_colors_settings || {}; const kcSettings = cloneData.key_colors_settings || {};
@@ -409,6 +430,7 @@ export async function showKCEditor(targetId = null, cloneData = null) {
document.getElementById('kc-editor-smoothing').value = kcSettings.smoothing ?? 0.3; document.getElementById('kc-editor-smoothing').value = kcSettings.smoothing ?? 0.3;
document.getElementById('kc-editor-smoothing-value').textContent = kcSettings.smoothing ?? 0.3; document.getElementById('kc-editor-smoothing-value').textContent = kcSettings.smoothing ?? 0.3;
patSelect.value = kcSettings.pattern_template_id || ''; patSelect.value = kcSettings.pattern_template_id || '';
_populateKCBrightnessVsDropdown(kcSettings.brightness_value_source_id || '');
document.getElementById('kc-editor-title').textContent = t('kc.add'); document.getElementById('kc-editor-title').textContent = t('kc.add');
} else { } else {
document.getElementById('kc-editor-id').value = ''; document.getElementById('kc-editor-id').value = '';
@@ -420,6 +442,7 @@ export async function showKCEditor(targetId = null, cloneData = null) {
document.getElementById('kc-editor-smoothing').value = 0.3; document.getElementById('kc-editor-smoothing').value = 0.3;
document.getElementById('kc-editor-smoothing-value').textContent = '0.3'; document.getElementById('kc-editor-smoothing-value').textContent = '0.3';
if (patTemplates.length > 0) patSelect.value = patTemplates[0].id; if (patTemplates.length > 0) patSelect.value = patTemplates[0].id;
_populateKCBrightnessVsDropdown('');
document.getElementById('kc-editor-title').textContent = t('kc.add'); document.getElementById('kc-editor-title').textContent = t('kc.add');
} }
@@ -464,6 +487,7 @@ export async function saveKCEditor() {
const interpolation = document.getElementById('kc-editor-interpolation').value; const interpolation = document.getElementById('kc-editor-interpolation').value;
const smoothing = parseFloat(document.getElementById('kc-editor-smoothing').value); const smoothing = parseFloat(document.getElementById('kc-editor-smoothing').value);
const patternTemplateId = document.getElementById('kc-editor-pattern-template').value; const patternTemplateId = document.getElementById('kc-editor-pattern-template').value;
const brightnessVsId = document.getElementById('kc-editor-brightness-vs').value;
if (!name) { if (!name) {
kcEditorModal.showError(t('kc.error.required')); kcEditorModal.showError(t('kc.error.required'));
@@ -483,6 +507,7 @@ export async function saveKCEditor() {
interpolation_mode: interpolation, interpolation_mode: interpolation,
smoothing, smoothing,
pattern_template_id: patternTemplateId, pattern_template_id: patternTemplateId,
brightness_value_source_id: brightnessVsId,
}, },
}; };

View File

@@ -492,10 +492,10 @@ export async function loadPictureSources() {
} }
export function switchStreamTab(tabKey) { export function switchStreamTab(tabKey) {
document.querySelectorAll('.stream-tab-btn').forEach(btn => document.querySelectorAll('.stream-tab-btn[data-stream-tab]').forEach(btn =>
btn.classList.toggle('active', btn.dataset.streamTab === tabKey) btn.classList.toggle('active', btn.dataset.streamTab === tabKey)
); );
document.querySelectorAll('.stream-tab-panel').forEach(panel => document.querySelectorAll('.stream-tab-panel[id^="stream-tab-"]').forEach(panel =>
panel.classList.toggle('active', panel.id === `stream-tab-${tabKey}`) panel.classList.toggle('active', panel.id === `stream-tab-${tabKey}`)
); );
localStorage.setItem('activeStreamTab', tabKey); localStorage.setItem('activeStreamTab', tabKey);
@@ -629,7 +629,7 @@ function renderPictureSourcesList(streams) {
{ key: 'static_image', icon: '🖼️', titleKey: 'streams.group.static_image', count: staticImageStreams.length }, { key: 'static_image', icon: '🖼️', titleKey: 'streams.group.static_image', count: staticImageStreams.length },
{ key: 'processed', icon: '🎨', titleKey: 'streams.group.processed', count: processedStreams.length }, { key: 'processed', icon: '🎨', titleKey: 'streams.group.processed', count: processedStreams.length },
{ key: 'audio', icon: '🔊', titleKey: 'streams.group.audio', count: _cachedAudioSources.length }, { key: 'audio', icon: '🔊', titleKey: 'streams.group.audio', count: _cachedAudioSources.length },
{ key: 'value', icon: '🎚️', titleKey: 'streams.group.value', count: _cachedValueSources.length }, { key: 'value', icon: '🔢', titleKey: 'streams.group.value', count: _cachedValueSources.length },
]; ];
const tabBar = `<div class="stream-tab-bar">${tabs.map(tab => const tabBar = `<div class="stream-tab-bar">${tabs.map(tab =>

View File

@@ -424,6 +424,9 @@
"kc.pattern_template": "Pattern Template:", "kc.pattern_template": "Pattern Template:",
"kc.pattern_template.hint": "Select the rectangle pattern to use for color extraction", "kc.pattern_template.hint": "Select the rectangle pattern to use for color extraction",
"kc.pattern_template.none": "-- Select a pattern template --", "kc.pattern_template.none": "-- Select a pattern template --",
"kc.brightness_vs": "🔢 Brightness Source:",
"kc.brightness_vs.hint": "Optional value source that dynamically controls brightness each frame (multiplied with the manual brightness slider)",
"kc.brightness_vs.none": "None (manual brightness only)",
"kc.created": "Key colors target created successfully", "kc.created": "Key colors target created successfully",
"kc.updated": "Key colors target updated successfully", "kc.updated": "Key colors target updated successfully",
"kc.deleted": "Key colors target deleted successfully", "kc.deleted": "Key colors target deleted successfully",
@@ -763,7 +766,7 @@
"audio_source.error.name_required": "Please enter a name", "audio_source.error.name_required": "Please enter a name",
"streams.group.value": "Value Sources", "streams.group.value": "Value Sources",
"value_source.group.title": "🎚️ Value Sources", "value_source.group.title": "🔢 Value Sources",
"value_source.add": "Add Value Source", "value_source.add": "Add Value Source",
"value_source.edit": "Edit Value Source", "value_source.edit": "Edit Value Source",
"value_source.name": "Name:", "value_source.name": "Name:",
@@ -807,7 +810,7 @@
"value_source.deleted": "Value source deleted", "value_source.deleted": "Value source deleted",
"value_source.delete.confirm": "Are you sure you want to delete this value source?", "value_source.delete.confirm": "Are you sure you want to delete this value source?",
"value_source.error.name_required": "Please enter a name", "value_source.error.name_required": "Please enter a name",
"targets.brightness_vs": "Brightness Source:", "targets.brightness_vs": "🔢 Brightness Source:",
"targets.brightness_vs.hint": "Optional value source that dynamically controls brightness each frame (overrides device brightness)", "targets.brightness_vs.hint": "Optional value source that dynamically controls brightness each frame (overrides device brightness)",
"targets.brightness_vs.none": "None (device brightness)" "targets.brightness_vs.none": "None (device brightness)"
} }

View File

@@ -424,6 +424,9 @@
"kc.pattern_template": "Шаблон Паттерна:", "kc.pattern_template": "Шаблон Паттерна:",
"kc.pattern_template.hint": "Выберите шаблон прямоугольников для извлечения цветов", "kc.pattern_template.hint": "Выберите шаблон прямоугольников для извлечения цветов",
"kc.pattern_template.none": "-- Выберите шаблон паттерна --", "kc.pattern_template.none": "-- Выберите шаблон паттерна --",
"kc.brightness_vs": "🔢 Источник Яркости:",
"kc.brightness_vs.hint": "Опциональный источник значений, динамически управляющий яркостью каждый кадр (умножается на ручной слайдер яркости)",
"kc.brightness_vs.none": "Нет (только ручная яркость)",
"kc.created": "Цель ключевых цветов успешно создана", "kc.created": "Цель ключевых цветов успешно создана",
"kc.updated": "Цель ключевых цветов успешно обновлена", "kc.updated": "Цель ключевых цветов успешно обновлена",
"kc.deleted": "Цель ключевых цветов успешно удалена", "kc.deleted": "Цель ключевых цветов успешно удалена",
@@ -763,7 +766,7 @@
"audio_source.error.name_required": "Введите название", "audio_source.error.name_required": "Введите название",
"streams.group.value": "Источники значений", "streams.group.value": "Источники значений",
"value_source.group.title": "🎚️ Источники значений", "value_source.group.title": "🔢 Источники значений",
"value_source.add": "Добавить источник значений", "value_source.add": "Добавить источник значений",
"value_source.edit": "Редактировать источник значений", "value_source.edit": "Редактировать источник значений",
"value_source.name": "Название:", "value_source.name": "Название:",
@@ -807,7 +810,7 @@
"value_source.deleted": "Источник значений удалён", "value_source.deleted": "Источник значений удалён",
"value_source.delete.confirm": "Удалить этот источник значений?", "value_source.delete.confirm": "Удалить этот источник значений?",
"value_source.error.name_required": "Введите название", "value_source.error.name_required": "Введите название",
"targets.brightness_vs": "Источник яркости:", "targets.brightness_vs": "🔢 Источник яркости:",
"targets.brightness_vs.hint": "Необязательный источник значений для динамического управления яркостью каждый кадр (переопределяет яркость устройства)", "targets.brightness_vs.hint": "Необязательный источник значений для динамического управления яркостью каждый кадр (переопределяет яркость устройства)",
"targets.brightness_vs.none": "Нет (яркость устройства)" "targets.brightness_vs.none": "Нет (яркость устройства)"
} }

View File

@@ -46,6 +46,7 @@ class KeyColorsSettings:
smoothing: float = 0.3 smoothing: float = 0.3
pattern_template_id: str = "" pattern_template_id: str = ""
brightness: float = 1.0 brightness: float = 1.0
brightness_value_source_id: str = ""
def to_dict(self) -> dict: def to_dict(self) -> dict:
return { return {
@@ -54,6 +55,7 @@ class KeyColorsSettings:
"smoothing": self.smoothing, "smoothing": self.smoothing,
"pattern_template_id": self.pattern_template_id, "pattern_template_id": self.pattern_template_id,
"brightness": self.brightness, "brightness": self.brightness,
"brightness_value_source_id": self.brightness_value_source_id,
} }
@classmethod @classmethod
@@ -64,6 +66,7 @@ class KeyColorsSettings:
smoothing=data.get("smoothing", 0.3), smoothing=data.get("smoothing", 0.3),
pattern_template_id=data.get("pattern_template_id", ""), pattern_template_id=data.get("pattern_template_id", ""),
brightness=data.get("brightness", 1.0), brightness=data.get("brightness", 1.0),
brightness_value_source_id=data.get("brightness_value_source_id", ""),
) )
@@ -82,12 +85,18 @@ class KeyColorsPictureTarget(PictureTarget):
settings=self.settings, settings=self.settings,
) )
def sync_with_manager(self, manager, *, settings_changed: bool, source_changed: bool, device_changed: bool) -> None: def sync_with_manager(self, manager, *, settings_changed: bool,
source_changed: bool = False,
css_changed: bool = False,
device_changed: bool = False,
brightness_vs_changed: bool = False) -> None:
"""Push changed fields to the processor manager.""" """Push changed fields to the processor manager."""
if settings_changed: if settings_changed:
manager.update_target_settings(self.id, self.settings) manager.update_target_settings(self.id, self.settings)
if source_changed: if source_changed:
manager.update_target_source(self.id, self.picture_source_id) manager.update_target_source(self.id, self.picture_source_id)
if brightness_vs_changed:
manager.update_target_brightness_vs(self.id, self.settings.brightness_value_source_id)
def update_fields(self, *, name=None, device_id=None, picture_source_id=None, def update_fields(self, *, name=None, device_id=None, picture_source_id=None,
settings=None, key_colors_settings=None, description=None, settings=None, key_colors_settings=None, description=None,

View File

@@ -32,6 +32,17 @@
<select id="kc-editor-pattern-template"></select> <select id="kc-editor-pattern-template"></select>
</div> </div>
<div class="form-group">
<div class="label-row">
<label for="kc-editor-brightness-vs" data-i18n="kc.brightness_vs">🔢 Brightness Source:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="kc.brightness_vs.hint">Optional value source that dynamically controls brightness each frame (multiplied with the manual brightness slider)</small>
<select id="kc-editor-brightness-vs">
<option value="" data-i18n="kc.brightness_vs.none">None (manual brightness only)</option>
</select>
</div>
<div class="form-group"> <div class="form-group">
<div class="label-row"> <div class="label-row">
<label for="kc-editor-fps" data-i18n="kc.fps">Extraction FPS:</label> <label for="kc-editor-fps" data-i18n="kc.fps">Extraction FPS:</label>

View File

@@ -35,7 +35,7 @@
<div class="form-group"> <div class="form-group">
<div class="label-row"> <div class="label-row">
<label for="target-editor-brightness-vs" data-i18n="targets.brightness_vs">Brightness Source:</label> <label for="target-editor-brightness-vs" data-i18n="targets.brightness_vs">🔢 Brightness Source:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button> <button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div> </div>
<small class="input-hint" style="display:none" data-i18n="targets.brightness_vs.hint">Optional value source that dynamically controls brightness each frame (overrides device brightness)</small> <small class="input-hint" style="display:none" data-i18n="targets.brightness_vs.hint">Optional value source that dynamically controls brightness each frame (overrides device brightness)</small>