Add notification reactive color strip source with webhook trigger
New source_type "notification" fires one-shot visual effects (flash, pulse, sweep) triggered via POST webhook. Designed as a composite layer for overlay on persistent sources. Includes app color mapping, whitelist/blacklist filtering, and auto-sizing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -116,6 +116,8 @@ import {
|
||||
applyGradientPreset,
|
||||
cloneColorStrip,
|
||||
copyEndpointUrl,
|
||||
onNotificationFilterModeChange,
|
||||
notificationAddAppColor, notificationRemoveAppColor,
|
||||
} from './features/color-strips.js';
|
||||
|
||||
// Layer 5: audio sources
|
||||
@@ -379,6 +381,8 @@ Object.assign(window, {
|
||||
applyGradientPreset,
|
||||
cloneColorStrip,
|
||||
copyEndpointUrl,
|
||||
onNotificationFilterModeChange,
|
||||
notificationAddAppColor, notificationRemoveAppColor,
|
||||
|
||||
// audio sources
|
||||
showAudioSourceModal,
|
||||
|
||||
@@ -37,6 +37,7 @@ export const eyeOff = '<path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6
|
||||
export const star = '<path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"/>';
|
||||
export const hash = '<line x1="4" x2="20" y1="9" y2="9"/><line x1="4" x2="20" y1="15" y2="15"/><line x1="10" x2="8" y1="3" y2="21"/><line x1="16" x2="14" y1="3" y2="21"/>';
|
||||
export const camera = '<path d="M13.997 4a2 2 0 0 1 1.76 1.05l.486.9A2 2 0 0 0 18.003 7H20a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h1.997a2 2 0 0 0 1.759-1.048l.489-.904A2 2 0 0 1 10.004 4z"/><circle cx="12" cy="13" r="3"/>';
|
||||
export const bellRing = '<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/><path d="M4 2C2.8 3.7 2 5.7 2 8"/><path d="M22 8c0-2.3-.8-4.3-2-6"/>';
|
||||
export const wrench = '<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z"/>';
|
||||
export const music = '<path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/>';
|
||||
export const search = '<path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/>';
|
||||
|
||||
@@ -22,6 +22,7 @@ const _colorStripTypeIcons = {
|
||||
mapped: _svg(P.mapPin), mapped_zones: _svg(P.mapPin),
|
||||
audio: _svg(P.music), audio_visualization: _svg(P.music),
|
||||
api_input: _svg(P.send),
|
||||
notification: _svg(P.bellRing),
|
||||
};
|
||||
const _valueSourceTypeIcons = {
|
||||
static: _svg(P.layoutDashboard), animated: _svg(P.refreshCw), audio: _svg(P.music),
|
||||
@@ -146,3 +147,4 @@ export const ICON_DOWNLOAD = _svg(P.download);
|
||||
export const ICON_UNDO = _svg(P.undo2);
|
||||
export const ICON_SCENE = _svg(P.sparkles);
|
||||
export const ICON_CAPTURE = _svg(P.camera);
|
||||
export const ICON_BELL = _svg(P.bellRing);
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
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,
|
||||
ICON_LINK, ICON_SPARKLES, ICON_ACTIVITY, ICON_CLOCK,
|
||||
ICON_LINK, ICON_SPARKLES, ICON_ACTIVITY, ICON_CLOCK, ICON_BELL,
|
||||
} from '../core/icons.js';
|
||||
import { wrapCard } from '../core/card-colors.js';
|
||||
|
||||
@@ -56,6 +56,12 @@ class CSSEditorModal extends Modal {
|
||||
audio_mirror: document.getElementById('css-editor-audio-mirror').checked,
|
||||
api_input_fallback_color: document.getElementById('css-editor-api-input-fallback-color').value,
|
||||
api_input_timeout: document.getElementById('css-editor-api-input-timeout').value,
|
||||
notification_effect: document.getElementById('css-editor-notification-effect').value,
|
||||
notification_duration: document.getElementById('css-editor-notification-duration').value,
|
||||
notification_default_color: document.getElementById('css-editor-notification-default-color').value,
|
||||
notification_filter_mode: document.getElementById('css-editor-notification-filter-mode').value,
|
||||
notification_filter_list: document.getElementById('css-editor-notification-filter-list').value,
|
||||
notification_app_colors: JSON.stringify(_notificationAppColors),
|
||||
clock_id: document.getElementById('css-editor-clock').value,
|
||||
};
|
||||
}
|
||||
@@ -76,6 +82,7 @@ export function onCSSTypeChange() {
|
||||
document.getElementById('css-editor-mapped-section').style.display = type === 'mapped' ? '' : 'none';
|
||||
document.getElementById('css-editor-audio-section').style.display = type === 'audio' ? '' : 'none';
|
||||
document.getElementById('css-editor-api-input-section').style.display = type === 'api_input' ? '' : 'none';
|
||||
document.getElementById('css-editor-notification-section').style.display = type === 'notification' ? '' : 'none';
|
||||
|
||||
if (type === 'effect') onEffectTypeChange();
|
||||
if (type === 'audio') onAudioVizChange();
|
||||
@@ -576,6 +583,108 @@ function _resetAudioState() {
|
||||
document.getElementById('css-editor-audio-mirror').checked = false;
|
||||
}
|
||||
|
||||
/* ── Notification helpers ────────────────────────────────────── */
|
||||
|
||||
let _notificationAppColors = []; // [{app: '', color: '#...'}]
|
||||
|
||||
export function onNotificationFilterModeChange() {
|
||||
const mode = document.getElementById('css-editor-notification-filter-mode').value;
|
||||
document.getElementById('css-editor-notification-filter-list-group').style.display = mode === 'off' ? 'none' : '';
|
||||
}
|
||||
|
||||
function _notificationAppColorsRenderList() {
|
||||
const list = document.getElementById('notification-app-colors-list');
|
||||
if (!list) return;
|
||||
list.innerHTML = _notificationAppColors.map((entry, i) => `
|
||||
<div class="color-cycle-item">
|
||||
<input type="text" class="notif-app-name" data-idx="${i}" value="${escapeHtml(entry.app)}" placeholder="App name" style="flex:1">
|
||||
<input type="color" class="notif-app-color" data-idx="${i}" value="${entry.color}">
|
||||
<button type="button" class="btn btn-secondary color-cycle-remove-btn"
|
||||
onclick="notificationRemoveAppColor(${i})">✕</button>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
export function notificationAddAppColor() {
|
||||
_notificationAppColorsSyncFromDom();
|
||||
_notificationAppColors.push({ app: '', color: '#ffffff' });
|
||||
_notificationAppColorsRenderList();
|
||||
}
|
||||
|
||||
export function notificationRemoveAppColor(i) {
|
||||
_notificationAppColorsSyncFromDom();
|
||||
_notificationAppColors.splice(i, 1);
|
||||
_notificationAppColorsRenderList();
|
||||
}
|
||||
|
||||
function _notificationAppColorsSyncFromDom() {
|
||||
const list = document.getElementById('notification-app-colors-list');
|
||||
if (!list) return;
|
||||
const names = list.querySelectorAll('.notif-app-name');
|
||||
const colors = list.querySelectorAll('.notif-app-color');
|
||||
if (names.length === _notificationAppColors.length) {
|
||||
for (let i = 0; i < names.length; i++) {
|
||||
_notificationAppColors[i].app = names[i].value;
|
||||
_notificationAppColors[i].color = colors[i].value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _notificationGetAppColorsDict() {
|
||||
_notificationAppColorsSyncFromDom();
|
||||
const dict = {};
|
||||
for (const entry of _notificationAppColors) {
|
||||
if (entry.app.trim()) dict[entry.app.trim()] = entry.color;
|
||||
}
|
||||
return dict;
|
||||
}
|
||||
|
||||
function _loadNotificationState(css) {
|
||||
document.getElementById('css-editor-notification-effect').value = css.notification_effect || 'flash';
|
||||
const dur = css.duration_ms ?? 1500;
|
||||
document.getElementById('css-editor-notification-duration').value = 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-filter-mode').value = css.app_filter_mode || 'off';
|
||||
document.getElementById('css-editor-notification-filter-list').value = (css.app_filter_list || []).join(', ');
|
||||
onNotificationFilterModeChange();
|
||||
|
||||
// App colors dict → list
|
||||
const ac = css.app_colors || {};
|
||||
_notificationAppColors = Object.entries(ac).map(([app, color]) => ({ app, color }));
|
||||
_notificationAppColorsRenderList();
|
||||
|
||||
_showNotificationEndpoint(css.id);
|
||||
}
|
||||
|
||||
function _resetNotificationState() {
|
||||
document.getElementById('css-editor-notification-effect').value = 'flash';
|
||||
document.getElementById('css-editor-notification-duration').value = 1500;
|
||||
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';
|
||||
document.getElementById('css-editor-notification-filter-list').value = '';
|
||||
onNotificationFilterModeChange();
|
||||
_notificationAppColors = [];
|
||||
_notificationAppColorsRenderList();
|
||||
_showNotificationEndpoint(null);
|
||||
}
|
||||
|
||||
function _showNotificationEndpoint(cssId) {
|
||||
const el = document.getElementById('css-editor-notification-endpoint');
|
||||
if (!el) return;
|
||||
if (!cssId) {
|
||||
el.innerHTML = `<em data-i18n="color_strip.notification.save_first">${t('color_strip.notification.save_first')}</em>`;
|
||||
return;
|
||||
}
|
||||
const base = `${window.location.origin}/api/v1`;
|
||||
const url = `${base}/color-strip-sources/${cssId}/notify`;
|
||||
el.innerHTML = `
|
||||
<small class="endpoint-label">POST</small>
|
||||
<div class="ws-url-row"><input type="text" value="${url}" readonly style="font-size:0.85em"><button type="button" class="btn btn-sm btn-secondary" onclick="copyEndpointUrl(this)" title="Copy">📋</button></div>
|
||||
`;
|
||||
}
|
||||
|
||||
/* ── Card ─────────────────────────────────────────────────────── */
|
||||
|
||||
export function createColorStripCard(source, pictureSourceMap, audioSourceMap) {
|
||||
@@ -587,6 +696,7 @@ export function createColorStripCard(source, pictureSourceMap, audioSourceMap) {
|
||||
const isMapped = source.source_type === 'mapped';
|
||||
const isAudio = source.source_type === 'audio';
|
||||
const isApiInput = source.source_type === 'api_input';
|
||||
const isNotification = source.source_type === 'notification';
|
||||
|
||||
// Clock crosslink badge (replaces speed badge when clock is assigned)
|
||||
const clockObj = source.clock_id ? _cachedSyncClocks.find(c => c.id === source.clock_id) : null;
|
||||
@@ -692,6 +802,20 @@ export function createColorStripCard(source, pictureSourceMap, audioSourceMap) {
|
||||
</span>
|
||||
<span class="stream-card-prop" title="${t('color_strip.api_input.timeout')}">${ICON_TIMER} ${timeoutVal}s</span>
|
||||
`;
|
||||
} else if (isNotification) {
|
||||
const effectLabel = t('color_strip.notification.effect.' + (source.notification_effect || 'flash')) || source.notification_effect || 'flash';
|
||||
const durationVal = source.duration_ms || 1500;
|
||||
const defColor = source.default_color || '#FFFFFF';
|
||||
const appCount = source.app_colors ? Object.keys(source.app_colors).length : 0;
|
||||
propsHtml = `
|
||||
<span class="stream-card-prop" title="${t('color_strip.notification.effect')}">${ICON_BELL} ${escapeHtml(effectLabel)}</span>
|
||||
<span class="stream-card-prop" title="${t('color_strip.notification.duration')}">${ICON_TIMER} ${durationVal}ms</span>
|
||||
<span class="stream-card-prop" title="${t('color_strip.notification.default_color')}">
|
||||
<span style="display:inline-block;width:14px;height:14px;background:${defColor};border:1px solid #888;border-radius:2px;vertical-align:middle;margin-right:4px"></span>${defColor.toUpperCase()}
|
||||
</span>
|
||||
${appCount > 0 ? `<span class="stream-card-prop">${ICON_PALETTE} ${appCount} ${t('color_strip.notification.app_count')}</span>` : ''}
|
||||
${source.led_count ? `<span class="stream-card-prop" title="${t('color_strip.leds')}">${ICON_LED} ${source.led_count}</span>` : ''}
|
||||
`;
|
||||
} else {
|
||||
const ps = pictureSourceMap && pictureSourceMap[source.picture_source_id];
|
||||
const srcName = ps ? ps.name : source.picture_source_id || '—';
|
||||
@@ -710,7 +834,7 @@ export function createColorStripCard(source, pictureSourceMap, audioSourceMap) {
|
||||
}
|
||||
|
||||
const icon = getColorStripIcon(source.source_type);
|
||||
const calibrationBtn = (!isStatic && !isGradient && !isColorCycle && !isEffect && !isComposite && !isMapped && !isAudio && !isApiInput)
|
||||
const calibrationBtn = (!isStatic && !isGradient && !isColorCycle && !isEffect && !isComposite && !isMapped && !isAudio && !isApiInput && !isNotification)
|
||||
? `<button class="btn btn-icon btn-secondary" onclick="showCSSCalibration('${source.id}')" title="${t('calibration.title')}">${ICON_CALIBRATION}</button>`
|
||||
: '';
|
||||
|
||||
@@ -809,6 +933,8 @@ export async function showCSSEditor(cssId = null, cloneData = null) {
|
||||
document.getElementById('css-editor-api-input-timeout-val').textContent =
|
||||
parseFloat(css.timeout ?? 5.0).toFixed(1);
|
||||
_showApiInputEndpoints(css.id);
|
||||
} else if (sourceType === 'notification') {
|
||||
_loadNotificationState(css);
|
||||
} else {
|
||||
sourceSelect.value = css.picture_source_id || '';
|
||||
|
||||
@@ -899,6 +1025,7 @@ export async function showCSSEditor(cssId = null, cloneData = null) {
|
||||
document.getElementById('css-editor-api-input-timeout').value = 5.0;
|
||||
document.getElementById('css-editor-api-input-timeout-val').textContent = '5.0';
|
||||
_showApiInputEndpoints(null);
|
||||
_resetNotificationState();
|
||||
document.getElementById('css-editor-title').innerHTML = `${ICON_FILM} ${t('color_strip.add')}`;
|
||||
document.getElementById('css-editor-gradient-preset').value = '';
|
||||
gradientInit([
|
||||
@@ -1030,6 +1157,20 @@ export async function saveCSSEditor() {
|
||||
timeout: parseFloat(document.getElementById('css-editor-api-input-timeout').value),
|
||||
};
|
||||
if (!cssId) payload.source_type = 'api_input';
|
||||
} else if (sourceType === 'notification') {
|
||||
const filterList = document.getElementById('css-editor-notification-filter-list').value
|
||||
.split(',').map(s => s.trim()).filter(Boolean);
|
||||
payload = {
|
||||
name,
|
||||
notification_effect: document.getElementById('css-editor-notification-effect').value,
|
||||
duration_ms: parseInt(document.getElementById('css-editor-notification-duration').value) || 1500,
|
||||
default_color: document.getElementById('css-editor-notification-default-color').value,
|
||||
app_filter_mode: document.getElementById('css-editor-notification-filter-mode').value,
|
||||
app_filter_list: filterList,
|
||||
app_colors: _notificationGetAppColorsDict(),
|
||||
led_count: parseInt(document.getElementById('css-editor-led-count').value) || 0,
|
||||
};
|
||||
if (!cssId) payload.source_type = 'notification';
|
||||
} else {
|
||||
payload = {
|
||||
name,
|
||||
|
||||
@@ -808,6 +808,33 @@
|
||||
"color_strip.api_input.endpoints": "Push Endpoints:",
|
||||
"color_strip.api_input.endpoints.hint": "Use these URLs to push LED color data from your external application. REST accepts JSON, WebSocket accepts both JSON and raw binary frames.",
|
||||
"color_strip.api_input.save_first": "Save the source first to see the push endpoint URLs.",
|
||||
"color_strip.type.notification": "Notification",
|
||||
"color_strip.type.notification.hint": "Fires a one-shot visual effect (flash, pulse, sweep) when triggered via a webhook. Designed for use as a composite layer over a persistent base source.",
|
||||
"color_strip.notification.effect": "Effect:",
|
||||
"color_strip.notification.effect.hint": "Visual effect when a notification fires. Flash fades linearly, Pulse uses a smooth bell curve, Sweep fills LEDs left-to-right then fades.",
|
||||
"color_strip.notification.effect.flash": "Flash",
|
||||
"color_strip.notification.effect.pulse": "Pulse",
|
||||
"color_strip.notification.effect.sweep": "Sweep",
|
||||
"color_strip.notification.duration": "Duration (ms):",
|
||||
"color_strip.notification.duration.hint": "How long the notification effect plays, in milliseconds.",
|
||||
"color_strip.notification.default_color": "Default Color:",
|
||||
"color_strip.notification.default_color.hint": "Color used when the notification has no app-specific color mapping.",
|
||||
"color_strip.notification.filter_mode": "App Filter:",
|
||||
"color_strip.notification.filter_mode.hint": "Filter notifications by app name. Off = accept all, Whitelist = only listed apps, Blacklist = all except listed apps.",
|
||||
"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_list": "App List:",
|
||||
"color_strip.notification.filter_list.hint": "Comma-separated app names for the filter.",
|
||||
"color_strip.notification.filter_list.placeholder": "Discord, Slack, Telegram",
|
||||
"color_strip.notification.app_colors": "App Colors",
|
||||
"color_strip.notification.app_colors.label": "Color Mappings:",
|
||||
"color_strip.notification.app_colors.hint": "Per-app color overrides. Each row maps an app name to a specific notification color.",
|
||||
"color_strip.notification.app_colors.add": "+ Add Mapping",
|
||||
"color_strip.notification.endpoint": "Webhook Endpoint:",
|
||||
"color_strip.notification.endpoint.hint": "Use this URL to trigger notifications from external systems. POST with optional JSON body: {\"app\": \"AppName\", \"color\": \"#FF0000\"}.",
|
||||
"color_strip.notification.save_first": "Save the source first to see the webhook endpoint URL.",
|
||||
"color_strip.notification.app_count": "apps",
|
||||
"color_strip.composite.layers": "Layers:",
|
||||
"color_strip.composite.layers.hint": "Stack multiple color strip sources. First layer is the bottom, last is the top. Each layer can have its own blend mode and opacity.",
|
||||
"color_strip.composite.add_layer": "+ Add Layer",
|
||||
|
||||
@@ -808,6 +808,33 @@
|
||||
"color_strip.api_input.endpoints": "Эндпоинты для отправки:",
|
||||
"color_strip.api_input.endpoints.hint": "Используйте эти URL для отправки данных о цветах LED из вашего внешнего приложения. REST принимает JSON, WebSocket принимает как JSON, так и бинарные кадры.",
|
||||
"color_strip.api_input.save_first": "Сначала сохраните источник, чтобы увидеть URL эндпоинтов.",
|
||||
"color_strip.type.notification": "Уведомления",
|
||||
"color_strip.type.notification.hint": "Вспышка, пульс или волна при срабатывании через вебхук. Предназначен для использования как слой в композитном источнике.",
|
||||
"color_strip.notification.effect": "Эффект:",
|
||||
"color_strip.notification.effect.hint": "Визуальный эффект при уведомлении. Вспышка — линейное затухание, Пульс — плавная волна, Волна — заполнение и затухание.",
|
||||
"color_strip.notification.effect.flash": "Вспышка",
|
||||
"color_strip.notification.effect.pulse": "Пульс",
|
||||
"color_strip.notification.effect.sweep": "Волна",
|
||||
"color_strip.notification.duration": "Длительность (мс):",
|
||||
"color_strip.notification.duration.hint": "Как долго длится эффект уведомления в миллисекундах.",
|
||||
"color_strip.notification.default_color": "Цвет по умолчанию:",
|
||||
"color_strip.notification.default_color.hint": "Цвет, когда для приложения нет специфического назначения цвета.",
|
||||
"color_strip.notification.filter_mode": "Фильтр приложений:",
|
||||
"color_strip.notification.filter_mode.hint": "Фильтр уведомлений по имени приложения. Выкл = все, Белый список = только указанные, Чёрный список = все кроме указанных.",
|
||||
"color_strip.notification.filter_mode.off": "Выкл",
|
||||
"color_strip.notification.filter_mode.whitelist": "Белый список",
|
||||
"color_strip.notification.filter_mode.blacklist": "Чёрный список",
|
||||
"color_strip.notification.filter_list": "Список приложений:",
|
||||
"color_strip.notification.filter_list.hint": "Имена приложений через запятую.",
|
||||
"color_strip.notification.filter_list.placeholder": "Discord, Slack, Telegram",
|
||||
"color_strip.notification.app_colors": "Цвета приложений",
|
||||
"color_strip.notification.app_colors.label": "Назначения цветов:",
|
||||
"color_strip.notification.app_colors.hint": "Индивидуальные цвета для приложений. Каждая строка связывает имя приложения с цветом уведомления.",
|
||||
"color_strip.notification.app_colors.add": "+ Добавить",
|
||||
"color_strip.notification.endpoint": "Вебхук:",
|
||||
"color_strip.notification.endpoint.hint": "URL для запуска уведомлений из внешних систем. POST с JSON телом: {\"app\": \"AppName\", \"color\": \"#FF0000\"}.",
|
||||
"color_strip.notification.save_first": "Сначала сохраните источник, чтобы увидеть URL вебхука.",
|
||||
"color_strip.notification.app_count": "прилож.",
|
||||
"color_strip.composite.layers": "Слои:",
|
||||
"color_strip.composite.layers.hint": "Наложение нескольких источников. Первый слой — нижний, последний — верхний. Каждый слой может иметь свой режим смешивания и прозрачность.",
|
||||
"color_strip.composite.add_layer": "+ Добавить слой",
|
||||
|
||||
@@ -808,6 +808,33 @@
|
||||
"color_strip.api_input.endpoints": "推送端点:",
|
||||
"color_strip.api_input.endpoints.hint": "使用这些 URL 从外部应用程序推送 LED 颜色数据。REST 接受 JSON,WebSocket 接受 JSON 和原始二进制帧。",
|
||||
"color_strip.api_input.save_first": "请先保存源以查看推送端点 URL。",
|
||||
"color_strip.type.notification": "通知",
|
||||
"color_strip.type.notification.hint": "通过 Webhook 触发时显示一次性视觉效果(闪烁、脉冲、扫描)。设计为组合源中的叠加层。",
|
||||
"color_strip.notification.effect": "效果:",
|
||||
"color_strip.notification.effect.hint": "通知触发时的视觉效果。闪烁线性衰减,脉冲平滑钟形曲线,扫描从左到右填充后衰减。",
|
||||
"color_strip.notification.effect.flash": "闪烁",
|
||||
"color_strip.notification.effect.pulse": "脉冲",
|
||||
"color_strip.notification.effect.sweep": "扫描",
|
||||
"color_strip.notification.duration": "持续时间(毫秒):",
|
||||
"color_strip.notification.duration.hint": "通知效果播放的时长(毫秒)。",
|
||||
"color_strip.notification.default_color": "默认颜色:",
|
||||
"color_strip.notification.default_color.hint": "当通知没有应用特定颜色映射时使用的颜色。",
|
||||
"color_strip.notification.filter_mode": "应用过滤:",
|
||||
"color_strip.notification.filter_mode.hint": "按应用名称过滤通知。关闭=接受全部,白名单=仅列出的应用,黑名单=排除列出的应用。",
|
||||
"color_strip.notification.filter_mode.off": "关闭",
|
||||
"color_strip.notification.filter_mode.whitelist": "白名单",
|
||||
"color_strip.notification.filter_mode.blacklist": "黑名单",
|
||||
"color_strip.notification.filter_list": "应用列表:",
|
||||
"color_strip.notification.filter_list.hint": "以逗号分隔的应用名称。",
|
||||
"color_strip.notification.filter_list.placeholder": "Discord, Slack, Telegram",
|
||||
"color_strip.notification.app_colors": "应用颜色",
|
||||
"color_strip.notification.app_colors.label": "颜色映射:",
|
||||
"color_strip.notification.app_colors.hint": "每个应用的自定义通知颜色。每行将一个应用名称映射到特定颜色。",
|
||||
"color_strip.notification.app_colors.add": "+ 添加映射",
|
||||
"color_strip.notification.endpoint": "Webhook 端点:",
|
||||
"color_strip.notification.endpoint.hint": "使用此 URL 从外部系统触发通知。POST 请求可选 JSON:{\"app\": \"AppName\", \"color\": \"#FF0000\"}。",
|
||||
"color_strip.notification.save_first": "请先保存源以查看 Webhook 端点 URL。",
|
||||
"color_strip.notification.app_count": "个应用",
|
||||
"color_strip.composite.layers": "图层:",
|
||||
"color_strip.composite.layers.hint": "叠加多个色带源。第一个图层在底部,最后一个在顶部。每个图层可以有自己的混合模式和不透明度。",
|
||||
"color_strip.composite.add_layer": "+ 添加图层",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* - Navigation: network-first with offline fallback
|
||||
*/
|
||||
|
||||
const CACHE_NAME = 'ledgrab-v10';
|
||||
const CACHE_NAME = 'ledgrab-v11';
|
||||
|
||||
// Only pre-cache static assets (no auth required).
|
||||
// Do NOT pre-cache '/' — it requires API key auth and would cache an error page.
|
||||
|
||||
Reference in New Issue
Block a user