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:
@@ -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": "每个图层必须选择一个源",
|
||||
|
||||
Reference in New Issue
Block a user