Remove per-source speed, fix device dirty check, and add frontend caching
Speed is now exclusively controlled via sync clocks — CSS sources no longer carry their own speed/cycle_speed fields. Streams default to 1.0× when no clock is assigned. Also fixes false-positive dirty check on the device settings modal (array reference comparison) and converts several frontend modules to use DataCache for consistent API response caching. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -74,9 +74,7 @@ def _css_to_response(source, overlay_active: bool = False) -> ColorStripSourceRe
|
||||
color=getattr(source, "color", None),
|
||||
stops=stops,
|
||||
colors=getattr(source, "colors", None),
|
||||
cycle_speed=getattr(source, "cycle_speed", None),
|
||||
effect_type=getattr(source, "effect_type", None),
|
||||
speed=getattr(source, "speed", None),
|
||||
palette=getattr(source, "palette", None),
|
||||
intensity=getattr(source, "intensity", None),
|
||||
scale=getattr(source, "scale", None),
|
||||
@@ -163,9 +161,7 @@ async def create_color_strip_source(
|
||||
frame_interpolation=data.frame_interpolation,
|
||||
animation=data.animation.model_dump() if data.animation else None,
|
||||
colors=data.colors,
|
||||
cycle_speed=data.cycle_speed,
|
||||
effect_type=data.effect_type,
|
||||
speed=data.speed,
|
||||
palette=data.palette,
|
||||
intensity=data.intensity,
|
||||
scale=data.scale,
|
||||
@@ -241,9 +237,7 @@ async def update_color_strip_source(
|
||||
frame_interpolation=data.frame_interpolation,
|
||||
animation=data.animation.model_dump() if data.animation else None,
|
||||
colors=data.colors,
|
||||
cycle_speed=data.cycle_speed,
|
||||
effect_type=data.effect_type,
|
||||
speed=data.speed,
|
||||
palette=data.palette,
|
||||
intensity=data.intensity,
|
||||
scale=data.scale,
|
||||
|
||||
@@ -64,10 +64,8 @@ class ColorStripSourceCreate(BaseModel):
|
||||
stops: Optional[List[ColorStop]] = Field(None, description="Color stops for gradient type")
|
||||
# color_cycle-type fields
|
||||
colors: Optional[List[List[int]]] = Field(None, description="List of [R,G,B] colors to cycle (color_cycle type)")
|
||||
cycle_speed: Optional[float] = Field(None, description="Cycle speed multiplier 0.1–10.0 (color_cycle type)", ge=0.1, le=10.0)
|
||||
# effect-type fields
|
||||
effect_type: Optional[str] = Field(None, description="Effect algorithm: fire|meteor|plasma|noise|aurora")
|
||||
speed: Optional[float] = Field(None, description="Effect speed multiplier 0.1-10.0", ge=0.1, le=10.0)
|
||||
palette: Optional[str] = Field(None, description="Named palette (fire/ocean/lava/forest/rainbow/aurora/sunset/ice)")
|
||||
intensity: Optional[float] = Field(None, description="Effect intensity 0.1-2.0", ge=0.1, le=2.0)
|
||||
scale: Optional[float] = Field(None, description="Spatial scale 0.5-5.0", ge=0.5, le=5.0)
|
||||
@@ -111,10 +109,8 @@ class ColorStripSourceUpdate(BaseModel):
|
||||
stops: Optional[List[ColorStop]] = Field(None, description="Color stops for gradient type")
|
||||
# color_cycle-type fields
|
||||
colors: Optional[List[List[int]]] = Field(None, description="List of [R,G,B] colors to cycle (color_cycle type)")
|
||||
cycle_speed: Optional[float] = Field(None, description="Cycle speed multiplier 0.1–10.0 (color_cycle type)", ge=0.1, le=10.0)
|
||||
# effect-type fields
|
||||
effect_type: Optional[str] = Field(None, description="Effect algorithm: fire|meteor|plasma|noise|aurora")
|
||||
speed: Optional[float] = Field(None, description="Effect speed multiplier 0.1-10.0", ge=0.1, le=10.0)
|
||||
palette: Optional[str] = Field(None, description="Named palette")
|
||||
intensity: Optional[float] = Field(None, description="Effect intensity 0.1-2.0", ge=0.1, le=2.0)
|
||||
scale: Optional[float] = Field(None, description="Spatial scale 0.5-5.0", ge=0.5, le=5.0)
|
||||
@@ -160,10 +156,8 @@ class ColorStripSourceResponse(BaseModel):
|
||||
stops: Optional[List[ColorStop]] = Field(None, description="Color stops for gradient type")
|
||||
# color_cycle-type fields
|
||||
colors: Optional[List[List[int]]] = Field(None, description="List of [R,G,B] colors to cycle (color_cycle type)")
|
||||
cycle_speed: Optional[float] = Field(None, description="Cycle speed multiplier (color_cycle type)")
|
||||
# effect-type fields
|
||||
effect_type: Optional[str] = Field(None, description="Effect algorithm")
|
||||
speed: Optional[float] = Field(None, description="Effect speed multiplier")
|
||||
palette: Optional[str] = Field(None, description="Named palette")
|
||||
intensity: Optional[float] = Field(None, description="Effect intensity")
|
||||
scale: Optional[float] = Field(None, description="Spatial scale")
|
||||
|
||||
@@ -684,7 +684,7 @@ class StaticColorStripStream(ColorStripStream):
|
||||
speed = clock.speed
|
||||
t = clock.get_time()
|
||||
else:
|
||||
speed = float(anim.get("speed", 1.0))
|
||||
speed = 1.0
|
||||
t = wall_start
|
||||
atype = anim.get("type", "breathing")
|
||||
n = self._led_count
|
||||
@@ -798,7 +798,6 @@ class ColorCycleColorStripStream(ColorStripStream):
|
||||
self._color_list = [
|
||||
c for c in raw if isinstance(c, list) and len(c) == 3
|
||||
] or default
|
||||
self._cycle_speed = float(source.cycle_speed) if source.cycle_speed else 1.0
|
||||
self._auto_size = not source.led_count
|
||||
self._led_count = source.led_count if source.led_count > 0 else 1
|
||||
self._rebuild_colors()
|
||||
@@ -892,7 +891,7 @@ class ColorCycleColorStripStream(ColorStripStream):
|
||||
speed = clock.speed
|
||||
t = clock.get_time()
|
||||
else:
|
||||
speed = self._cycle_speed
|
||||
speed = 1.0
|
||||
t = wall_start
|
||||
n = self._led_count
|
||||
num = len(color_list)
|
||||
@@ -1067,7 +1066,7 @@ class GradientColorStripStream(ColorStripStream):
|
||||
speed = clock.speed
|
||||
t = clock.get_time()
|
||||
else:
|
||||
speed = float(anim.get("speed", 1.0))
|
||||
speed = 1.0
|
||||
t = wall_start
|
||||
atype = anim.get("type", "breathing")
|
||||
n = self._led_count
|
||||
|
||||
@@ -203,7 +203,6 @@ class EffectColorStripStream(ColorStripStream):
|
||||
|
||||
def _update_from_source(self, source) -> None:
|
||||
self._effect_type = getattr(source, "effect_type", "fire")
|
||||
self._speed = float(getattr(source, "speed", 1.0))
|
||||
self._auto_size = not source.led_count
|
||||
self._led_count = source.led_count if source.led_count and source.led_count > 0 else 1
|
||||
self._palette_name = getattr(source, "palette", None) or _EFFECT_DEFAULT_PALETTE.get(self._effect_type, "fire")
|
||||
@@ -307,7 +306,7 @@ class EffectColorStripStream(ColorStripStream):
|
||||
self._effective_speed = clock.speed
|
||||
else:
|
||||
anim_time = wall_start
|
||||
self._effective_speed = self._speed
|
||||
self._effective_speed = 1.0
|
||||
|
||||
n = self._led_count
|
||||
if n != _pool_n:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
calibrationTestState, EDGE_TEST_COLORS,
|
||||
calibrationTestState, EDGE_TEST_COLORS, displaysCache,
|
||||
} from '../core/state.js';
|
||||
import { API_BASE, getHeaders, fetchWithAuth } from '../core/api.js';
|
||||
import { t } from '../core/i18n.js';
|
||||
@@ -134,9 +134,9 @@ export async function toggleCalibrationOverlay() {
|
||||
|
||||
export async function showCalibration(deviceId) {
|
||||
try {
|
||||
const [response, displaysResponse] = await Promise.all([
|
||||
const [response, displays] = await Promise.all([
|
||||
fetchWithAuth(`/devices/${deviceId}`),
|
||||
fetchWithAuth('/config/displays'),
|
||||
displaysCache.fetch().catch(() => []),
|
||||
]);
|
||||
|
||||
if (!response.ok) { showToast(t('calibration.error.load_failed'), 'error'); return; }
|
||||
@@ -145,18 +145,13 @@ export async function showCalibration(deviceId) {
|
||||
const calibration = device.calibration;
|
||||
|
||||
const preview = document.querySelector('.calibration-preview');
|
||||
if (displaysResponse.ok) {
|
||||
const displaysData = await displaysResponse.json();
|
||||
const displayIndex = device.settings?.display_index ?? 0;
|
||||
const display = (displaysData.displays || []).find(d => d.index === displayIndex);
|
||||
const display = displays.find(d => d.index === displayIndex);
|
||||
if (display && display.width && display.height) {
|
||||
preview.style.aspectRatio = `${display.width} / ${display.height}`;
|
||||
} else {
|
||||
preview.style.aspectRatio = '';
|
||||
}
|
||||
} else {
|
||||
preview.style.aspectRatio = '';
|
||||
}
|
||||
|
||||
document.getElementById('calibration-device-id').value = device.id;
|
||||
document.getElementById('cal-device-led-count-inline').textContent = device.led_count;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import { fetchWithAuth, escapeHtml } from '../core/api.js';
|
||||
import { _cachedSyncClocks } from '../core/state.js';
|
||||
import { _cachedSyncClocks, audioSourcesCache, streamsCache } from '../core/state.js';
|
||||
import { t } from '../core/i18n.js';
|
||||
import { showToast, showConfirm } from '../core/ui.js';
|
||||
import { Modal } from '../core/modal.js';
|
||||
@@ -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_FAST_FORWARD, ICON_ACTIVITY, ICON_CLOCK,
|
||||
ICON_LINK, ICON_SPARKLES, ICON_ACTIVITY, ICON_CLOCK,
|
||||
} from '../core/icons.js';
|
||||
import { wrapCard } from '../core/card-colors.js';
|
||||
|
||||
@@ -37,11 +37,8 @@ class CSSEditorModal extends Modal {
|
||||
led_count: document.getElementById('css-editor-led-count').value,
|
||||
gradient_stops: type === 'gradient' ? JSON.stringify(_gradientStops) : '[]',
|
||||
animation_type: document.getElementById('css-editor-animation-type').value,
|
||||
animation_speed: document.getElementById('css-editor-animation-speed').value,
|
||||
cycle_speed: document.getElementById('css-editor-cycle-speed').value,
|
||||
cycle_colors: JSON.stringify(_colorCycleColors),
|
||||
effect_type: document.getElementById('css-editor-effect-type').value,
|
||||
effect_speed: document.getElementById('css-editor-effect-speed').value,
|
||||
effect_palette: document.getElementById('css-editor-effect-palette').value,
|
||||
effect_color: document.getElementById('css-editor-effect-color').value,
|
||||
effect_intensity: document.getElementById('css-editor-effect-intensity').value,
|
||||
@@ -141,16 +138,7 @@ function _populateClockDropdown(selectedId) {
|
||||
}
|
||||
|
||||
export function onCSSClockChange() {
|
||||
// When a clock is selected, hide speed sliders (speed comes from clock)
|
||||
const clockId = document.getElementById('css-editor-clock').value;
|
||||
const type = document.getElementById('css-editor-type').value;
|
||||
if (type === 'effect') {
|
||||
document.getElementById('css-editor-effect-speed-group').style.display = clockId ? 'none' : '';
|
||||
} else if (type === 'color_cycle') {
|
||||
document.getElementById('css-editor-cycle-speed-group').style.display = clockId ? 'none' : '';
|
||||
} else if (type === 'static' || type === 'gradient') {
|
||||
document.getElementById('css-editor-animation-speed-group').style.display = clockId ? 'none' : '';
|
||||
}
|
||||
// No-op: speed sliders removed; speed is now clock-only
|
||||
}
|
||||
|
||||
function _getAnimationPayload() {
|
||||
@@ -158,15 +146,10 @@ function _getAnimationPayload() {
|
||||
return {
|
||||
enabled: type !== 'none',
|
||||
type: type !== 'none' ? type : 'breathing',
|
||||
speed: parseFloat(document.getElementById('css-editor-animation-speed').value),
|
||||
};
|
||||
}
|
||||
|
||||
function _loadAnimationState(anim) {
|
||||
const speedEl = document.getElementById('css-editor-animation-speed');
|
||||
speedEl.value = (anim && anim.speed != null) ? anim.speed : 1.0;
|
||||
document.getElementById('css-editor-animation-speed-val').textContent =
|
||||
parseFloat(speedEl.value).toFixed(1);
|
||||
// Set type after onCSSTypeChange() has populated the dropdown
|
||||
if (anim && anim.enabled && anim.type) {
|
||||
document.getElementById('css-editor-animation-type').value = anim.type;
|
||||
@@ -182,8 +165,6 @@ export function onAnimationTypeChange() {
|
||||
|
||||
function _syncAnimationSpeedState() {
|
||||
const type = document.getElementById('css-editor-animation-type').value;
|
||||
const isNone = type === 'none';
|
||||
document.getElementById('css-editor-animation-speed').disabled = isNone;
|
||||
const descEl = document.getElementById('css-editor-animation-type-desc');
|
||||
if (descEl) {
|
||||
const desc = t('color_strip.animation.type.' + type + '.desc') || '';
|
||||
@@ -302,13 +283,6 @@ function _loadColorCycleState(css) {
|
||||
? raw.map(c => rgbArrayToHex(c))
|
||||
: [..._DEFAULT_CYCLE_COLORS];
|
||||
_colorCycleRenderList();
|
||||
const speed = (css && css.cycle_speed != null) ? css.cycle_speed : 1.0;
|
||||
const speedEl = document.getElementById('css-editor-cycle-speed');
|
||||
if (speedEl) {
|
||||
speedEl.value = speed;
|
||||
document.getElementById('css-editor-cycle-speed-val').textContent =
|
||||
parseFloat(speed).toFixed(1);
|
||||
}
|
||||
}
|
||||
|
||||
/** Convert an [R, G, B] array to a CSS hex color string like "#rrggbb". */
|
||||
@@ -553,10 +527,7 @@ async function _loadAudioSources() {
|
||||
const select = document.getElementById('css-editor-audio-source');
|
||||
if (!select) return;
|
||||
try {
|
||||
const resp = await fetchWithAuth('/audio-sources');
|
||||
if (!resp.ok) return;
|
||||
const data = await resp.json();
|
||||
const sources = data.sources || [];
|
||||
const sources = await audioSourcesCache.fetch();
|
||||
select.innerHTML = sources.map(s => {
|
||||
const badge = s.source_type === 'multichannel' ? ' [multichannel]' : ' [mono]';
|
||||
return `<option value="${s.id}">${escapeHtml(s.name)}${badge}</option>`;
|
||||
@@ -626,7 +597,6 @@ export function createColorStripCard(source, pictureSourceMap, audioSourceMap) {
|
||||
const anim = (isStatic || isGradient) && source.animation && source.animation.enabled ? source.animation : null;
|
||||
const animBadge = anim
|
||||
? `<span class="stream-card-prop" title="${t('color_strip.animation')}">${ICON_SPARKLES} ${t('color_strip.animation.type.' + anim.type) || anim.type}</span>`
|
||||
+ (source.clock_id ? '' : `<span class="stream-card-prop" title="${t('color_strip.animation.speed')}">${ICON_FAST_FORWARD} ${(anim.speed || 1.0).toFixed(1)}×</span>`)
|
||||
: '';
|
||||
|
||||
let propsHtml;
|
||||
@@ -647,7 +617,6 @@ export function createColorStripCard(source, pictureSourceMap, audioSourceMap) {
|
||||
).join('');
|
||||
propsHtml = `
|
||||
<span class="stream-card-prop">${swatches}</span>
|
||||
${source.clock_id ? '' : `<span class="stream-card-prop" title="${t('color_strip.color_cycle.speed')}">${ICON_FAST_FORWARD} ${(source.cycle_speed || 1.0).toFixed(1)}×</span>`}
|
||||
${source.led_count ? `<span class="stream-card-prop" title="${t('color_strip.leds')}">${ICON_LED} ${source.led_count}</span>` : ''}
|
||||
${clockBadge}
|
||||
`;
|
||||
@@ -680,7 +649,6 @@ export function createColorStripCard(source, pictureSourceMap, audioSourceMap) {
|
||||
propsHtml = `
|
||||
<span class="stream-card-prop">${ICON_FPS} ${escapeHtml(effectLabel)}</span>
|
||||
${paletteLabel ? `<span class="stream-card-prop" title="${t('color_strip.effect.palette')}">${ICON_PALETTE} ${escapeHtml(paletteLabel)}</span>` : ''}
|
||||
${source.clock_id ? '' : `<span class="stream-card-prop" title="${t('color_strip.effect.speed')}">${ICON_FAST_FORWARD} ${(source.speed || 1.0).toFixed(1)}×</span>`}
|
||||
${source.led_count ? `<span class="stream-card-prop" title="${t('color_strip.leds')}">${ICON_LED} ${source.led_count}</span>` : ''}
|
||||
${clockBadge}
|
||||
`;
|
||||
@@ -771,8 +739,7 @@ export function createColorStripCard(source, pictureSourceMap, audioSourceMap) {
|
||||
|
||||
export async function showCSSEditor(cssId = null, cloneData = null) {
|
||||
try {
|
||||
const sourcesResp = await fetchWithAuth('/picture-sources');
|
||||
const sources = sourcesResp.ok ? ((await sourcesResp.json()).streams || []) : [];
|
||||
const sources = await streamsCache.fetch();
|
||||
|
||||
// Fetch all color strip sources for composite layer dropdowns
|
||||
const cssListResp = await fetchWithAuth('/color-strip-sources');
|
||||
@@ -821,8 +788,6 @@ export async function showCSSEditor(cssId = null, cloneData = null) {
|
||||
} else if (sourceType === 'effect') {
|
||||
document.getElementById('css-editor-effect-type').value = css.effect_type || 'fire';
|
||||
onEffectTypeChange();
|
||||
document.getElementById('css-editor-effect-speed').value = css.speed ?? 1.0;
|
||||
document.getElementById('css-editor-effect-speed-val').textContent = parseFloat(css.speed ?? 1.0).toFixed(1);
|
||||
document.getElementById('css-editor-effect-palette').value = css.palette || 'fire';
|
||||
document.getElementById('css-editor-effect-color').value = rgbArrayToHex(css.color || [255, 80, 0]);
|
||||
document.getElementById('css-editor-effect-intensity').value = css.intensity ?? 1.0;
|
||||
@@ -920,8 +885,6 @@ export async function showCSSEditor(cssId = null, cloneData = null) {
|
||||
_loadAnimationState(null);
|
||||
_loadColorCycleState(null);
|
||||
document.getElementById('css-editor-effect-type').value = 'fire';
|
||||
document.getElementById('css-editor-effect-speed').value = 1.0;
|
||||
document.getElementById('css-editor-effect-speed-val').textContent = '1.0';
|
||||
document.getElementById('css-editor-effect-palette').value = 'fire';
|
||||
document.getElementById('css-editor-effect-color').value = '#ff5000';
|
||||
document.getElementById('css-editor-effect-intensity').value = 1.0;
|
||||
@@ -988,7 +951,6 @@ export async function saveCSSEditor() {
|
||||
payload = {
|
||||
name,
|
||||
colors: cycleColors,
|
||||
cycle_speed: parseFloat(document.getElementById('css-editor-cycle-speed').value),
|
||||
};
|
||||
if (!cssId) payload.source_type = 'color_cycle';
|
||||
} else if (sourceType === 'gradient') {
|
||||
@@ -1010,7 +972,6 @@ export async function saveCSSEditor() {
|
||||
payload = {
|
||||
name,
|
||||
effect_type: document.getElementById('css-editor-effect-type').value,
|
||||
speed: parseFloat(document.getElementById('css-editor-effect-speed').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),
|
||||
|
||||
@@ -28,7 +28,7 @@ class DeviceSettingsModal extends Modal {
|
||||
led_count: this.$('settings-led-count').value,
|
||||
led_type: document.getElementById('settings-led-type')?.value || 'rgb',
|
||||
send_latency: document.getElementById('settings-send-latency')?.value || '0',
|
||||
zones: _getCheckedZones('settings-zone-list'),
|
||||
zones: JSON.stringify(_getCheckedZones('settings-zone-list')),
|
||||
zoneMode: _getZoneMode('settings-zone-mode'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
_kcNameManuallyEdited, set_kcNameManuallyEdited,
|
||||
kcWebSockets,
|
||||
PATTERN_RECT_BORDERS,
|
||||
_cachedValueSources, valueSourcesCache,
|
||||
_cachedValueSources, valueSourcesCache, streamsCache,
|
||||
} from '../core/state.js';
|
||||
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml } from '../core/api.js';
|
||||
import { t } from '../core/i18n.js';
|
||||
@@ -413,12 +413,11 @@ function _populateKCBrightnessVsDropdown(selectedId = '') {
|
||||
export async function showKCEditor(targetId = null, cloneData = null) {
|
||||
try {
|
||||
// Load sources, pattern templates, and value sources in parallel
|
||||
const [sourcesResp, patResp, valueSources] = await Promise.all([
|
||||
fetchWithAuth('/picture-sources').catch(() => null),
|
||||
const [sources, patResp, valueSources] = await Promise.all([
|
||||
streamsCache.fetch().catch(() => []),
|
||||
fetchWithAuth('/pattern-templates').catch(() => null),
|
||||
valueSourcesCache.fetch(),
|
||||
]);
|
||||
const sources = (sourcesResp && sourcesResp.ok) ? (await sourcesResp.json()).streams || [] : [];
|
||||
const patTemplates = (patResp && patResp.ok) ? (await patResp.json()).templates || [] : [];
|
||||
|
||||
// Populate source select
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
patternEditorHoverHit, setPatternEditorHoverHit,
|
||||
PATTERN_RECT_COLORS,
|
||||
PATTERN_RECT_BORDERS,
|
||||
streamsCache,
|
||||
} from '../core/state.js';
|
||||
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml } from '../core/api.js';
|
||||
import { t } from '../core/i18n.js';
|
||||
@@ -76,8 +77,7 @@ export function createPatternTemplateCard(pt) {
|
||||
export async function showPatternTemplateEditor(templateId = null, cloneData = null) {
|
||||
try {
|
||||
// Load sources for background capture
|
||||
const sourcesResp = await fetchWithAuth('/picture-sources').catch(() => null);
|
||||
const sources = (sourcesResp && sourcesResp.ok) ? (await sourcesResp.json()).streams || [] : [];
|
||||
const sources = await streamsCache.fetch().catch(() => []);
|
||||
|
||||
const bgSelect = document.getElementById('pattern-bg-source');
|
||||
bgSelect.innerHTML = '';
|
||||
|
||||
@@ -247,10 +247,8 @@ function restoreCaptureDuration() {
|
||||
|
||||
export async function showTestTemplateModal(templateId) {
|
||||
try {
|
||||
const resp = await fetchWithAuth('/capture-templates');
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
const data = await resp.json();
|
||||
const template = (data.templates || []).find(tp => tp.id === templateId);
|
||||
const templates = await captureTemplatesCache.fetch();
|
||||
const template = templates.find(tp => tp.id === templateId);
|
||||
|
||||
if (!template) {
|
||||
showToast(t('templates.error.load'), 'error');
|
||||
@@ -1597,25 +1595,17 @@ export async function editStream(streamId) {
|
||||
let _streamModalDisplaysEngine = null;
|
||||
|
||||
async function populateStreamModalDropdowns() {
|
||||
const [displaysRes, captureTemplatesRes, streamsRes, ppTemplatesRes] = await Promise.all([
|
||||
fetch(`${API_BASE}/config/displays`, { headers: getHeaders() }),
|
||||
fetchWithAuth('/capture-templates'),
|
||||
fetchWithAuth('/picture-sources'),
|
||||
fetchWithAuth('/postprocessing-templates'),
|
||||
const [captureTemplates, streams, ppTemplates] = await Promise.all([
|
||||
captureTemplatesCache.fetch().catch(() => []),
|
||||
streamsCache.fetch().catch(() => []),
|
||||
ppTemplatesCache.fetch().catch(() => []),
|
||||
displaysCache.fetch().catch(() => []),
|
||||
]);
|
||||
|
||||
// Cache desktop displays (used as default unless engine has own displays)
|
||||
if (displaysRes.ok) {
|
||||
const displaysData = await displaysRes.json();
|
||||
displaysCache.update(displaysData.displays || []);
|
||||
}
|
||||
_streamModalDisplaysEngine = null;
|
||||
|
||||
const templateSelect = document.getElementById('stream-capture-template');
|
||||
templateSelect.innerHTML = '';
|
||||
if (captureTemplatesRes.ok) {
|
||||
const data = await captureTemplatesRes.json();
|
||||
(data.templates || []).forEach(tmpl => {
|
||||
captureTemplates.forEach(tmpl => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = tmpl.id;
|
||||
opt.dataset.name = tmpl.name;
|
||||
@@ -1624,7 +1614,6 @@ async function populateStreamModalDropdowns() {
|
||||
opt.textContent = `${tmpl.name} (${tmpl.engine_type})`;
|
||||
templateSelect.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
// When template changes, refresh displays if engine type switched
|
||||
templateSelect.addEventListener('change', _onCaptureTemplateChanged);
|
||||
@@ -1640,10 +1629,8 @@ async function populateStreamModalDropdowns() {
|
||||
|
||||
const sourceSelect = document.getElementById('stream-source');
|
||||
sourceSelect.innerHTML = '';
|
||||
if (streamsRes.ok) {
|
||||
const data = await streamsRes.json();
|
||||
const editingId = document.getElementById('stream-id').value;
|
||||
(data.streams || []).forEach(s => {
|
||||
streams.forEach(s => {
|
||||
if (s.id === editingId) return;
|
||||
const opt = document.createElement('option');
|
||||
opt.value = s.id;
|
||||
@@ -1651,21 +1638,16 @@ async function populateStreamModalDropdowns() {
|
||||
opt.textContent = s.name;
|
||||
sourceSelect.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
set_streamModalPPTemplates([]);
|
||||
set_streamModalPPTemplates(ppTemplates);
|
||||
const ppSelect = document.getElementById('stream-pp-template');
|
||||
ppSelect.innerHTML = '';
|
||||
if (ppTemplatesRes.ok) {
|
||||
const data = await ppTemplatesRes.json();
|
||||
set_streamModalPPTemplates(data.templates || []);
|
||||
_streamModalPPTemplates.forEach(tmpl => {
|
||||
ppTemplates.forEach(tmpl => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = tmpl.id;
|
||||
opt.textContent = tmpl.name;
|
||||
ppSelect.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
_autoGenerateStreamName();
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* - Navigation: network-first with offline fallback
|
||||
*/
|
||||
|
||||
const CACHE_NAME = 'ledgrab-v7';
|
||||
const CACHE_NAME = 'ledgrab-v8';
|
||||
|
||||
// Only pre-cache static assets (no auth required).
|
||||
// Do NOT pre-cache '/' — it requires API key auth and would cache an error page.
|
||||
|
||||
@@ -69,7 +69,6 @@ class ColorStripSource:
|
||||
"stops": None,
|
||||
"animation": None,
|
||||
"colors": None,
|
||||
"cycle_speed": None,
|
||||
"effect_type": None,
|
||||
"palette": None,
|
||||
"intensity": None,
|
||||
@@ -149,7 +148,6 @@ class ColorStripSource:
|
||||
id=sid, name=name, source_type="color_cycle",
|
||||
created_at=created_at, updated_at=updated_at, description=description,
|
||||
clock_id=clock_id, colors=colors,
|
||||
cycle_speed=float(data.get("cycle_speed") or 1.0),
|
||||
led_count=data.get("led_count") or 0,
|
||||
)
|
||||
|
||||
@@ -198,7 +196,6 @@ class ColorStripSource:
|
||||
id=sid, name=name, source_type="effect",
|
||||
created_at=created_at, updated_at=updated_at, description=description,
|
||||
clock_id=clock_id, effect_type=data.get("effect_type") or "fire",
|
||||
speed=float(data.get("speed") or 1.0),
|
||||
led_count=data.get("led_count") or 0,
|
||||
palette=data.get("palette") or "fire",
|
||||
color=color,
|
||||
@@ -340,13 +337,11 @@ class ColorCycleColorStripSource(ColorStripSource):
|
||||
[255, 0, 0], [255, 255, 0], [0, 255, 0],
|
||||
[0, 255, 255], [0, 0, 255], [255, 0, 255],
|
||||
])
|
||||
cycle_speed: float = 1.0 # speed multiplier; 1.0 ≈ one full cycle every 20 seconds
|
||||
led_count: int = 0 # 0 = use device LED count
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
d = super().to_dict()
|
||||
d["colors"] = [list(c) for c in self.colors]
|
||||
d["cycle_speed"] = self.cycle_speed
|
||||
d["led_count"] = self.led_count
|
||||
return d
|
||||
|
||||
@@ -361,7 +356,6 @@ class EffectColorStripSource(ColorStripSource):
|
||||
"""
|
||||
|
||||
effect_type: str = "fire" # fire | meteor | plasma | noise | aurora
|
||||
speed: float = 1.0 # animation speed multiplier (0.1–10.0)
|
||||
led_count: int = 0 # 0 = use device LED count
|
||||
palette: str = "fire" # named color palette
|
||||
color: list = field(default_factory=lambda: [255, 80, 0]) # [R,G,B] for meteor head
|
||||
@@ -372,7 +366,6 @@ class EffectColorStripSource(ColorStripSource):
|
||||
def to_dict(self) -> dict:
|
||||
d = super().to_dict()
|
||||
d["effect_type"] = self.effect_type
|
||||
d["speed"] = self.speed
|
||||
d["led_count"] = self.led_count
|
||||
d["palette"] = self.palette
|
||||
d["color"] = list(self.color)
|
||||
|
||||
@@ -106,9 +106,7 @@ class ColorStripStore:
|
||||
frame_interpolation: bool = False,
|
||||
animation: Optional[dict] = None,
|
||||
colors: Optional[list] = None,
|
||||
cycle_speed: float = 1.0,
|
||||
effect_type: str = "fire",
|
||||
speed: float = 1.0,
|
||||
palette: str = "fire",
|
||||
intensity: float = 1.0,
|
||||
scale: float = 1.0,
|
||||
@@ -182,7 +180,6 @@ class ColorStripStore:
|
||||
description=description,
|
||||
clock_id=clock_id,
|
||||
colors=colors if isinstance(colors, list) and len(colors) >= 2 else default_colors,
|
||||
cycle_speed=float(cycle_speed) if cycle_speed else 1.0,
|
||||
led_count=led_count,
|
||||
)
|
||||
elif source_type == "effect":
|
||||
@@ -196,7 +193,6 @@ class ColorStripStore:
|
||||
description=description,
|
||||
clock_id=clock_id,
|
||||
effect_type=effect_type or "fire",
|
||||
speed=float(speed) if speed else 1.0,
|
||||
led_count=led_count,
|
||||
palette=palette or "fire",
|
||||
color=rgb,
|
||||
@@ -311,9 +307,7 @@ class ColorStripStore:
|
||||
frame_interpolation: Optional[bool] = None,
|
||||
animation: Optional[dict] = None,
|
||||
colors: Optional[list] = None,
|
||||
cycle_speed: Optional[float] = None,
|
||||
effect_type: Optional[str] = None,
|
||||
speed: Optional[float] = None,
|
||||
palette: Optional[str] = None,
|
||||
intensity: Optional[float] = None,
|
||||
scale: Optional[float] = None,
|
||||
@@ -389,15 +383,11 @@ class ColorStripStore:
|
||||
elif isinstance(source, ColorCycleColorStripSource):
|
||||
if colors is not None and isinstance(colors, list) and len(colors) >= 2:
|
||||
source.colors = colors
|
||||
if cycle_speed is not None:
|
||||
source.cycle_speed = float(cycle_speed)
|
||||
if led_count is not None:
|
||||
source.led_count = led_count
|
||||
elif isinstance(source, EffectColorStripSource):
|
||||
if effect_type is not None:
|
||||
source.effect_type = effect_type
|
||||
if speed is not None:
|
||||
source.speed = float(speed)
|
||||
if led_count is not None:
|
||||
source.led_count = led_count
|
||||
if palette is not None:
|
||||
|
||||
@@ -146,18 +146,6 @@
|
||||
<div id="color-cycle-colors-list"></div>
|
||||
<button type="button" class="btn btn-secondary" onclick="colorCycleAddColor()" data-i18n="color_strip.color_cycle.add_color">+ Add Color</button>
|
||||
</div>
|
||||
<div id="css-editor-cycle-speed-group" class="form-group">
|
||||
<div class="label-row">
|
||||
<label for="css-editor-cycle-speed">
|
||||
<span data-i18n="color_strip.color_cycle.speed">Speed:</span>
|
||||
<span id="css-editor-cycle-speed-val">1.0</span>×
|
||||
</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="color_strip.color_cycle.speed.hint">Cycle speed multiplier. 1.0 ≈ one full cycle every 20 seconds.</small>
|
||||
<input type="range" id="css-editor-cycle-speed" min="0.1" max="10.0" step="0.1" value="1.0"
|
||||
oninput="document.getElementById('css-editor-cycle-speed-val').textContent = parseFloat(this.value).toFixed(1)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gradient-specific fields -->
|
||||
@@ -226,19 +214,6 @@
|
||||
<div id="css-editor-effect-preview" class="effect-palette-preview"></div>
|
||||
</div>
|
||||
|
||||
<div id="css-editor-effect-speed-group" class="form-group">
|
||||
<div class="label-row">
|
||||
<label for="css-editor-effect-speed">
|
||||
<span data-i18n="color_strip.effect.speed">Speed:</span>
|
||||
<span id="css-editor-effect-speed-val">1.0</span>x
|
||||
</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="color_strip.effect.speed.hint">How fast the effect animates. 1.0 = default speed.</small>
|
||||
<input type="range" id="css-editor-effect-speed" min="0.1" max="10.0" step="0.1" value="1.0"
|
||||
oninput="document.getElementById('css-editor-effect-speed-val').textContent = parseFloat(this.value).toFixed(1)">
|
||||
</div>
|
||||
|
||||
<div id="css-editor-effect-palette-group" class="form-group">
|
||||
<div class="label-row">
|
||||
<label for="css-editor-effect-palette" data-i18n="color_strip.effect.palette">Palette:</label>
|
||||
@@ -492,18 +467,6 @@
|
||||
</select>
|
||||
<small id="css-editor-animation-type-desc" class="field-desc"></small>
|
||||
</div>
|
||||
<div id="css-editor-animation-speed-group" class="form-group">
|
||||
<div class="label-row">
|
||||
<label for="css-editor-animation-speed">
|
||||
<span data-i18n="color_strip.animation.speed">Speed:</span>
|
||||
<span id="css-editor-animation-speed-val">1.0</span>×
|
||||
</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="color_strip.animation.speed.hint">Animation speed multiplier. 1.0 ≈ one cycle per second for Breathing; higher values cycle faster.</small>
|
||||
<input type="range" id="css-editor-animation-speed" min="0.1" max="10.0" step="0.1" value="1.0"
|
||||
oninput="document.getElementById('css-editor-animation-speed-val').textContent = parseFloat(this.value).toFixed(1)">
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="sync_clock.description.hint">Optional notes about this clock's purpose</small>
|
||||
<textarea id="sync-clock-description" rows="2" data-i18n-placeholder="sync_clock.description.placeholder" placeholder=""></textarea>
|
||||
<input type="text" id="sync-clock-description" data-i18n-placeholder="sync_clock.description.placeholder" placeholder="">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user