Add transient preview WS endpoint and test button in CSS editor modal
- Add /color-strip-sources/preview/ws endpoint for ad-hoc source preview
without saving (accepts full config JSON, streams RGB frames)
- Add test preview button (flask icon) to CSS editor modal footer
- For self-contained types (static, gradient, color_cycle, effect, daylight,
candlelight), always previews current form values via transient WS
- For non-previewable types, falls back to saved source test endpoint
- Fix route ordering: preview/ws registered before {source_id}/ws
- Fix css-test-led-control label alignment (display: inline globally)
- Add gradient onChange callback for future live-update support
- Add i18n keys for preview (en/ru/zh)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -342,6 +342,11 @@
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.css-test-led-control label {
|
||||
display: inline;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.css-test-led-input {
|
||||
width: 70px;
|
||||
flex: 0 0 70px;
|
||||
@@ -1263,3 +1268,4 @@
|
||||
height: 26px;
|
||||
flex: 0 0 26px;
|
||||
}
|
||||
|
||||
|
||||
@@ -125,6 +125,7 @@ import {
|
||||
applyGradientPreset,
|
||||
cloneColorStrip,
|
||||
toggleCSSOverlay,
|
||||
previewCSSFromEditor,
|
||||
copyEndpointUrl,
|
||||
onNotificationFilterModeChange,
|
||||
notificationAddAppColor, notificationRemoveAppColor,
|
||||
@@ -425,6 +426,7 @@ Object.assign(window, {
|
||||
applyGradientPreset,
|
||||
cloneColorStrip,
|
||||
toggleCSSOverlay,
|
||||
previewCSSFromEditor,
|
||||
copyEndpointUrl,
|
||||
onNotificationFilterModeChange,
|
||||
notificationAddAppColor, notificationRemoveAppColor,
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
ICON_CLONE, ICON_EDIT, ICON_CALIBRATION, ICON_OVERLAY,
|
||||
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_BELL, ICON_EYE,
|
||||
ICON_LINK, ICON_SPARKLES, ICON_ACTIVITY, ICON_CLOCK, ICON_BELL, ICON_TEST,
|
||||
ICON_SUN_DIM, ICON_WARNING,
|
||||
} from '../core/icons.js';
|
||||
import * as P from '../core/icon-paths.js';
|
||||
@@ -1354,7 +1354,7 @@ export function createColorStripCard(source, pictureSourceMap, audioSourceMap) {
|
||||
? `<button class="btn btn-icon btn-secondary" onclick="event.stopPropagation(); testNotification('${source.id}')" title="${t('color_strip.notification.test')}">${ICON_BELL}</button>`
|
||||
: '';
|
||||
const testPreviewBtn = !isApiInput
|
||||
? `<button class="btn btn-icon btn-secondary" onclick="event.stopPropagation(); testColorStrip('${source.id}')" title="${t('color_strip.test.title')}">${ICON_EYE}</button>`
|
||||
? `<button class="btn btn-icon btn-secondary" onclick="event.stopPropagation(); testColorStrip('${source.id}')" title="${t('color_strip.test.title')}">${ICON_TEST}</button>`
|
||||
: '';
|
||||
|
||||
return wrapCard({
|
||||
@@ -2006,6 +2006,65 @@ export async function stopCSSOverlay(cssId) {
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Editor Preview (opens existing test modal with transient WS) ──── */
|
||||
|
||||
const _PREVIEW_TYPES = new Set(['static', 'gradient', 'color_cycle', 'effect', 'daylight', 'candlelight']);
|
||||
|
||||
/** Collect current editor form state into a source config for transient preview. */
|
||||
function _collectPreviewConfig() {
|
||||
const sourceType = document.getElementById('css-editor-type').value;
|
||||
if (!_PREVIEW_TYPES.has(sourceType)) return null;
|
||||
let config;
|
||||
if (sourceType === 'static') {
|
||||
config = { source_type: 'static', color: hexToRgbArray(document.getElementById('css-editor-color').value), animation: _getAnimationPayload() };
|
||||
} else if (sourceType === 'gradient') {
|
||||
const stops = getGradientStops();
|
||||
if (stops.length < 2) return null;
|
||||
config = { source_type: 'gradient', stops: stops.map(s => ({ position: s.position, color: s.color, ...(s.colorRight ? { color_right: s.colorRight } : {}) })), animation: _getAnimationPayload() };
|
||||
} else if (sourceType === 'color_cycle') {
|
||||
const colors = _colorCycleGetColors();
|
||||
if (colors.length < 2) return null;
|
||||
config = { source_type: 'color_cycle', colors };
|
||||
} else if (sourceType === 'effect') {
|
||||
config = { source_type: 'effect', effect_type: document.getElementById('css-editor-effect-type').value, palette: document.getElementById('css-editor-effect-palette').value, intensity: parseFloat(document.getElementById('css-editor-effect-intensity').value), scale: parseFloat(document.getElementById('css-editor-effect-scale').value), mirror: document.getElementById('css-editor-effect-mirror').checked };
|
||||
if (config.effect_type === 'meteor') { const hex = document.getElementById('css-editor-effect-color').value; config.color = [parseInt(hex.slice(1, 3), 16), parseInt(hex.slice(3, 5), 16), parseInt(hex.slice(5, 7), 16)]; }
|
||||
} else if (sourceType === 'daylight') {
|
||||
config = { source_type: 'daylight', speed: parseFloat(document.getElementById('css-editor-daylight-speed').value), use_real_time: document.getElementById('css-editor-daylight-real-time').checked, latitude: parseFloat(document.getElementById('css-editor-daylight-latitude').value) };
|
||||
} else if (sourceType === 'candlelight') {
|
||||
config = { source_type: 'candlelight', color: hexToRgbArray(document.getElementById('css-editor-candlelight-color').value), intensity: parseFloat(document.getElementById('css-editor-candlelight-intensity').value), num_candles: parseInt(document.getElementById('css-editor-candlelight-num-candles').value) || 3, speed: parseFloat(document.getElementById('css-editor-candlelight-speed').value) };
|
||||
}
|
||||
const clockEl = document.getElementById('css-editor-clock');
|
||||
if (clockEl && clockEl.value) config.clock_id = clockEl.value;
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the existing Test Preview modal from the CSS editor.
|
||||
* For saved sources, uses the normal test endpoint.
|
||||
* For unsaved/self-contained types, uses the transient preview endpoint.
|
||||
*/
|
||||
export function previewCSSFromEditor() {
|
||||
// Always use transient preview with current form values
|
||||
const config = _collectPreviewConfig();
|
||||
if (!config) {
|
||||
// Non-previewable type (picture, composite, etc.) — fall back to saved source test
|
||||
const cssId = document.getElementById('css-editor-id').value;
|
||||
if (cssId) { testColorStrip(cssId); return; }
|
||||
showToast(t('color_strip.preview.unsupported'), 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
_cssTestCSPTMode = false;
|
||||
_cssTestCSPTId = null;
|
||||
_cssTestTransientConfig = config;
|
||||
const csptGroup = document.getElementById('css-test-cspt-input-group');
|
||||
if (csptGroup) csptGroup.style.display = 'none';
|
||||
_openTestModal('__preview__');
|
||||
}
|
||||
|
||||
/** Store transient config so _cssTestConnect and applyCssTestSettings can use it. */
|
||||
let _cssTestTransientConfig = null;
|
||||
|
||||
/* ── Test / Preview ───────────────────────────────────────────── */
|
||||
|
||||
const _CSS_TEST_LED_KEY = 'css_test_led_count';
|
||||
@@ -2124,8 +2183,11 @@ function _cssTestConnect(sourceId, ledCount, fps) {
|
||||
if (!fps) fps = _getCssTestFps();
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const apiKey = localStorage.getItem('wled_api_key') || '';
|
||||
const isTransient = sourceId === '__preview__' && _cssTestTransientConfig;
|
||||
let wsUrl;
|
||||
if (_cssTestCSPTMode && _cssTestCSPTId) {
|
||||
if (isTransient) {
|
||||
wsUrl = `${protocol}//${window.location.host}/api/v1/color-strip-sources/preview/ws?token=${encodeURIComponent(apiKey)}&led_count=${ledCount}&fps=${fps}`;
|
||||
} else if (_cssTestCSPTMode && _cssTestCSPTId) {
|
||||
wsUrl = `${protocol}//${window.location.host}/api/v1/color-strip-processing-templates/${_cssTestCSPTId}/test/ws?token=${encodeURIComponent(apiKey)}&input_source_id=${encodeURIComponent(sourceId)}&led_count=${ledCount}&fps=${fps}`;
|
||||
} else {
|
||||
wsUrl = `${protocol}//${window.location.host}/api/v1/color-strip-sources/${sourceId}/test/ws?token=${encodeURIComponent(apiKey)}&led_count=${ledCount}&fps=${fps}`;
|
||||
@@ -2134,6 +2196,13 @@ function _cssTestConnect(sourceId, ledCount, fps) {
|
||||
_cssTestWs = new WebSocket(wsUrl);
|
||||
_cssTestWs.binaryType = 'arraybuffer';
|
||||
|
||||
if (isTransient) {
|
||||
_cssTestWs.onopen = () => {
|
||||
if (gen !== _cssTestGeneration) return;
|
||||
_cssTestWs.send(JSON.stringify(_cssTestTransientConfig));
|
||||
};
|
||||
}
|
||||
|
||||
_cssTestWs.onmessage = (event) => {
|
||||
// Ignore messages from a stale connection
|
||||
if (gen !== _cssTestGeneration) return;
|
||||
|
||||
@@ -29,6 +29,10 @@ export function hexToRgbArray(hex) {
|
||||
let _gradientStops = [];
|
||||
let _gradientSelectedIdx = -1;
|
||||
let _gradientDragging = null; // { idx, trackRect } while dragging
|
||||
let _gradientOnChange = null;
|
||||
|
||||
/** Set a callback that fires whenever stops change. */
|
||||
export function gradientSetOnChange(fn) { _gradientOnChange = fn; }
|
||||
|
||||
/** Read-only accessor for save/dirty-check from the parent module. */
|
||||
export function getGradientStops() {
|
||||
@@ -183,6 +187,7 @@ export function gradientRenderAll() {
|
||||
_gradientRenderCanvas();
|
||||
_gradientRenderMarkers();
|
||||
_gradientRenderStopList();
|
||||
if (_gradientOnChange) _gradientOnChange();
|
||||
}
|
||||
|
||||
function _gradientRenderCanvas() {
|
||||
|
||||
@@ -1030,6 +1030,11 @@
|
||||
"color_strip.test.fps": "FPS:",
|
||||
"color_strip.test.apply": "Apply",
|
||||
"color_strip.test.composite": "Composite",
|
||||
"color_strip.preview.title": "Live Preview",
|
||||
"color_strip.preview.not_connected": "Not connected",
|
||||
"color_strip.preview.connecting": "Connecting...",
|
||||
"color_strip.preview.connected": "Connected",
|
||||
"color_strip.preview.unsupported": "Preview not available for this source type",
|
||||
"color_strip.type.daylight": "Daylight Cycle",
|
||||
"color_strip.type.daylight.desc": "Simulates natural daylight over 24 hours",
|
||||
"color_strip.type.daylight.hint": "Simulates the sun's color temperature throughout a 24-hour day/night cycle — from warm sunrise to cool daylight to warm sunset and dim night.",
|
||||
|
||||
@@ -979,6 +979,11 @@
|
||||
"color_strip.test.fps": "FPS:",
|
||||
"color_strip.test.apply": "Применить",
|
||||
"color_strip.test.composite": "Композит",
|
||||
"color_strip.preview.title": "Предпросмотр",
|
||||
"color_strip.preview.not_connected": "Не подключено",
|
||||
"color_strip.preview.connecting": "Подключение...",
|
||||
"color_strip.preview.connected": "Подключено",
|
||||
"color_strip.preview.unsupported": "Предпросмотр недоступен для этого типа источника",
|
||||
"color_strip.type.daylight": "Дневной цикл",
|
||||
"color_strip.type.daylight.desc": "Имитация естественного дневного света за 24 часа",
|
||||
"color_strip.type.daylight.hint": "Имитирует цветовую температуру солнца в течение суток — от тёплого рассвета до прохладного дневного света, заката и ночи.",
|
||||
|
||||
@@ -979,6 +979,11 @@
|
||||
"color_strip.test.fps": "FPS:",
|
||||
"color_strip.test.apply": "应用",
|
||||
"color_strip.test.composite": "合成",
|
||||
"color_strip.preview.title": "实时预览",
|
||||
"color_strip.preview.not_connected": "未连接",
|
||||
"color_strip.preview.connecting": "连接中...",
|
||||
"color_strip.preview.connected": "已连接",
|
||||
"color_strip.preview.unsupported": "此源类型不支持预览",
|
||||
"color_strip.type.daylight": "日光循环",
|
||||
"color_strip.type.daylight.desc": "模拟24小时自然日光变化",
|
||||
"color_strip.type.daylight.hint": "模拟太阳在24小时内的色温变化——从温暖的日出到冷白的日光,再到温暖的日落和昏暗的夜晚。",
|
||||
|
||||
Reference in New Issue
Block a user