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")
|
||||
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")
|
||||
brightness_source_id: Optional[str] = Field(None, description="Optional value source ID for dynamic brightness")
|
||||
|
||||
|
||||
class MappedZone(BaseModel):
|
||||
|
||||
@@ -68,7 +68,7 @@ class ColorStripStreamManager:
|
||||
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:
|
||||
color_strip_store: ColorStripStore for resolving source configs
|
||||
@@ -76,6 +76,7 @@ class ColorStripStreamManager:
|
||||
audio_capture_manager: AudioCaptureManager for audio-reactive sources
|
||||
audio_source_store: AudioSourceStore for resolving audio source chains
|
||||
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._live_stream_manager = live_stream_manager
|
||||
@@ -83,6 +84,7 @@ class ColorStripStreamManager:
|
||||
self._audio_source_store = audio_source_store
|
||||
self._audio_template_store = audio_template_store
|
||||
self._sync_clock_manager = sync_clock_manager
|
||||
self._value_stream_manager = value_stream_manager
|
||||
self._streams: Dict[str, _ColorStripEntry] = {}
|
||||
|
||||
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)
|
||||
elif source.source_type == "composite":
|
||||
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":
|
||||
from wled_controller.core.processing.mapped_stream import MappedColorStripStream
|
||||
css_stream = MappedColorStripStream(source, self)
|
||||
|
||||
@@ -29,12 +29,13 @@ class CompositeColorStripStream(ColorStripStream):
|
||||
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._layers: List[dict] = list(source.layers)
|
||||
self._led_count: int = source.led_count
|
||||
self._auto_size: bool = source.led_count == 0
|
||||
self._css_manager = css_manager
|
||||
self._value_stream_manager = value_stream_manager
|
||||
self._fps: int = 30
|
||||
self._frame_time: float = 1.0 / 30
|
||||
|
||||
@@ -45,7 +46,9 @@ class CompositeColorStripStream(ColorStripStream):
|
||||
|
||||
# layer_index -> (source_id, consumer_id, stream)
|
||||
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)
|
||||
self._pool_n = 0
|
||||
@@ -115,9 +118,9 @@ class CompositeColorStripStream(ColorStripStream):
|
||||
def update_source(self, source) -> None:
|
||||
"""Hot-update: rebuild sub-streams if layer config changed."""
|
||||
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]
|
||||
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]
|
||||
|
||||
self._layers = new_layers
|
||||
@@ -152,6 +155,16 @@ class CompositeColorStripStream(ColorStripStream):
|
||||
logger.warning(
|
||||
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:
|
||||
for _idx, (src_id, consumer_id, _stream) in list(self._sub_streams.items()):
|
||||
@@ -160,6 +173,14 @@ class CompositeColorStripStream(ColorStripStream):
|
||||
except Exception as e:
|
||||
logger.warning(f"Composite layer release error ({src_id}): {e}")
|
||||
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 ────────────────────────────────────────────
|
||||
|
||||
@@ -299,6 +320,13 @@ class CompositeColorStripStream(ColorStripStream):
|
||||
if len(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)
|
||||
blend_mode = layer.get("blend_mode", _BLEND_NORMAL)
|
||||
alpha = int(opacity * 256)
|
||||
|
||||
@@ -122,6 +122,8 @@ class ProcessorManager:
|
||||
live_stream_manager=self._live_stream_manager,
|
||||
audio_template_store=audio_template_store,
|
||||
) 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._event_queues: List[asyncio.Queue] = []
|
||||
self._metrics_history = MetricsHistory(self)
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
*/
|
||||
|
||||
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 { showToast, showConfirm } from '../core/ui.js';
|
||||
import { Modal } from '../core/modal.js';
|
||||
import {
|
||||
getColorStripIcon, getPictureSourceIcon, getAudioSourceIcon,
|
||||
getColorStripIcon, getPictureSourceIcon, getAudioSourceIcon, getValueSourceIcon,
|
||||
ICON_CLONE, ICON_EDIT, ICON_CALIBRATION,
|
||||
ICON_LED, ICON_PALETTE, ICON_FPS, ICON_MAP_PIN, ICON_MUSIC,
|
||||
ICON_AUDIO_LOOPBACK, ICON_TIMER, ICON_LINK_SOURCE, ICON_FILM,
|
||||
@@ -36,6 +36,7 @@ class CSSEditorModal extends Modal {
|
||||
|
||||
onForceClose() {
|
||||
if (_cssTagsInput) { _cssTagsInput.destroy(); _cssTagsInput = null; }
|
||||
_compositeDestroyEntitySelects();
|
||||
}
|
||||
|
||||
snapshotValues() {
|
||||
@@ -170,7 +171,10 @@ export function onCSSTypeChange() {
|
||||
onAudioVizChange();
|
||||
}
|
||||
if (type === 'gradient') _ensureGradientPresetIconSelect();
|
||||
if (type === 'notification') _ensureNotificationEffectIconSelect();
|
||||
if (type === 'notification') {
|
||||
_ensureNotificationEffectIconSelect();
|
||||
_ensureNotificationFilterModeIconSelect();
|
||||
}
|
||||
|
||||
// Animation section — shown for static/gradient only
|
||||
const animSection = document.getElementById('css-editor-animation-section');
|
||||
@@ -319,6 +323,7 @@ let _audioPaletteIconSelect = null;
|
||||
let _audioVizIconSelect = null;
|
||||
let _gradientPresetIconSelect = null;
|
||||
let _notificationEffectIconSelect = null;
|
||||
let _notificationFilterModeIconSelect = null;
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
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 ──────────────────────────────────────── */
|
||||
|
||||
// Palette color control points — mirrors _PALETTE_DEFS in effect_stream.py
|
||||
@@ -504,14 +521,57 @@ function _loadColorCycleState(css) {
|
||||
|
||||
let _compositeLayers = [];
|
||||
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() {
|
||||
const list = document.getElementById('composite-layers-list');
|
||||
if (!list) return;
|
||||
_compositeDestroyEntitySelects();
|
||||
const vsList = _cachedValueSources || [];
|
||||
list.innerHTML = _compositeLayers.map((layer, i) => {
|
||||
const srcOptions = _compositeAvailableSources.map(s =>
|
||||
`<option value="${s.id}"${layer.source_id === s.id ? ' selected' : ''}>${escapeHtml(s.name)}</option>`
|
||||
).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;
|
||||
return `
|
||||
<div class="composite-layer-item">
|
||||
@@ -540,6 +600,12 @@ function _compositeRenderList() {
|
||||
onclick="compositeRemoveLayer(${i})">✕</button>`
|
||||
: ''}
|
||||
</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>
|
||||
`;
|
||||
}).join('');
|
||||
@@ -551,6 +617,33 @@ function _compositeRenderList() {
|
||||
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() {
|
||||
@@ -560,6 +653,7 @@ export function compositeAddLayer() {
|
||||
blend_mode: 'normal',
|
||||
opacity: 1.0,
|
||||
enabled: true,
|
||||
brightness_source_id: null,
|
||||
});
|
||||
_compositeRenderList();
|
||||
}
|
||||
@@ -578,24 +672,30 @@ function _compositeLayersSyncFromDom() {
|
||||
const blends = list.querySelectorAll('.composite-layer-blend');
|
||||
const opacities = list.querySelectorAll('.composite-layer-opacity');
|
||||
const enableds = list.querySelectorAll('.composite-layer-enabled');
|
||||
const briSrcs = list.querySelectorAll('.composite-layer-brightness');
|
||||
if (srcs.length === _compositeLayers.length) {
|
||||
for (let i = 0; i < srcs.length; i++) {
|
||||
_compositeLayers[i].source_id = srcs[i].value;
|
||||
_compositeLayers[i].blend_mode = blends[i].value;
|
||||
_compositeLayers[i].opacity = parseFloat(opacities[i].value);
|
||||
_compositeLayers[i].enabled = enableds[i].checked;
|
||||
_compositeLayers[i].brightness_source_id = briSrcs[i] ? (briSrcs[i].value || null) : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _compositeGetLayers() {
|
||||
_compositeLayersSyncFromDom();
|
||||
return _compositeLayers.map(l => ({
|
||||
source_id: l.source_id,
|
||||
blend_mode: l.blend_mode,
|
||||
opacity: l.opacity,
|
||||
enabled: l.enabled,
|
||||
}));
|
||||
return _compositeLayers.map(l => {
|
||||
const layer = {
|
||||
source_id: l.source_id,
|
||||
blend_mode: l.blend_mode,
|
||||
opacity: l.opacity,
|
||||
enabled: l.enabled,
|
||||
};
|
||||
if (l.brightness_source_id) layer.brightness_source_id = l.brightness_source_id;
|
||||
return layer;
|
||||
});
|
||||
}
|
||||
|
||||
function _loadCompositeState(css) {
|
||||
@@ -606,8 +706,9 @@ function _loadCompositeState(css) {
|
||||
blend_mode: l.blend_mode || 'normal',
|
||||
opacity: l.opacity != null ? l.opacity : 1.0,
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -905,6 +1006,7 @@ function _loadNotificationState(css) {
|
||||
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-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');
|
||||
onNotificationFilterModeChange();
|
||||
_attachNotificationProcessPicker();
|
||||
@@ -924,6 +1026,7 @@ function _resetNotificationState() {
|
||||
document.getElementById('css-editor-notification-duration-val').textContent = '1500';
|
||||
document.getElementById('css-editor-notification-default-color').value = '#ffffff';
|
||||
document.getElementById('css-editor-notification-filter-mode').value = 'off';
|
||||
if (_notificationFilterModeIconSelect) _notificationFilterModeIconSelect.setValue('off');
|
||||
document.getElementById('css-editor-notification-filter-list').value = '';
|
||||
onNotificationFilterModeChange();
|
||||
_attachNotificationProcessPicker();
|
||||
@@ -1195,6 +1298,7 @@ export async function showCSSEditor(cssId = null, cloneData = null) {
|
||||
const sources = await streamsCache.fetch();
|
||||
|
||||
// Fetch all color strip sources for composite layer dropdowns
|
||||
await valueSourcesCache.fetch().catch(() => []);
|
||||
const allCssSources = await colorStripSourcesCache.fetch().catch(() => []);
|
||||
_compositeAvailableSources = allCssSources.filter(s =>
|
||||
s.source_type !== 'composite' && (!cssId || s.id !== cssId)
|
||||
|
||||
@@ -902,6 +902,9 @@
|
||||
"color_strip.notification.filter_mode.off": "Off",
|
||||
"color_strip.notification.filter_mode.whitelist": "Whitelist",
|
||||
"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.hint": "One app name per line. Use Browse to pick from running processes.",
|
||||
"color_strip.notification.filter_list.placeholder": "Discord\nSlack\nTelegram",
|
||||
@@ -945,10 +948,16 @@
|
||||
"color_strip.composite.source": "Source",
|
||||
"color_strip.composite.blend_mode": "Blend",
|
||||
"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.desc": "Brightens by adding colors",
|
||||
"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.desc": "Brightens, inverse of multiply",
|
||||
"color_strip.composite.opacity": "Opacity",
|
||||
"color_strip.composite.brightness": "Brightness",
|
||||
"color_strip.composite.brightness.none": "— None —",
|
||||
"color_strip.composite.enabled": "Enabled",
|
||||
"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",
|
||||
|
||||
@@ -902,6 +902,9 @@
|
||||
"color_strip.notification.filter_mode.off": "Выкл",
|
||||
"color_strip.notification.filter_mode.whitelist": "Белый список",
|
||||
"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.hint": "Одно имя приложения на строку. Используйте «Обзор» для выбора из запущенных процессов.",
|
||||
"color_strip.notification.filter_list.placeholder": "Discord\nSlack\nTelegram",
|
||||
@@ -945,10 +948,16 @@
|
||||
"color_strip.composite.source": "Источник",
|
||||
"color_strip.composite.blend_mode": "Смешивание",
|
||||
"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.desc": "Осветляет, складывая цвета",
|
||||
"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.desc": "Осветляет, обратное умножение",
|
||||
"color_strip.composite.opacity": "Непрозрачность",
|
||||
"color_strip.composite.brightness": "Яркость",
|
||||
"color_strip.composite.brightness.none": "— Нет —",
|
||||
"color_strip.composite.enabled": "Включён",
|
||||
"color_strip.composite.error.min_layers": "Необходим хотя бы 1 слой",
|
||||
"color_strip.composite.error.no_source": "Для каждого слоя должен быть выбран источник",
|
||||
|
||||
@@ -902,6 +902,9 @@
|
||||
"color_strip.notification.filter_mode.off": "关闭",
|
||||
"color_strip.notification.filter_mode.whitelist": "白名单",
|
||||
"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.hint": "每行一个应用名称。使用「浏览」从运行中的进程中选择。",
|
||||
"color_strip.notification.filter_list.placeholder": "Discord\nSlack\nTelegram",
|
||||
@@ -945,10 +948,16 @@
|
||||
"color_strip.composite.source": "源",
|
||||
"color_strip.composite.blend_mode": "混合",
|
||||
"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.desc": "通过叠加颜色提亮",
|
||||
"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.desc": "提亮,正片叠底的反转",
|
||||
"color_strip.composite.opacity": "不透明度",
|
||||
"color_strip.composite.brightness": "亮度",
|
||||
"color_strip.composite.brightness.none": "— 无 —",
|
||||
"color_strip.composite.enabled": "启用",
|
||||
"color_strip.composite.error.min_layers": "至少需要 1 个图层",
|
||||
"color_strip.composite.error.no_source": "每个图层必须选择一个源",
|
||||
|
||||
@@ -518,7 +518,7 @@ class CompositeColorStripSource(ColorStripSource):
|
||||
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)
|
||||
led_count: int = 0 # 0 = use device LED count
|
||||
|
||||
|
||||
Reference in New Issue
Block a user