Add per-layer brightness source to composite CSS and enhance selectors
- Add optional brightness_source_id per composite layer using ValueStreamManager - Use EntitySelect for composite layer source and brightness dropdowns - Use IconSelect for composite blend mode and notification filter mode - Add i18n keys for blend mode and filter mode descriptions (en/ru/zh) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,7 @@ class CompositeLayer(BaseModel):
|
|||||||
blend_mode: str = Field(default="normal", description="Blend mode: normal|add|multiply|screen")
|
blend_mode: str = Field(default="normal", description="Blend mode: normal|add|multiply|screen")
|
||||||
opacity: float = Field(default=1.0, ge=0.0, le=1.0, description="Layer opacity 0.0-1.0")
|
opacity: float = Field(default=1.0, ge=0.0, le=1.0, description="Layer opacity 0.0-1.0")
|
||||||
enabled: bool = Field(default=True, description="Whether this layer is active")
|
enabled: bool = Field(default=True, description="Whether this layer is active")
|
||||||
|
brightness_source_id: Optional[str] = Field(None, description="Optional value source ID for dynamic brightness")
|
||||||
|
|
||||||
|
|
||||||
class MappedZone(BaseModel):
|
class MappedZone(BaseModel):
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ class ColorStripStreamManager:
|
|||||||
keyed by ``{css_id}:{consumer_id}``.
|
keyed by ``{css_id}:{consumer_id}``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, color_strip_store, live_stream_manager, audio_capture_manager=None, audio_source_store=None, audio_template_store=None, sync_clock_manager=None):
|
def __init__(self, color_strip_store, live_stream_manager, audio_capture_manager=None, audio_source_store=None, audio_template_store=None, sync_clock_manager=None, value_stream_manager=None):
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
color_strip_store: ColorStripStore for resolving source configs
|
color_strip_store: ColorStripStore for resolving source configs
|
||||||
@@ -76,6 +76,7 @@ class ColorStripStreamManager:
|
|||||||
audio_capture_manager: AudioCaptureManager for audio-reactive sources
|
audio_capture_manager: AudioCaptureManager for audio-reactive sources
|
||||||
audio_source_store: AudioSourceStore for resolving audio source chains
|
audio_source_store: AudioSourceStore for resolving audio source chains
|
||||||
sync_clock_manager: SyncClockManager for acquiring clock runtimes
|
sync_clock_manager: SyncClockManager for acquiring clock runtimes
|
||||||
|
value_stream_manager: ValueStreamManager for per-layer brightness sources
|
||||||
"""
|
"""
|
||||||
self._color_strip_store = color_strip_store
|
self._color_strip_store = color_strip_store
|
||||||
self._live_stream_manager = live_stream_manager
|
self._live_stream_manager = live_stream_manager
|
||||||
@@ -83,6 +84,7 @@ class ColorStripStreamManager:
|
|||||||
self._audio_source_store = audio_source_store
|
self._audio_source_store = audio_source_store
|
||||||
self._audio_template_store = audio_template_store
|
self._audio_template_store = audio_template_store
|
||||||
self._sync_clock_manager = sync_clock_manager
|
self._sync_clock_manager = sync_clock_manager
|
||||||
|
self._value_stream_manager = value_stream_manager
|
||||||
self._streams: Dict[str, _ColorStripEntry] = {}
|
self._streams: Dict[str, _ColorStripEntry] = {}
|
||||||
|
|
||||||
def _inject_clock(self, css_stream, source) -> Optional[str]:
|
def _inject_clock(self, css_stream, source) -> Optional[str]:
|
||||||
@@ -159,7 +161,7 @@ class ColorStripStreamManager:
|
|||||||
css_stream = AudioColorStripStream(source, self._audio_capture_manager, self._audio_source_store, self._audio_template_store)
|
css_stream = AudioColorStripStream(source, self._audio_capture_manager, self._audio_source_store, self._audio_template_store)
|
||||||
elif source.source_type == "composite":
|
elif source.source_type == "composite":
|
||||||
from wled_controller.core.processing.composite_stream import CompositeColorStripStream
|
from wled_controller.core.processing.composite_stream import CompositeColorStripStream
|
||||||
css_stream = CompositeColorStripStream(source, self)
|
css_stream = CompositeColorStripStream(source, self, self._value_stream_manager)
|
||||||
elif source.source_type == "mapped":
|
elif source.source_type == "mapped":
|
||||||
from wled_controller.core.processing.mapped_stream import MappedColorStripStream
|
from wled_controller.core.processing.mapped_stream import MappedColorStripStream
|
||||||
css_stream = MappedColorStripStream(source, self)
|
css_stream = MappedColorStripStream(source, self)
|
||||||
|
|||||||
@@ -29,12 +29,13 @@ class CompositeColorStripStream(ColorStripStream):
|
|||||||
sub-stream's latest colors and blending bottom-to-top.
|
sub-stream's latest colors and blending bottom-to-top.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, source, css_manager):
|
def __init__(self, source, css_manager, value_stream_manager=None):
|
||||||
self._source_id: str = source.id
|
self._source_id: str = source.id
|
||||||
self._layers: List[dict] = list(source.layers)
|
self._layers: List[dict] = list(source.layers)
|
||||||
self._led_count: int = source.led_count
|
self._led_count: int = source.led_count
|
||||||
self._auto_size: bool = source.led_count == 0
|
self._auto_size: bool = source.led_count == 0
|
||||||
self._css_manager = css_manager
|
self._css_manager = css_manager
|
||||||
|
self._value_stream_manager = value_stream_manager
|
||||||
self._fps: int = 30
|
self._fps: int = 30
|
||||||
self._frame_time: float = 1.0 / 30
|
self._frame_time: float = 1.0 / 30
|
||||||
|
|
||||||
@@ -45,7 +46,9 @@ class CompositeColorStripStream(ColorStripStream):
|
|||||||
|
|
||||||
# layer_index -> (source_id, consumer_id, stream)
|
# layer_index -> (source_id, consumer_id, stream)
|
||||||
self._sub_streams: Dict[int, tuple] = {}
|
self._sub_streams: Dict[int, tuple] = {}
|
||||||
self._sub_lock = threading.Lock() # guards _sub_streams access across threads
|
# layer_index -> (vs_id, value_stream)
|
||||||
|
self._brightness_streams: Dict[int, tuple] = {}
|
||||||
|
self._sub_lock = threading.Lock() # guards _sub_streams and _brightness_streams
|
||||||
|
|
||||||
# Pre-allocated scratch (rebuilt when LED count changes)
|
# Pre-allocated scratch (rebuilt when LED count changes)
|
||||||
self._pool_n = 0
|
self._pool_n = 0
|
||||||
@@ -115,9 +118,9 @@ class CompositeColorStripStream(ColorStripStream):
|
|||||||
def update_source(self, source) -> None:
|
def update_source(self, source) -> None:
|
||||||
"""Hot-update: rebuild sub-streams if layer config changed."""
|
"""Hot-update: rebuild sub-streams if layer config changed."""
|
||||||
new_layers = list(source.layers)
|
new_layers = list(source.layers)
|
||||||
old_layer_ids = [(l.get("source_id"), l.get("blend_mode"), l.get("opacity"), l.get("enabled"))
|
old_layer_ids = [(l.get("source_id"), l.get("blend_mode"), l.get("opacity"), l.get("enabled"), l.get("brightness_source_id"))
|
||||||
for l in self._layers]
|
for l in self._layers]
|
||||||
new_layer_ids = [(l.get("source_id"), l.get("blend_mode"), l.get("opacity"), l.get("enabled"))
|
new_layer_ids = [(l.get("source_id"), l.get("blend_mode"), l.get("opacity"), l.get("enabled"), l.get("brightness_source_id"))
|
||||||
for l in new_layers]
|
for l in new_layers]
|
||||||
|
|
||||||
self._layers = new_layers
|
self._layers = new_layers
|
||||||
@@ -152,6 +155,16 @@ class CompositeColorStripStream(ColorStripStream):
|
|||||||
logger.warning(
|
logger.warning(
|
||||||
f"Composite layer {i} (source {src_id}) failed to acquire: {e}"
|
f"Composite layer {i} (source {src_id}) failed to acquire: {e}"
|
||||||
)
|
)
|
||||||
|
# Acquire brightness value stream if configured
|
||||||
|
vs_id = layer.get("brightness_source_id")
|
||||||
|
if vs_id and self._value_stream_manager:
|
||||||
|
try:
|
||||||
|
vs = self._value_stream_manager.acquire(vs_id)
|
||||||
|
self._brightness_streams[i] = (vs_id, vs)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
f"Composite layer {i} brightness source {vs_id} failed: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
def _release_sub_streams(self) -> None:
|
def _release_sub_streams(self) -> None:
|
||||||
for _idx, (src_id, consumer_id, _stream) in list(self._sub_streams.items()):
|
for _idx, (src_id, consumer_id, _stream) in list(self._sub_streams.items()):
|
||||||
@@ -160,6 +173,14 @@ class CompositeColorStripStream(ColorStripStream):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Composite layer release error ({src_id}): {e}")
|
logger.warning(f"Composite layer release error ({src_id}): {e}")
|
||||||
self._sub_streams.clear()
|
self._sub_streams.clear()
|
||||||
|
# Release brightness value streams
|
||||||
|
if self._value_stream_manager:
|
||||||
|
for _idx, (vs_id, _vs) in list(self._brightness_streams.items()):
|
||||||
|
try:
|
||||||
|
self._value_stream_manager.release(vs_id)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Composite brightness release error ({vs_id}): {e}")
|
||||||
|
self._brightness_streams.clear()
|
||||||
|
|
||||||
# ── Scratch pool ────────────────────────────────────────────
|
# ── Scratch pool ────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -299,6 +320,13 @@ class CompositeColorStripStream(ColorStripStream):
|
|||||||
if len(colors) != target_n:
|
if len(colors) != target_n:
|
||||||
colors = self._resize_to_target(colors, target_n)
|
colors = self._resize_to_target(colors, target_n)
|
||||||
|
|
||||||
|
# Apply per-layer brightness from value source
|
||||||
|
if i in self._brightness_streams:
|
||||||
|
_vs_id, vs = self._brightness_streams[i]
|
||||||
|
bri = vs.get_value()
|
||||||
|
if bri < 1.0:
|
||||||
|
colors = (colors.astype(np.uint16) * int(bri * 256) >> 8).astype(np.uint8)
|
||||||
|
|
||||||
opacity = layer.get("opacity", 1.0)
|
opacity = layer.get("opacity", 1.0)
|
||||||
blend_mode = layer.get("blend_mode", _BLEND_NORMAL)
|
blend_mode = layer.get("blend_mode", _BLEND_NORMAL)
|
||||||
alpha = int(opacity * 256)
|
alpha = int(opacity * 256)
|
||||||
|
|||||||
@@ -122,6 +122,8 @@ class ProcessorManager:
|
|||||||
live_stream_manager=self._live_stream_manager,
|
live_stream_manager=self._live_stream_manager,
|
||||||
audio_template_store=audio_template_store,
|
audio_template_store=audio_template_store,
|
||||||
) if value_source_store else None
|
) if value_source_store else None
|
||||||
|
# Wire value stream manager into CSS stream manager for composite layer brightness
|
||||||
|
self._color_strip_stream_manager._value_stream_manager = self._value_stream_manager
|
||||||
self._overlay_manager = OverlayManager()
|
self._overlay_manager = OverlayManager()
|
||||||
self._event_queues: List[asyncio.Queue] = []
|
self._event_queues: List[asyncio.Queue] = []
|
||||||
self._metrics_history = MetricsHistory(self)
|
self._metrics_history = MetricsHistory(self)
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { fetchWithAuth, escapeHtml } from '../core/api.js';
|
import { fetchWithAuth, escapeHtml } from '../core/api.js';
|
||||||
import { _cachedSyncClocks, audioSourcesCache, streamsCache, colorStripSourcesCache } from '../core/state.js';
|
import { _cachedSyncClocks, _cachedValueSources, audioSourcesCache, streamsCache, colorStripSourcesCache, valueSourcesCache } from '../core/state.js';
|
||||||
import { t } from '../core/i18n.js';
|
import { t } from '../core/i18n.js';
|
||||||
import { showToast, showConfirm } from '../core/ui.js';
|
import { showToast, showConfirm } from '../core/ui.js';
|
||||||
import { Modal } from '../core/modal.js';
|
import { Modal } from '../core/modal.js';
|
||||||
import {
|
import {
|
||||||
getColorStripIcon, getPictureSourceIcon, getAudioSourceIcon,
|
getColorStripIcon, getPictureSourceIcon, getAudioSourceIcon, getValueSourceIcon,
|
||||||
ICON_CLONE, ICON_EDIT, ICON_CALIBRATION,
|
ICON_CLONE, ICON_EDIT, ICON_CALIBRATION,
|
||||||
ICON_LED, ICON_PALETTE, ICON_FPS, ICON_MAP_PIN, ICON_MUSIC,
|
ICON_LED, ICON_PALETTE, ICON_FPS, ICON_MAP_PIN, ICON_MUSIC,
|
||||||
ICON_AUDIO_LOOPBACK, ICON_TIMER, ICON_LINK_SOURCE, ICON_FILM,
|
ICON_AUDIO_LOOPBACK, ICON_TIMER, ICON_LINK_SOURCE, ICON_FILM,
|
||||||
@@ -36,6 +36,7 @@ class CSSEditorModal extends Modal {
|
|||||||
|
|
||||||
onForceClose() {
|
onForceClose() {
|
||||||
if (_cssTagsInput) { _cssTagsInput.destroy(); _cssTagsInput = null; }
|
if (_cssTagsInput) { _cssTagsInput.destroy(); _cssTagsInput = null; }
|
||||||
|
_compositeDestroyEntitySelects();
|
||||||
}
|
}
|
||||||
|
|
||||||
snapshotValues() {
|
snapshotValues() {
|
||||||
@@ -170,7 +171,10 @@ export function onCSSTypeChange() {
|
|||||||
onAudioVizChange();
|
onAudioVizChange();
|
||||||
}
|
}
|
||||||
if (type === 'gradient') _ensureGradientPresetIconSelect();
|
if (type === 'gradient') _ensureGradientPresetIconSelect();
|
||||||
if (type === 'notification') _ensureNotificationEffectIconSelect();
|
if (type === 'notification') {
|
||||||
|
_ensureNotificationEffectIconSelect();
|
||||||
|
_ensureNotificationFilterModeIconSelect();
|
||||||
|
}
|
||||||
|
|
||||||
// Animation section — shown for static/gradient only
|
// Animation section — shown for static/gradient only
|
||||||
const animSection = document.getElementById('css-editor-animation-section');
|
const animSection = document.getElementById('css-editor-animation-section');
|
||||||
@@ -319,6 +323,7 @@ let _audioPaletteIconSelect = null;
|
|||||||
let _audioVizIconSelect = null;
|
let _audioVizIconSelect = null;
|
||||||
let _gradientPresetIconSelect = null;
|
let _gradientPresetIconSelect = null;
|
||||||
let _notificationEffectIconSelect = null;
|
let _notificationEffectIconSelect = null;
|
||||||
|
let _notificationFilterModeIconSelect = null;
|
||||||
|
|
||||||
const _icon = (d) => `<svg class="icon" viewBox="0 0 24 24">${d}</svg>`;
|
const _icon = (d) => `<svg class="icon" viewBox="0 0 24 24">${d}</svg>`;
|
||||||
|
|
||||||
@@ -405,6 +410,18 @@ function _ensureNotificationEffectIconSelect() {
|
|||||||
_notificationEffectIconSelect = new IconSelect({ target: sel, items, columns: 3 });
|
_notificationEffectIconSelect = new IconSelect({ target: sel, items, columns: 3 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _ensureNotificationFilterModeIconSelect() {
|
||||||
|
const sel = document.getElementById('css-editor-notification-filter-mode');
|
||||||
|
if (!sel) return;
|
||||||
|
const items = [
|
||||||
|
{ value: 'off', icon: _icon(P.globe), label: t('color_strip.notification.filter_mode.off'), desc: t('color_strip.notification.filter_mode.off.desc') },
|
||||||
|
{ value: 'whitelist', icon: _icon(P.circleCheck), label: t('color_strip.notification.filter_mode.whitelist'), desc: t('color_strip.notification.filter_mode.whitelist.desc') },
|
||||||
|
{ value: 'blacklist', icon: _icon(P.eyeOff), label: t('color_strip.notification.filter_mode.blacklist'), desc: t('color_strip.notification.filter_mode.blacklist.desc') },
|
||||||
|
];
|
||||||
|
if (_notificationFilterModeIconSelect) { _notificationFilterModeIconSelect.updateItems(items); return; }
|
||||||
|
_notificationFilterModeIconSelect = new IconSelect({ target: sel, items, columns: 3 });
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Effect type helpers ──────────────────────────────────────── */
|
/* ── Effect type helpers ──────────────────────────────────────── */
|
||||||
|
|
||||||
// Palette color control points — mirrors _PALETTE_DEFS in effect_stream.py
|
// Palette color control points — mirrors _PALETTE_DEFS in effect_stream.py
|
||||||
@@ -504,14 +521,57 @@ function _loadColorCycleState(css) {
|
|||||||
|
|
||||||
let _compositeLayers = [];
|
let _compositeLayers = [];
|
||||||
let _compositeAvailableSources = []; // non-composite sources for layer dropdowns
|
let _compositeAvailableSources = []; // non-composite sources for layer dropdowns
|
||||||
|
let _compositeSourceEntitySelects = [];
|
||||||
|
let _compositeBrightnessEntitySelects = [];
|
||||||
|
let _compositeBlendIconSelects = [];
|
||||||
|
|
||||||
|
function _compositeDestroyEntitySelects() {
|
||||||
|
_compositeSourceEntitySelects.forEach(es => es.destroy());
|
||||||
|
_compositeSourceEntitySelects = [];
|
||||||
|
_compositeBrightnessEntitySelects.forEach(es => es.destroy());
|
||||||
|
_compositeBrightnessEntitySelects = [];
|
||||||
|
_compositeBlendIconSelects.forEach(is => is.destroy());
|
||||||
|
_compositeBlendIconSelects = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getCompositeBlendItems() {
|
||||||
|
return [
|
||||||
|
{ value: 'normal', icon: _icon(P.square), label: t('color_strip.composite.blend_mode.normal'), desc: t('color_strip.composite.blend_mode.normal.desc') },
|
||||||
|
{ value: 'add', icon: _icon(P.sun), label: t('color_strip.composite.blend_mode.add'), desc: t('color_strip.composite.blend_mode.add.desc') },
|
||||||
|
{ value: 'multiply', icon: _icon(P.eye), label: t('color_strip.composite.blend_mode.multiply'), desc: t('color_strip.composite.blend_mode.multiply.desc') },
|
||||||
|
{ value: 'screen', icon: _icon(P.monitor), label: t('color_strip.composite.blend_mode.screen'), desc: t('color_strip.composite.blend_mode.screen.desc') },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getCompositeSourceItems() {
|
||||||
|
return _compositeAvailableSources.map(s => ({
|
||||||
|
value: s.id,
|
||||||
|
label: s.name,
|
||||||
|
icon: getColorStripIcon(s.source_type),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getCompositeBrightnessItems() {
|
||||||
|
return (_cachedValueSources || []).map(v => ({
|
||||||
|
value: v.id,
|
||||||
|
label: v.name,
|
||||||
|
icon: getValueSourceIcon(v.source_type),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
function _compositeRenderList() {
|
function _compositeRenderList() {
|
||||||
const list = document.getElementById('composite-layers-list');
|
const list = document.getElementById('composite-layers-list');
|
||||||
if (!list) return;
|
if (!list) return;
|
||||||
|
_compositeDestroyEntitySelects();
|
||||||
|
const vsList = _cachedValueSources || [];
|
||||||
list.innerHTML = _compositeLayers.map((layer, i) => {
|
list.innerHTML = _compositeLayers.map((layer, i) => {
|
||||||
const srcOptions = _compositeAvailableSources.map(s =>
|
const srcOptions = _compositeAvailableSources.map(s =>
|
||||||
`<option value="${s.id}"${layer.source_id === s.id ? ' selected' : ''}>${escapeHtml(s.name)}</option>`
|
`<option value="${s.id}"${layer.source_id === s.id ? ' selected' : ''}>${escapeHtml(s.name)}</option>`
|
||||||
).join('');
|
).join('');
|
||||||
|
const vsOptions = `<option value="">${t('color_strip.composite.brightness.none')}</option>` +
|
||||||
|
vsList.map(v =>
|
||||||
|
`<option value="${v.id}"${layer.brightness_source_id === v.id ? ' selected' : ''}>${escapeHtml(v.name)}</option>`
|
||||||
|
).join('');
|
||||||
const canRemove = _compositeLayers.length > 1;
|
const canRemove = _compositeLayers.length > 1;
|
||||||
return `
|
return `
|
||||||
<div class="composite-layer-item">
|
<div class="composite-layer-item">
|
||||||
@@ -540,6 +600,12 @@ function _compositeRenderList() {
|
|||||||
onclick="compositeRemoveLayer(${i})">✕</button>`
|
onclick="compositeRemoveLayer(${i})">✕</button>`
|
||||||
: ''}
|
: ''}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="composite-layer-row">
|
||||||
|
<label class="composite-layer-brightness-label">
|
||||||
|
<span>${t('color_strip.composite.brightness')}:</span>
|
||||||
|
</label>
|
||||||
|
<select class="composite-layer-brightness" data-idx="${i}">${vsOptions}</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
@@ -551,6 +617,33 @@ function _compositeRenderList() {
|
|||||||
el.closest('.composite-layer-row').querySelector('.composite-opacity-val').textContent = val.toFixed(2);
|
el.closest('.composite-layer-row').querySelector('.composite-opacity-val').textContent = val.toFixed(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Attach IconSelect to each layer's blend mode dropdown
|
||||||
|
const blendItems = _getCompositeBlendItems();
|
||||||
|
list.querySelectorAll('.composite-layer-blend').forEach(sel => {
|
||||||
|
const is = new IconSelect({ target: sel, items: blendItems, columns: 2 });
|
||||||
|
_compositeBlendIconSelects.push(is);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Attach EntitySelect to each layer's source dropdown
|
||||||
|
list.querySelectorAll('.composite-layer-source').forEach(sel => {
|
||||||
|
_compositeSourceEntitySelects.push(new EntitySelect({
|
||||||
|
target: sel,
|
||||||
|
getItems: _getCompositeSourceItems,
|
||||||
|
placeholder: t('palette.search'),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Attach EntitySelect to each layer's brightness dropdown
|
||||||
|
list.querySelectorAll('.composite-layer-brightness').forEach(sel => {
|
||||||
|
_compositeBrightnessEntitySelects.push(new EntitySelect({
|
||||||
|
target: sel,
|
||||||
|
getItems: _getCompositeBrightnessItems,
|
||||||
|
placeholder: t('palette.search'),
|
||||||
|
allowNone: true,
|
||||||
|
noneLabel: t('color_strip.composite.brightness.none'),
|
||||||
|
}));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function compositeAddLayer() {
|
export function compositeAddLayer() {
|
||||||
@@ -560,6 +653,7 @@ export function compositeAddLayer() {
|
|||||||
blend_mode: 'normal',
|
blend_mode: 'normal',
|
||||||
opacity: 1.0,
|
opacity: 1.0,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
brightness_source_id: null,
|
||||||
});
|
});
|
||||||
_compositeRenderList();
|
_compositeRenderList();
|
||||||
}
|
}
|
||||||
@@ -578,24 +672,30 @@ function _compositeLayersSyncFromDom() {
|
|||||||
const blends = list.querySelectorAll('.composite-layer-blend');
|
const blends = list.querySelectorAll('.composite-layer-blend');
|
||||||
const opacities = list.querySelectorAll('.composite-layer-opacity');
|
const opacities = list.querySelectorAll('.composite-layer-opacity');
|
||||||
const enableds = list.querySelectorAll('.composite-layer-enabled');
|
const enableds = list.querySelectorAll('.composite-layer-enabled');
|
||||||
|
const briSrcs = list.querySelectorAll('.composite-layer-brightness');
|
||||||
if (srcs.length === _compositeLayers.length) {
|
if (srcs.length === _compositeLayers.length) {
|
||||||
for (let i = 0; i < srcs.length; i++) {
|
for (let i = 0; i < srcs.length; i++) {
|
||||||
_compositeLayers[i].source_id = srcs[i].value;
|
_compositeLayers[i].source_id = srcs[i].value;
|
||||||
_compositeLayers[i].blend_mode = blends[i].value;
|
_compositeLayers[i].blend_mode = blends[i].value;
|
||||||
_compositeLayers[i].opacity = parseFloat(opacities[i].value);
|
_compositeLayers[i].opacity = parseFloat(opacities[i].value);
|
||||||
_compositeLayers[i].enabled = enableds[i].checked;
|
_compositeLayers[i].enabled = enableds[i].checked;
|
||||||
|
_compositeLayers[i].brightness_source_id = briSrcs[i] ? (briSrcs[i].value || null) : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _compositeGetLayers() {
|
function _compositeGetLayers() {
|
||||||
_compositeLayersSyncFromDom();
|
_compositeLayersSyncFromDom();
|
||||||
return _compositeLayers.map(l => ({
|
return _compositeLayers.map(l => {
|
||||||
|
const layer = {
|
||||||
source_id: l.source_id,
|
source_id: l.source_id,
|
||||||
blend_mode: l.blend_mode,
|
blend_mode: l.blend_mode,
|
||||||
opacity: l.opacity,
|
opacity: l.opacity,
|
||||||
enabled: l.enabled,
|
enabled: l.enabled,
|
||||||
}));
|
};
|
||||||
|
if (l.brightness_source_id) layer.brightness_source_id = l.brightness_source_id;
|
||||||
|
return layer;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function _loadCompositeState(css) {
|
function _loadCompositeState(css) {
|
||||||
@@ -606,8 +706,9 @@ function _loadCompositeState(css) {
|
|||||||
blend_mode: l.blend_mode || 'normal',
|
blend_mode: l.blend_mode || 'normal',
|
||||||
opacity: l.opacity != null ? l.opacity : 1.0,
|
opacity: l.opacity != null ? l.opacity : 1.0,
|
||||||
enabled: l.enabled != null ? l.enabled : true,
|
enabled: l.enabled != null ? l.enabled : true,
|
||||||
|
brightness_source_id: l.brightness_source_id || null,
|
||||||
}))
|
}))
|
||||||
: [{ source_id: '', blend_mode: 'normal', opacity: 1.0, enabled: true }];
|
: [{ source_id: '', blend_mode: 'normal', opacity: 1.0, enabled: true, brightness_source_id: null }];
|
||||||
_compositeRenderList();
|
_compositeRenderList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -905,6 +1006,7 @@ function _loadNotificationState(css) {
|
|||||||
document.getElementById('css-editor-notification-duration-val').textContent = dur;
|
document.getElementById('css-editor-notification-duration-val').textContent = dur;
|
||||||
document.getElementById('css-editor-notification-default-color').value = css.default_color || '#ffffff';
|
document.getElementById('css-editor-notification-default-color').value = css.default_color || '#ffffff';
|
||||||
document.getElementById('css-editor-notification-filter-mode').value = css.app_filter_mode || 'off';
|
document.getElementById('css-editor-notification-filter-mode').value = css.app_filter_mode || 'off';
|
||||||
|
if (_notificationFilterModeIconSelect) _notificationFilterModeIconSelect.setValue(css.app_filter_mode || 'off');
|
||||||
document.getElementById('css-editor-notification-filter-list').value = (css.app_filter_list || []).join('\n');
|
document.getElementById('css-editor-notification-filter-list').value = (css.app_filter_list || []).join('\n');
|
||||||
onNotificationFilterModeChange();
|
onNotificationFilterModeChange();
|
||||||
_attachNotificationProcessPicker();
|
_attachNotificationProcessPicker();
|
||||||
@@ -924,6 +1026,7 @@ function _resetNotificationState() {
|
|||||||
document.getElementById('css-editor-notification-duration-val').textContent = '1500';
|
document.getElementById('css-editor-notification-duration-val').textContent = '1500';
|
||||||
document.getElementById('css-editor-notification-default-color').value = '#ffffff';
|
document.getElementById('css-editor-notification-default-color').value = '#ffffff';
|
||||||
document.getElementById('css-editor-notification-filter-mode').value = 'off';
|
document.getElementById('css-editor-notification-filter-mode').value = 'off';
|
||||||
|
if (_notificationFilterModeIconSelect) _notificationFilterModeIconSelect.setValue('off');
|
||||||
document.getElementById('css-editor-notification-filter-list').value = '';
|
document.getElementById('css-editor-notification-filter-list').value = '';
|
||||||
onNotificationFilterModeChange();
|
onNotificationFilterModeChange();
|
||||||
_attachNotificationProcessPicker();
|
_attachNotificationProcessPicker();
|
||||||
@@ -1195,6 +1298,7 @@ export async function showCSSEditor(cssId = null, cloneData = null) {
|
|||||||
const sources = await streamsCache.fetch();
|
const sources = await streamsCache.fetch();
|
||||||
|
|
||||||
// Fetch all color strip sources for composite layer dropdowns
|
// Fetch all color strip sources for composite layer dropdowns
|
||||||
|
await valueSourcesCache.fetch().catch(() => []);
|
||||||
const allCssSources = await colorStripSourcesCache.fetch().catch(() => []);
|
const allCssSources = await colorStripSourcesCache.fetch().catch(() => []);
|
||||||
_compositeAvailableSources = allCssSources.filter(s =>
|
_compositeAvailableSources = allCssSources.filter(s =>
|
||||||
s.source_type !== 'composite' && (!cssId || s.id !== cssId)
|
s.source_type !== 'composite' && (!cssId || s.id !== cssId)
|
||||||
|
|||||||
@@ -902,6 +902,9 @@
|
|||||||
"color_strip.notification.filter_mode.off": "Off",
|
"color_strip.notification.filter_mode.off": "Off",
|
||||||
"color_strip.notification.filter_mode.whitelist": "Whitelist",
|
"color_strip.notification.filter_mode.whitelist": "Whitelist",
|
||||||
"color_strip.notification.filter_mode.blacklist": "Blacklist",
|
"color_strip.notification.filter_mode.blacklist": "Blacklist",
|
||||||
|
"color_strip.notification.filter_mode.off.desc": "Accept all notifications",
|
||||||
|
"color_strip.notification.filter_mode.whitelist.desc": "Only listed apps",
|
||||||
|
"color_strip.notification.filter_mode.blacklist.desc": "All except listed apps",
|
||||||
"color_strip.notification.filter_list": "App List:",
|
"color_strip.notification.filter_list": "App List:",
|
||||||
"color_strip.notification.filter_list.hint": "One app name per line. Use Browse to pick from running processes.",
|
"color_strip.notification.filter_list.hint": "One app name per line. Use Browse to pick from running processes.",
|
||||||
"color_strip.notification.filter_list.placeholder": "Discord\nSlack\nTelegram",
|
"color_strip.notification.filter_list.placeholder": "Discord\nSlack\nTelegram",
|
||||||
@@ -945,10 +948,16 @@
|
|||||||
"color_strip.composite.source": "Source",
|
"color_strip.composite.source": "Source",
|
||||||
"color_strip.composite.blend_mode": "Blend",
|
"color_strip.composite.blend_mode": "Blend",
|
||||||
"color_strip.composite.blend_mode.normal": "Normal",
|
"color_strip.composite.blend_mode.normal": "Normal",
|
||||||
|
"color_strip.composite.blend_mode.normal.desc": "Standard alpha blending",
|
||||||
"color_strip.composite.blend_mode.add": "Add",
|
"color_strip.composite.blend_mode.add": "Add",
|
||||||
|
"color_strip.composite.blend_mode.add.desc": "Brightens by adding colors",
|
||||||
"color_strip.composite.blend_mode.multiply": "Multiply",
|
"color_strip.composite.blend_mode.multiply": "Multiply",
|
||||||
|
"color_strip.composite.blend_mode.multiply.desc": "Darkens by multiplying colors",
|
||||||
"color_strip.composite.blend_mode.screen": "Screen",
|
"color_strip.composite.blend_mode.screen": "Screen",
|
||||||
|
"color_strip.composite.blend_mode.screen.desc": "Brightens, inverse of multiply",
|
||||||
"color_strip.composite.opacity": "Opacity",
|
"color_strip.composite.opacity": "Opacity",
|
||||||
|
"color_strip.composite.brightness": "Brightness",
|
||||||
|
"color_strip.composite.brightness.none": "— None —",
|
||||||
"color_strip.composite.enabled": "Enabled",
|
"color_strip.composite.enabled": "Enabled",
|
||||||
"color_strip.composite.error.min_layers": "At least 1 layer is required",
|
"color_strip.composite.error.min_layers": "At least 1 layer is required",
|
||||||
"color_strip.composite.error.no_source": "Each layer must have a source selected",
|
"color_strip.composite.error.no_source": "Each layer must have a source selected",
|
||||||
|
|||||||
@@ -902,6 +902,9 @@
|
|||||||
"color_strip.notification.filter_mode.off": "Выкл",
|
"color_strip.notification.filter_mode.off": "Выкл",
|
||||||
"color_strip.notification.filter_mode.whitelist": "Белый список",
|
"color_strip.notification.filter_mode.whitelist": "Белый список",
|
||||||
"color_strip.notification.filter_mode.blacklist": "Чёрный список",
|
"color_strip.notification.filter_mode.blacklist": "Чёрный список",
|
||||||
|
"color_strip.notification.filter_mode.off.desc": "Принимать все уведомления",
|
||||||
|
"color_strip.notification.filter_mode.whitelist.desc": "Только указанные приложения",
|
||||||
|
"color_strip.notification.filter_mode.blacklist.desc": "Все кроме указанных приложений",
|
||||||
"color_strip.notification.filter_list": "Список приложений:",
|
"color_strip.notification.filter_list": "Список приложений:",
|
||||||
"color_strip.notification.filter_list.hint": "Одно имя приложения на строку. Используйте «Обзор» для выбора из запущенных процессов.",
|
"color_strip.notification.filter_list.hint": "Одно имя приложения на строку. Используйте «Обзор» для выбора из запущенных процессов.",
|
||||||
"color_strip.notification.filter_list.placeholder": "Discord\nSlack\nTelegram",
|
"color_strip.notification.filter_list.placeholder": "Discord\nSlack\nTelegram",
|
||||||
@@ -945,10 +948,16 @@
|
|||||||
"color_strip.composite.source": "Источник",
|
"color_strip.composite.source": "Источник",
|
||||||
"color_strip.composite.blend_mode": "Смешивание",
|
"color_strip.composite.blend_mode": "Смешивание",
|
||||||
"color_strip.composite.blend_mode.normal": "Обычное",
|
"color_strip.composite.blend_mode.normal": "Обычное",
|
||||||
|
"color_strip.composite.blend_mode.normal.desc": "Стандартное альфа-смешивание",
|
||||||
"color_strip.composite.blend_mode.add": "Сложение",
|
"color_strip.composite.blend_mode.add": "Сложение",
|
||||||
|
"color_strip.composite.blend_mode.add.desc": "Осветляет, складывая цвета",
|
||||||
"color_strip.composite.blend_mode.multiply": "Умножение",
|
"color_strip.composite.blend_mode.multiply": "Умножение",
|
||||||
|
"color_strip.composite.blend_mode.multiply.desc": "Затемняет, умножая цвета",
|
||||||
"color_strip.composite.blend_mode.screen": "Экран",
|
"color_strip.composite.blend_mode.screen": "Экран",
|
||||||
|
"color_strip.composite.blend_mode.screen.desc": "Осветляет, обратное умножение",
|
||||||
"color_strip.composite.opacity": "Непрозрачность",
|
"color_strip.composite.opacity": "Непрозрачность",
|
||||||
|
"color_strip.composite.brightness": "Яркость",
|
||||||
|
"color_strip.composite.brightness.none": "— Нет —",
|
||||||
"color_strip.composite.enabled": "Включён",
|
"color_strip.composite.enabled": "Включён",
|
||||||
"color_strip.composite.error.min_layers": "Необходим хотя бы 1 слой",
|
"color_strip.composite.error.min_layers": "Необходим хотя бы 1 слой",
|
||||||
"color_strip.composite.error.no_source": "Для каждого слоя должен быть выбран источник",
|
"color_strip.composite.error.no_source": "Для каждого слоя должен быть выбран источник",
|
||||||
|
|||||||
@@ -902,6 +902,9 @@
|
|||||||
"color_strip.notification.filter_mode.off": "关闭",
|
"color_strip.notification.filter_mode.off": "关闭",
|
||||||
"color_strip.notification.filter_mode.whitelist": "白名单",
|
"color_strip.notification.filter_mode.whitelist": "白名单",
|
||||||
"color_strip.notification.filter_mode.blacklist": "黑名单",
|
"color_strip.notification.filter_mode.blacklist": "黑名单",
|
||||||
|
"color_strip.notification.filter_mode.off.desc": "接受所有通知",
|
||||||
|
"color_strip.notification.filter_mode.whitelist.desc": "仅列出的应用",
|
||||||
|
"color_strip.notification.filter_mode.blacklist.desc": "排除列出的应用",
|
||||||
"color_strip.notification.filter_list": "应用列表:",
|
"color_strip.notification.filter_list": "应用列表:",
|
||||||
"color_strip.notification.filter_list.hint": "每行一个应用名称。使用「浏览」从运行中的进程中选择。",
|
"color_strip.notification.filter_list.hint": "每行一个应用名称。使用「浏览」从运行中的进程中选择。",
|
||||||
"color_strip.notification.filter_list.placeholder": "Discord\nSlack\nTelegram",
|
"color_strip.notification.filter_list.placeholder": "Discord\nSlack\nTelegram",
|
||||||
@@ -945,10 +948,16 @@
|
|||||||
"color_strip.composite.source": "源",
|
"color_strip.composite.source": "源",
|
||||||
"color_strip.composite.blend_mode": "混合",
|
"color_strip.composite.blend_mode": "混合",
|
||||||
"color_strip.composite.blend_mode.normal": "正常",
|
"color_strip.composite.blend_mode.normal": "正常",
|
||||||
|
"color_strip.composite.blend_mode.normal.desc": "标准 Alpha 混合",
|
||||||
"color_strip.composite.blend_mode.add": "叠加",
|
"color_strip.composite.blend_mode.add": "叠加",
|
||||||
|
"color_strip.composite.blend_mode.add.desc": "通过叠加颜色提亮",
|
||||||
"color_strip.composite.blend_mode.multiply": "正片叠底",
|
"color_strip.composite.blend_mode.multiply": "正片叠底",
|
||||||
|
"color_strip.composite.blend_mode.multiply.desc": "通过相乘颜色变暗",
|
||||||
"color_strip.composite.blend_mode.screen": "滤色",
|
"color_strip.composite.blend_mode.screen": "滤色",
|
||||||
|
"color_strip.composite.blend_mode.screen.desc": "提亮,正片叠底的反转",
|
||||||
"color_strip.composite.opacity": "不透明度",
|
"color_strip.composite.opacity": "不透明度",
|
||||||
|
"color_strip.composite.brightness": "亮度",
|
||||||
|
"color_strip.composite.brightness.none": "— 无 —",
|
||||||
"color_strip.composite.enabled": "启用",
|
"color_strip.composite.enabled": "启用",
|
||||||
"color_strip.composite.error.min_layers": "至少需要 1 个图层",
|
"color_strip.composite.error.min_layers": "至少需要 1 个图层",
|
||||||
"color_strip.composite.error.no_source": "每个图层必须选择一个源",
|
"color_strip.composite.error.no_source": "每个图层必须选择一个源",
|
||||||
|
|||||||
@@ -518,7 +518,7 @@ class CompositeColorStripSource(ColorStripSource):
|
|||||||
when led_count == 0.
|
when led_count == 0.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Each layer: {"source_id": str, "blend_mode": str, "opacity": float, "enabled": bool}
|
# Each layer: {"source_id": str, "blend_mode": str, "opacity": float, "enabled": bool, "brightness_source_id": str|None}
|
||||||
layers: list = field(default_factory=list)
|
layers: list = field(default_factory=list)
|
||||||
led_count: int = 0 # 0 = use device LED count
|
led_count: int = 0 # 0 = use device LED count
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user