feat(color-strips): in-editor live preview for all viable source types
Extend the editor's Preview button to render unsaved form values for every CSS source type that can be previewed without external calibration. New types now supported transiently: audio, math_wave, weather, game_event, api_input, mapped, composite, processed. Backend (preview WebSocket): - Dispatch in _create_stream by source_type, injecting the dependencies each stream needs (audio managers, weather manager, value stream manager, CSPT store via public get_cspt_store, color strip stream manager). - Roll back clock + stream resources if start() fails so failed previews don't leak refs. - On source_type change mid-preview, drop the rebuilt-stream reference if rebuild fails and close the WS rather than poll a stopped stream. Stream lifecycle fixes flushed out by the new preview paths: - MappedColorStripStream and ProcessedColorStripStream now stamp a per- instance UUID into the sub-stream consumer_id so concurrent consumers (multiple preview WS connections) don't collide in the CSM registry. - ProcessedColorStripStream.update_source now re-acquires the input stream when input_source_id changes (previously silently kept the old input). Frontend: - Expand _PREVIEW_TYPES; route non-quirky types through a new exported getCSSEditorPreviewPayload helper that reuses the existing per-type handler registry. - For picture / picture_advanced / key_colors (which depend on calibration or rectangles edited elsewhere), show a clearer "save the source first" message instead of the generic "unsupported" toast.
This commit is contained in:
@@ -35,6 +35,14 @@ _PREVIEW_ALLOWED_TYPES = {
|
|||||||
"daylight",
|
"daylight",
|
||||||
"candlelight",
|
"candlelight",
|
||||||
"notification",
|
"notification",
|
||||||
|
"audio",
|
||||||
|
"math_wave",
|
||||||
|
"weather",
|
||||||
|
"game_event",
|
||||||
|
"api_input",
|
||||||
|
"mapped",
|
||||||
|
"composite",
|
||||||
|
"processed",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -92,10 +100,62 @@ async def preview_color_strip_ws(
|
|||||||
"""Instantiate and start the appropriate stream class for *source*."""
|
"""Instantiate and start the appropriate stream class for *source*."""
|
||||||
from ledgrab.core.processing.color_strip_stream_manager import _SIMPLE_STREAM_MAP
|
from ledgrab.core.processing.color_strip_stream_manager import _SIMPLE_STREAM_MAP
|
||||||
|
|
||||||
stream_cls = _SIMPLE_STREAM_MAP.get(source.source_type)
|
mgr = get_processor_manager()
|
||||||
if not stream_cls:
|
csm = mgr.color_strip_stream_manager
|
||||||
raise ValueError(f"Unsupported preview source_type: {source.source_type}")
|
|
||||||
s = stream_cls(source)
|
if source.source_type == "audio":
|
||||||
|
from ledgrab.api.dependencies import (
|
||||||
|
get_audio_processing_template_store,
|
||||||
|
get_audio_source_store,
|
||||||
|
get_audio_template_store,
|
||||||
|
)
|
||||||
|
from ledgrab.core.processing.audio_stream import AudioColorStripStream
|
||||||
|
|
||||||
|
s = AudioColorStripStream(
|
||||||
|
source,
|
||||||
|
mgr.audio_capture_manager,
|
||||||
|
get_audio_source_store(),
|
||||||
|
get_audio_template_store(),
|
||||||
|
get_audio_processing_template_store(),
|
||||||
|
)
|
||||||
|
elif source.source_type == "weather":
|
||||||
|
from ledgrab.core.processing.weather_stream import WeatherColorStripStream
|
||||||
|
|
||||||
|
s = WeatherColorStripStream(source, mgr.weather_manager)
|
||||||
|
elif source.source_type == "game_event":
|
||||||
|
from ledgrab.core.processing.game_event_stream import GameEventColorStripStream
|
||||||
|
|
||||||
|
s = GameEventColorStripStream(source)
|
||||||
|
try:
|
||||||
|
from ledgrab.api.dependencies import get_game_event_bus
|
||||||
|
|
||||||
|
bus = get_game_event_bus()
|
||||||
|
except RuntimeError as e:
|
||||||
|
logger.debug("Preview: no game event bus available: %s", e)
|
||||||
|
else:
|
||||||
|
if bus is not None:
|
||||||
|
s.set_event_bus(bus)
|
||||||
|
elif source.source_type == "mapped":
|
||||||
|
from ledgrab.core.processing.mapped_stream import MappedColorStripStream
|
||||||
|
|
||||||
|
s = MappedColorStripStream(source, csm)
|
||||||
|
elif source.source_type == "composite":
|
||||||
|
from ledgrab.api.dependencies import get_cspt_store
|
||||||
|
from ledgrab.core.processing.composite_stream import CompositeColorStripStream
|
||||||
|
|
||||||
|
s = CompositeColorStripStream(
|
||||||
|
source, csm, mgr.value_stream_manager, get_cspt_store(), depth=0
|
||||||
|
)
|
||||||
|
elif source.source_type == "processed":
|
||||||
|
from ledgrab.api.dependencies import get_cspt_store
|
||||||
|
from ledgrab.core.processing.processed_stream import ProcessedColorStripStream
|
||||||
|
|
||||||
|
s = ProcessedColorStripStream(source, csm, get_cspt_store())
|
||||||
|
else:
|
||||||
|
stream_cls = _SIMPLE_STREAM_MAP.get(source.source_type)
|
||||||
|
if not stream_cls:
|
||||||
|
raise ValueError(f"Unsupported preview source_type: {source.source_type}")
|
||||||
|
s = stream_cls(source)
|
||||||
# Inject gradient store for palette resolution
|
# Inject gradient store for palette resolution
|
||||||
if hasattr(s, "set_gradient_store"):
|
if hasattr(s, "set_gradient_store"):
|
||||||
try:
|
try:
|
||||||
@@ -121,7 +181,24 @@ async def preview_color_strip_ws(
|
|||||||
cid = None
|
cid = None
|
||||||
else:
|
else:
|
||||||
cid = None
|
cid = None
|
||||||
s.start()
|
# Start the stream; if start() raises, release any resources we
|
||||||
|
# already acquired (clock + anything the stream itself grabbed in
|
||||||
|
# its __init__) so we don't leak refs across failed previews.
|
||||||
|
try:
|
||||||
|
s.start()
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
s.stop()
|
||||||
|
except Exception as e_stop:
|
||||||
|
logger.exception("unexpected in start-failure rollback s.stop: %s", e_stop)
|
||||||
|
if cid:
|
||||||
|
scm = _get_sync_clock_manager()
|
||||||
|
if scm:
|
||||||
|
try:
|
||||||
|
scm.release(cid)
|
||||||
|
except Exception as e_rel:
|
||||||
|
logger.exception("unexpected in start-failure clock release: %s", e_rel)
|
||||||
|
raise
|
||||||
return s, cid
|
return s, cid
|
||||||
|
|
||||||
def _stop_stream(s, cid):
|
def _stop_stream(s, cid):
|
||||||
@@ -222,10 +299,24 @@ async def preview_color_strip_ws(
|
|||||||
continue
|
continue
|
||||||
new_source = _build_source(new_config)
|
new_source = _build_source(new_config)
|
||||||
if new_type != current_source_type:
|
if new_type != current_source_type:
|
||||||
# Source type changed — recreate stream
|
# Source type changed — stop the old stream first, then
|
||||||
|
# build the new one. If the rebuild fails, drop the
|
||||||
|
# reference so the frame loop doesn't keep polling a
|
||||||
|
# stopped stream and the finally-block doesn't double-stop.
|
||||||
_stop_stream(stream, clock_id)
|
_stop_stream(stream, clock_id)
|
||||||
stream, clock_id = _create_stream(new_source)
|
stream, clock_id = None, None
|
||||||
current_source_type = new_type
|
try:
|
||||||
|
stream, clock_id = _create_stream(new_source)
|
||||||
|
current_source_type = new_type
|
||||||
|
except Exception as rebuild_err:
|
||||||
|
logger.error(
|
||||||
|
f"Preview WS: failed to rebuild stream for new type {new_type}: {rebuild_err}"
|
||||||
|
)
|
||||||
|
await websocket.send_text(
|
||||||
|
_json.dumps({"type": "error", "detail": str(rebuild_err)})
|
||||||
|
)
|
||||||
|
await websocket.close(code=4003, reason=str(rebuild_err))
|
||||||
|
return
|
||||||
else:
|
else:
|
||||||
stream.update_source(new_source)
|
stream.update_source(new_source)
|
||||||
if hasattr(stream, "configure"):
|
if hasattr(stream, "configure"):
|
||||||
@@ -236,12 +327,15 @@ async def preview_color_strip_ws(
|
|||||||
await websocket.send_text(_json.dumps({"type": "error", "detail": str(e)}))
|
await websocket.send_text(_json.dumps({"type": "error", "detail": str(e)}))
|
||||||
|
|
||||||
# Send frame
|
# Send frame
|
||||||
colors = stream.get_latest_colors()
|
if stream is None:
|
||||||
if colors is not None:
|
|
||||||
await websocket.send_bytes(colors.tobytes())
|
|
||||||
else:
|
|
||||||
# Stream hasn't produced a frame yet — send black
|
|
||||||
await websocket.send_bytes(b"\x00" * led_count * 3)
|
await websocket.send_bytes(b"\x00" * led_count * 3)
|
||||||
|
else:
|
||||||
|
colors = stream.get_latest_colors()
|
||||||
|
if colors is not None:
|
||||||
|
await websocket.send_bytes(colors.tobytes())
|
||||||
|
else:
|
||||||
|
# Stream hasn't produced a frame yet — send black
|
||||||
|
await websocket.send_bytes(b"\x00" * led_count * 3)
|
||||||
|
|
||||||
except WebSocketDisconnect:
|
except WebSocketDisconnect:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -25,7 +25,13 @@ class MappedColorStripStream(ColorStripStream):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, source, css_manager):
|
def __init__(self, source, css_manager):
|
||||||
|
import uuid as _uuid
|
||||||
|
|
||||||
self._source_id: str = source.id
|
self._source_id: str = source.id
|
||||||
|
# Unique per instance so concurrent consumers don't collide on
|
||||||
|
# sub-stream consumer IDs (e.g. two preview WS connections against
|
||||||
|
# the same mapped source, or future fan-out to multiple targets).
|
||||||
|
self._instance_id: str = _uuid.uuid4().hex[:8]
|
||||||
self._zones: List[dict] = list(source.zones)
|
self._zones: List[dict] = list(source.zones)
|
||||||
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
|
||||||
@@ -127,7 +133,7 @@ class MappedColorStripStream(ColorStripStream):
|
|||||||
src_id = zone.get("source_id", "")
|
src_id = zone.get("source_id", "")
|
||||||
if not src_id:
|
if not src_id:
|
||||||
continue
|
continue
|
||||||
consumer_id = f"{self._source_id}__zone_{i}"
|
consumer_id = f"{self._source_id}__{self._instance_id}__zone_{i}"
|
||||||
try:
|
try:
|
||||||
stream = self._css_manager.acquire(src_id, consumer_id)
|
stream = self._css_manager.acquire(src_id, consumer_id)
|
||||||
zone_len = self._zone_length(zone)
|
zone_len = self._zone_length(zone)
|
||||||
|
|||||||
@@ -22,11 +22,17 @@ class ProcessedColorStripStream(ColorStripStream):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, source, css_manager, cspt_store=None):
|
def __init__(self, source, css_manager, cspt_store=None):
|
||||||
|
import uuid as _uuid
|
||||||
|
|
||||||
self._source = source
|
self._source = source
|
||||||
self._css_manager = css_manager
|
self._css_manager = css_manager
|
||||||
self._cspt_store = cspt_store
|
self._cspt_store = cspt_store
|
||||||
self._input_stream: Optional[ColorStripStream] = None
|
self._input_stream: Optional[ColorStripStream] = None
|
||||||
self._consumer_id = f"__processed_{source.id}__"
|
# Unique per instance so concurrent consumers don't collide on
|
||||||
|
# sub-stream consumer IDs (e.g. two preview WS connections against
|
||||||
|
# the same processed source).
|
||||||
|
self._instance_id = _uuid.uuid4().hex[:8]
|
||||||
|
self._consumer_id = f"__processed_{source.id}__{self._instance_id}__"
|
||||||
self._filters = []
|
self._filters = []
|
||||||
self._cached_template_id = None
|
self._cached_template_id = None
|
||||||
self._running = False
|
self._running = False
|
||||||
@@ -97,9 +103,28 @@ class ProcessedColorStripStream(ColorStripStream):
|
|||||||
return self._colors
|
return self._colors
|
||||||
|
|
||||||
def update_source(self, source) -> None:
|
def update_source(self, source) -> None:
|
||||||
|
old_input = self._source.input_source_id
|
||||||
|
new_input = source.input_source_id
|
||||||
self._source = source
|
self._source = source
|
||||||
# Force re-resolve filters on next iteration
|
# Force re-resolve filters on next iteration
|
||||||
self._cached_template_id = None
|
self._cached_template_id = None
|
||||||
|
# If the input source changed while running, swap the acquired stream.
|
||||||
|
if self._running and new_input != old_input:
|
||||||
|
if self._input_stream and old_input:
|
||||||
|
try:
|
||||||
|
self._css_manager.release(old_input, self._consumer_id)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
f"Processed update: release of old input {old_input} failed: {e}"
|
||||||
|
)
|
||||||
|
self._input_stream = None
|
||||||
|
if new_input:
|
||||||
|
try:
|
||||||
|
self._input_stream = self._css_manager.acquire(new_input, self._consumer_id)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
f"Processed update: acquire of new input {new_input} failed: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
def set_clock(self, clock_runtime) -> None:
|
def set_clock(self, clock_runtime) -> None:
|
||||||
if self._input_stream and hasattr(self._input_stream, "set_clock"):
|
if self._input_stream and hasattr(self._input_stream, "set_clock"):
|
||||||
|
|||||||
@@ -1405,6 +1405,26 @@ const _typeHandlers: Record<string, { load: (...args: any[]) => any; reset: (...
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
// Preview payload — collects the current editor form into a config dict
|
||||||
|
// suitable for the transient preview WebSocket. Returns null if the type
|
||||||
|
// has no handler or if validation fails. Used by test.ts.
|
||||||
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
export function getCSSEditorPreviewPayload(sourceType: string): any {
|
||||||
|
const handler = _typeHandlers[sourceType];
|
||||||
|
if (!handler) return null;
|
||||||
|
const payload = handler.getPayload('__preview__');
|
||||||
|
if (!payload) return null;
|
||||||
|
payload.source_type = sourceType;
|
||||||
|
const clockTypes = ['static', 'gradient', 'effect', 'daylight', 'candlelight', 'weather', 'math_wave'];
|
||||||
|
if (clockTypes.includes(sourceType)) {
|
||||||
|
const clockEl = document.getElementById('css-editor-clock') as HTMLInputElement | null;
|
||||||
|
if (clockEl && clockEl.value) payload.clock_id = clockEl.value;
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════
|
||||||
// Editor open / close / save
|
// Editor open / close / save
|
||||||
// ══════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -15,14 +15,23 @@ import {
|
|||||||
} from '../../core/icons.ts';
|
} from '../../core/icons.ts';
|
||||||
import { EntitySelect } from '../../core/entity-palette.ts';
|
import { EntitySelect } from '../../core/entity-palette.ts';
|
||||||
import { hexToRgbArray, getGradientStops } from '../css-gradient-editor.ts';
|
import { hexToRgbArray, getGradientStops } from '../css-gradient-editor.ts';
|
||||||
import { testNotification, _getAnimationPayload } from './index.ts';
|
import { testNotification, _getAnimationPayload, getCSSEditorPreviewPayload } from './index.ts';
|
||||||
import { notificationGetAppColorsDict, notificationGetAppSoundsDict, getNotificationDurationValue, getNotificationVolumeValue } from './notification.ts';
|
import { notificationGetAppColorsDict, notificationGetAppSoundsDict, getNotificationDurationValue, getNotificationVolumeValue } from './notification.ts';
|
||||||
|
import {
|
||||||
|
ensureAudioSensitivityWidget,
|
||||||
|
ensureAudioSmoothingWidget,
|
||||||
|
ensureAudioBeatDecayWidget,
|
||||||
|
ensureAudioColorWidget,
|
||||||
|
ensureAudioColorPeakWidget,
|
||||||
|
} from './audio.ts';
|
||||||
|
import { ensureMathWaveSpeedWidget, mathWaveGetLayers } from './math-wave.ts';
|
||||||
import { openAuthedWs } from '../../core/ws-auth.ts';
|
import { openAuthedWs } from '../../core/ws-auth.ts';
|
||||||
|
|
||||||
/* ── Preview config builder ───────────────────────────────────── */
|
/* ── Preview config builder ───────────────────────────────────── */
|
||||||
|
|
||||||
const _PREVIEW_TYPES = new Set([
|
const _PREVIEW_TYPES = new Set([
|
||||||
'static', 'gradient', 'effect', 'daylight', 'candlelight', 'notification',
|
'static', 'gradient', 'effect', 'daylight', 'candlelight', 'notification', 'audio', 'math_wave',
|
||||||
|
'weather', 'game_event', 'api_input', 'mapped', 'composite', 'processed',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function _collectPreviewConfig() {
|
function _collectPreviewConfig() {
|
||||||
@@ -42,6 +51,28 @@ function _collectPreviewConfig() {
|
|||||||
config = { source_type: 'daylight', speed: parseFloat((document.getElementById('css-editor-daylight-speed') as HTMLInputElement).value), use_real_time: (document.getElementById('css-editor-daylight-real-time') as HTMLInputElement).checked, latitude: parseFloat((document.getElementById('css-editor-daylight-latitude') as HTMLInputElement).value), longitude: parseFloat((document.getElementById('css-editor-daylight-longitude') as HTMLInputElement).value) };
|
config = { source_type: 'daylight', speed: parseFloat((document.getElementById('css-editor-daylight-speed') as HTMLInputElement).value), use_real_time: (document.getElementById('css-editor-daylight-real-time') as HTMLInputElement).checked, latitude: parseFloat((document.getElementById('css-editor-daylight-latitude') as HTMLInputElement).value), longitude: parseFloat((document.getElementById('css-editor-daylight-longitude') as HTMLInputElement).value) };
|
||||||
} else if (sourceType === 'candlelight') {
|
} else if (sourceType === 'candlelight') {
|
||||||
config = { source_type: 'candlelight', color: hexToRgbArray((document.getElementById('css-editor-candlelight-color') as HTMLInputElement).value), intensity: parseFloat((document.getElementById('css-editor-candlelight-intensity') as HTMLInputElement).value), num_candles: parseInt((document.getElementById('css-editor-candlelight-num-candles') as HTMLInputElement).value) || 3, speed: parseFloat((document.getElementById('css-editor-candlelight-speed') as HTMLInputElement).value), wind_strength: parseFloat((document.getElementById('css-editor-candlelight-wind') as HTMLInputElement).value), candle_type: (document.getElementById('css-editor-candlelight-type') as HTMLSelectElement).value };
|
config = { source_type: 'candlelight', color: hexToRgbArray((document.getElementById('css-editor-candlelight-color') as HTMLInputElement).value), intensity: parseFloat((document.getElementById('css-editor-candlelight-intensity') as HTMLInputElement).value), num_candles: parseInt((document.getElementById('css-editor-candlelight-num-candles') as HTMLInputElement).value) || 3, speed: parseFloat((document.getElementById('css-editor-candlelight-speed') as HTMLInputElement).value), wind_strength: parseFloat((document.getElementById('css-editor-candlelight-wind') as HTMLInputElement).value), candle_type: (document.getElementById('css-editor-candlelight-type') as HTMLSelectElement).value };
|
||||||
|
} else if (sourceType === 'math_wave') {
|
||||||
|
const waves = mathWaveGetLayers();
|
||||||
|
if (waves.length === 0) return null;
|
||||||
|
config = {
|
||||||
|
source_type: 'math_wave',
|
||||||
|
gradient_id: (document.getElementById('css-editor-math-wave-gradient') as HTMLInputElement).value || null,
|
||||||
|
speed: ensureMathWaveSpeedWidget().getValue(),
|
||||||
|
waves,
|
||||||
|
};
|
||||||
|
} else if (sourceType === 'audio') {
|
||||||
|
config = {
|
||||||
|
source_type: 'audio',
|
||||||
|
visualization_mode: (document.getElementById('css-editor-audio-viz') as HTMLInputElement).value,
|
||||||
|
audio_source_id: (document.getElementById('css-editor-audio-source') as HTMLInputElement).value || '',
|
||||||
|
sensitivity: ensureAudioSensitivityWidget().getValue(),
|
||||||
|
smoothing: ensureAudioSmoothingWidget().getValue(),
|
||||||
|
beat_decay: ensureAudioBeatDecayWidget().getValue(),
|
||||||
|
gradient_id: (document.getElementById('css-editor-audio-palette') as HTMLInputElement).value,
|
||||||
|
color: ensureAudioColorWidget().getValue(),
|
||||||
|
color_peak: ensureAudioColorPeakWidget().getValue(),
|
||||||
|
mirror: (document.getElementById('css-editor-audio-mirror') as HTMLInputElement).checked,
|
||||||
|
};
|
||||||
} else if (sourceType === 'notification') {
|
} else if (sourceType === 'notification') {
|
||||||
const filterList = (document.getElementById('css-editor-notification-filter-list') as HTMLInputElement).value.split('\n').map(s => s.trim()).filter(Boolean);
|
const filterList = (document.getElementById('css-editor-notification-filter-list') as HTMLInputElement).value.split('\n').map(s => s.trim()).filter(Boolean);
|
||||||
config = {
|
config = {
|
||||||
@@ -56,9 +87,13 @@ function _collectPreviewConfig() {
|
|||||||
sound_volume: getNotificationVolumeValue(),
|
sound_volume: getNotificationVolumeValue(),
|
||||||
app_sounds: notificationGetAppSoundsDict(),
|
app_sounds: notificationGetAppSoundsDict(),
|
||||||
};
|
};
|
||||||
|
} else {
|
||||||
|
// Types without quirks: build config from the editor handler payload
|
||||||
|
config = getCSSEditorPreviewPayload(sourceType);
|
||||||
|
if (!config) return null;
|
||||||
}
|
}
|
||||||
const clockEl = document.getElementById('css-editor-clock') as HTMLSelectElement | null;
|
const clockEl = document.getElementById('css-editor-clock') as HTMLSelectElement | null;
|
||||||
if (clockEl && clockEl.value) config.clock_id = clockEl.value;
|
if (clockEl && clockEl.value && !config.clock_id) config.clock_id = clockEl.value;
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,14 +102,29 @@ function _collectPreviewConfig() {
|
|||||||
* For saved sources, uses the normal test endpoint.
|
* For saved sources, uses the normal test endpoint.
|
||||||
* For unsaved/self-contained types, uses the transient preview endpoint.
|
* For unsaved/self-contained types, uses the transient preview endpoint.
|
||||||
*/
|
*/
|
||||||
|
// Types whose preview needs data edited outside the source-editor form
|
||||||
|
// (calibration on a separate Calibration screen, or rectangles in the
|
||||||
|
// key-colors region picker). The Preview button falls back to the saved
|
||||||
|
// source for these; if the source isn't saved yet, prompt the user to save
|
||||||
|
// first instead of showing the generic "unsupported" message.
|
||||||
|
const _NEEDS_SAVE_FIRST = new Set(['picture', 'picture_advanced', 'key_colors']);
|
||||||
|
|
||||||
export function previewCSSFromEditor() {
|
export function previewCSSFromEditor() {
|
||||||
// Always use transient preview with current form values
|
// Always use transient preview with current form values
|
||||||
|
const sourceType = (document.getElementById('css-editor-type') as HTMLInputElement).value;
|
||||||
const config = _collectPreviewConfig();
|
const config = _collectPreviewConfig();
|
||||||
if (!config) {
|
if (!config) {
|
||||||
// Non-previewable type (picture, composite, etc.) — fall back to saved source test
|
// Either the type can't be previewed transiently
|
||||||
|
// (picture, picture_advanced, key_colors) or handler validation
|
||||||
|
// failed (already showed error in modal).
|
||||||
const cssId = (document.getElementById('css-editor-id') as HTMLInputElement).value;
|
const cssId = (document.getElementById('css-editor-id') as HTMLInputElement).value;
|
||||||
if (cssId) { testColorStrip(cssId); return; }
|
if (!_PREVIEW_TYPES.has(sourceType) && cssId) { testColorStrip(cssId); return; }
|
||||||
showToast(t('color_strip.preview.unsupported'), 'info');
|
if (!_PREVIEW_TYPES.has(sourceType)) {
|
||||||
|
const key = _NEEDS_SAVE_FIRST.has(sourceType)
|
||||||
|
? 'color_strip.preview.save_first'
|
||||||
|
: 'color_strip.preview.unsupported';
|
||||||
|
showToast(t(key), 'info');
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1489,6 +1489,7 @@
|
|||||||
"color_strip.preview.connecting": "Connecting...",
|
"color_strip.preview.connecting": "Connecting...",
|
||||||
"color_strip.preview.connected": "Connected",
|
"color_strip.preview.connected": "Connected",
|
||||||
"color_strip.preview.unsupported": "Preview not available for this source type",
|
"color_strip.preview.unsupported": "Preview not available for this source type",
|
||||||
|
"color_strip.preview.save_first": "Save the source first — calibration is needed for preview",
|
||||||
"color_strip.type.daylight": "Daylight Cycle",
|
"color_strip.type.daylight": "Daylight Cycle",
|
||||||
"color_strip.type.daylight.desc": "Simulates natural daylight over 24 hours",
|
"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.",
|
"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.",
|
||||||
|
|||||||
@@ -1501,6 +1501,7 @@
|
|||||||
"color_strip.preview.connecting": "Подключение...",
|
"color_strip.preview.connecting": "Подключение...",
|
||||||
"color_strip.preview.connected": "Подключено",
|
"color_strip.preview.connected": "Подключено",
|
||||||
"color_strip.preview.unsupported": "Предпросмотр недоступен для этого типа источника",
|
"color_strip.preview.unsupported": "Предпросмотр недоступен для этого типа источника",
|
||||||
|
"color_strip.preview.save_first": "Сначала сохраните источник — для предпросмотра нужна калибровка",
|
||||||
"color_strip.type.daylight": "Дневной цикл",
|
"color_strip.type.daylight": "Дневной цикл",
|
||||||
"color_strip.type.daylight.desc": "Имитация естественного дневного света за 24 часа",
|
"color_strip.type.daylight.desc": "Имитация естественного дневного света за 24 часа",
|
||||||
"color_strip.type.daylight.hint": "Имитирует цветовую температуру солнца в течение суток — от тёплого рассвета до прохладного дневного света, заката и ночи.",
|
"color_strip.type.daylight.hint": "Имитирует цветовую температуру солнца в течение суток — от тёплого рассвета до прохладного дневного света, заката и ночи.",
|
||||||
|
|||||||
@@ -1498,6 +1498,7 @@
|
|||||||
"color_strip.preview.connecting": "连接中...",
|
"color_strip.preview.connecting": "连接中...",
|
||||||
"color_strip.preview.connected": "已连接",
|
"color_strip.preview.connected": "已连接",
|
||||||
"color_strip.preview.unsupported": "此源类型不支持预览",
|
"color_strip.preview.unsupported": "此源类型不支持预览",
|
||||||
|
"color_strip.preview.save_first": "请先保存源——预览需要校准数据",
|
||||||
"color_strip.type.daylight": "日光循环",
|
"color_strip.type.daylight": "日光循环",
|
||||||
"color_strip.type.daylight.desc": "模拟24小时自然日光变化",
|
"color_strip.type.daylight.desc": "模拟24小时自然日光变化",
|
||||||
"color_strip.type.daylight.hint": "模拟太阳在24小时内的色温变化——从温暖的日出到冷白的日光,再到温暖的日落和昏暗的夜晚。",
|
"color_strip.type.daylight.hint": "模拟太阳在24小时内的色温变化——从温暖的日出到冷白的日光,再到温暖的日落和昏暗的夜晚。",
|
||||||
|
|||||||
Reference in New Issue
Block a user