Remove led_count from static, gradient, color_cycle, and effect CSS sources

These types always auto-size from the connected device — the explicit
led_count override was unused clutter. Streams now use getattr fallback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 21:21:00 +03:00
parent de04872fdc
commit bc5d8fdc9b
6 changed files with 19 additions and 43 deletions

View File

@@ -573,8 +573,9 @@ class StaticColorStripStream(ColorStripStream):
def _update_from_source(self, source) -> None: def _update_from_source(self, source) -> None:
color = source.color if isinstance(source.color, list) and len(source.color) == 3 else [255, 255, 255] color = source.color if isinstance(source.color, list) and len(source.color) == 3 else [255, 255, 255]
self._source_color = color # stored separately so configure() can rebuild self._source_color = color # stored separately so configure() can rebuild
self._auto_size = not source.led_count # True when led_count == 0 _lc = getattr(source, "led_count", 0)
led_count = source.led_count if source.led_count and source.led_count > 0 else 1 self._auto_size = not _lc
led_count = _lc if _lc and _lc > 0 else 1
self._led_count = led_count self._led_count = led_count
self._animation = source.animation # dict or None; read atomically by _animate_loop self._animation = source.animation # dict or None; read atomically by _animate_loop
self._rebuild_colors() self._rebuild_colors()
@@ -798,8 +799,9 @@ class ColorCycleColorStripStream(ColorStripStream):
self._color_list = [ self._color_list = [
c for c in raw if isinstance(c, list) and len(c) == 3 c for c in raw if isinstance(c, list) and len(c) == 3
] or default ] or default
self._auto_size = not source.led_count _lc = getattr(source, "led_count", 0)
self._led_count = source.led_count if source.led_count > 0 else 1 self._auto_size = not _lc
self._led_count = _lc if _lc > 0 else 1
self._rebuild_colors() self._rebuild_colors()
def _rebuild_colors(self) -> None: def _rebuild_colors(self) -> None:
@@ -950,8 +952,9 @@ class GradientColorStripStream(ColorStripStream):
def _update_from_source(self, source) -> None: def _update_from_source(self, source) -> None:
self._stops = list(source.stops) if source.stops else [] self._stops = list(source.stops) if source.stops else []
self._auto_size = not source.led_count _lc = getattr(source, "led_count", 0)
led_count = source.led_count if source.led_count and source.led_count > 0 else 1 self._auto_size = not _lc
led_count = _lc if _lc and _lc > 0 else 1
self._led_count = led_count self._led_count = led_count
self._animation = source.animation # dict or None; read atomically by _animate_loop self._animation = source.animation # dict or None; read atomically by _animate_loop
self._rebuild_colors() self._rebuild_colors()

View File

@@ -203,8 +203,9 @@ class EffectColorStripStream(ColorStripStream):
def _update_from_source(self, source) -> None: def _update_from_source(self, source) -> None:
self._effect_type = getattr(source, "effect_type", "fire") self._effect_type = getattr(source, "effect_type", "fire")
self._auto_size = not source.led_count _lc = getattr(source, "led_count", 0)
self._led_count = source.led_count if source.led_count and source.led_count > 0 else 1 self._auto_size = not _lc
self._led_count = _lc if _lc and _lc > 0 else 1
self._palette_name = getattr(source, "palette", None) or _EFFECT_DEFAULT_PALETTE.get(self._effect_type, "fire") self._palette_name = getattr(source, "palette", None) or _EFFECT_DEFAULT_PALETTE.get(self._effect_type, "fire")
self._palette_lut = _build_palette_lut(self._palette_name) self._palette_lut = _build_palette_lut(self._palette_name)
color = getattr(source, "color", None) color = getattr(source, "color", None)

View File

@@ -116,9 +116,10 @@ export function onCSSTypeChange() {
} }
_syncAnimationSpeedState(); _syncAnimationSpeedState();
// LED count — not needed for composite/mapped/audio/api_input (uses device count) // LED count — only shown for picture, api_input, notification
const hasLedCount = ['picture', 'api_input', 'notification'];
document.getElementById('css-editor-led-count-group').style.display = document.getElementById('css-editor-led-count-group').style.display =
(type === 'composite' || type === 'mapped' || type === 'audio' || type === 'api_input') ? 'none' : ''; hasLedCount.includes(type) ? '' : 'none';
// Sync clock — shown for animated types (static, gradient, color_cycle, effect) // Sync clock — shown for animated types (static, gradient, color_cycle, effect)
const clockTypes = ['static', 'gradient', 'color_cycle', 'effect']; const clockTypes = ['static', 'gradient', 'color_cycle', 'effect'];
@@ -716,7 +717,6 @@ export function createColorStripCard(source, pictureSourceMap, audioSourceMap) {
<span class="stream-card-prop" title="${t('color_strip.static_color')}"> <span class="stream-card-prop" title="${t('color_strip.static_color')}">
<span style="display:inline-block;width:14px;height:14px;background:${hexColor};border:1px solid #888;border-radius:2px;vertical-align:middle;margin-right:4px"></span>${hexColor.toUpperCase()} <span style="display:inline-block;width:14px;height:14px;background:${hexColor};border:1px solid #888;border-radius:2px;vertical-align:middle;margin-right:4px"></span>${hexColor.toUpperCase()}
</span> </span>
${source.led_count ? `<span class="stream-card-prop" title="${t('color_strip.leds')}">${ICON_LED} ${source.led_count}</span>` : ''}
${animBadge} ${animBadge}
${clockBadge} ${clockBadge}
`; `;
@@ -727,7 +727,6 @@ export function createColorStripCard(source, pictureSourceMap, audioSourceMap) {
).join(''); ).join('');
propsHtml = ` propsHtml = `
<span class="stream-card-prop">${swatches}</span> <span class="stream-card-prop">${swatches}</span>
${source.led_count ? `<span class="stream-card-prop" title="${t('color_strip.leds')}">${ICON_LED} ${source.led_count}</span>` : ''}
${clockBadge} ${clockBadge}
`; `;
} else if (isGradient) { } else if (isGradient) {
@@ -749,7 +748,6 @@ export function createColorStripCard(source, pictureSourceMap, audioSourceMap) {
propsHtml = ` propsHtml = `
${cssGradient ? `<span style="flex:1 1 100%;height:12px;background:${cssGradient};border-radius:3px;border:1px solid rgba(128,128,128,0.3)"></span>` : ''} ${cssGradient ? `<span style="flex:1 1 100%;height:12px;background:${cssGradient};border-radius:3px;border:1px solid rgba(128,128,128,0.3)"></span>` : ''}
<span class="stream-card-prop">${ICON_PALETTE} ${stops.length} ${t('color_strip.gradient.stops_count')}</span> <span class="stream-card-prop">${ICON_PALETTE} ${stops.length} ${t('color_strip.gradient.stops_count')}</span>
${source.led_count ? `<span class="stream-card-prop" title="${t('color_strip.leds')}">${ICON_LED} ${source.led_count}</span>` : ''}
${animBadge} ${animBadge}
${clockBadge} ${clockBadge}
`; `;
@@ -759,7 +757,6 @@ export function createColorStripCard(source, pictureSourceMap, audioSourceMap) {
propsHtml = ` propsHtml = `
<span class="stream-card-prop">${ICON_FPS} ${escapeHtml(effectLabel)}</span> <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>` : ''} ${paletteLabel ? `<span class="stream-card-prop" title="${t('color_strip.effect.palette')}">${ICON_PALETTE} ${escapeHtml(paletteLabel)}</span>` : ''}
${source.led_count ? `<span class="stream-card-prop" title="${t('color_strip.leds')}">${ICON_LED} ${source.led_count}</span>` : ''}
${clockBadge} ${clockBadge}
`; `;
} else if (isComposite) { } else if (isComposite) {
@@ -1103,7 +1100,6 @@ export async function saveCSSEditor() {
intensity: parseFloat(document.getElementById('css-editor-effect-intensity').value), intensity: parseFloat(document.getElementById('css-editor-effect-intensity').value),
scale: parseFloat(document.getElementById('css-editor-effect-scale').value), scale: parseFloat(document.getElementById('css-editor-effect-scale').value),
mirror: document.getElementById('css-editor-effect-mirror').checked, mirror: document.getElementById('css-editor-effect-mirror').checked,
led_count: parseInt(document.getElementById('css-editor-led-count').value) || 0,
}; };
// Meteor uses a color picker // Meteor uses a color picker
if (payload.effect_type === 'meteor') { if (payload.effect_type === 'meteor') {

View File

@@ -7,7 +7,7 @@
* - Navigation: network-first with offline fallback * - Navigation: network-first with offline fallback
*/ */
const CACHE_NAME = 'ledgrab-v11'; const CACHE_NAME = 'ledgrab-v12';
// Only pre-cache static assets (no auth required). // Only pre-cache static assets (no auth required).
// Do NOT pre-cache '/' — it requires API key auth and would cache an error page. // Do NOT pre-cache '/' — it requires API key auth and would cache an error page.

View File

@@ -134,7 +134,6 @@ class ColorStripSource:
id=sid, name=name, source_type="static", id=sid, name=name, source_type="static",
created_at=created_at, updated_at=updated_at, description=description, created_at=created_at, updated_at=updated_at, description=description,
clock_id=clock_id, color=color, clock_id=clock_id, color=color,
led_count=data.get("led_count") or 0,
animation=data.get("animation"), animation=data.get("animation"),
) )
@@ -145,7 +144,6 @@ class ColorStripSource:
id=sid, name=name, source_type="gradient", id=sid, name=name, source_type="gradient",
created_at=created_at, updated_at=updated_at, description=description, created_at=created_at, updated_at=updated_at, description=description,
clock_id=clock_id, stops=stops, clock_id=clock_id, stops=stops,
led_count=data.get("led_count") or 0,
animation=data.get("animation"), animation=data.get("animation"),
) )
@@ -156,7 +154,6 @@ class ColorStripSource:
id=sid, name=name, source_type="color_cycle", id=sid, name=name, source_type="color_cycle",
created_at=created_at, updated_at=updated_at, description=description, created_at=created_at, updated_at=updated_at, description=description,
clock_id=clock_id, colors=colors, clock_id=clock_id, colors=colors,
led_count=data.get("led_count") or 0,
) )
if source_type == "composite": if source_type == "composite":
@@ -204,7 +201,6 @@ class ColorStripSource:
id=sid, name=name, source_type="effect", id=sid, name=name, source_type="effect",
created_at=created_at, updated_at=updated_at, description=description, created_at=created_at, updated_at=updated_at, description=description,
clock_id=clock_id, effect_type=data.get("effect_type") or "fire", clock_id=clock_id, effect_type=data.get("effect_type") or "fire",
led_count=data.get("led_count") or 0,
palette=data.get("palette") or "fire", palette=data.get("palette") or "fire",
color=color, color=color,
intensity=float(data.get("intensity") or 1.0), intensity=float(data.get("intensity") or 1.0),
@@ -313,13 +309,11 @@ class StaticColorStripSource(ColorStripSource):
""" """
color: list = field(default_factory=lambda: [255, 255, 255]) # [R, G, B] color: list = field(default_factory=lambda: [255, 255, 255]) # [R, G, B]
led_count: int = 0 # 0 = use device LED count
animation: Optional[dict] = None # {"enabled": bool, "type": str, "speed": float} or None animation: Optional[dict] = None # {"enabled": bool, "type": str, "speed": float} or None
def to_dict(self) -> dict: def to_dict(self) -> dict:
d = super().to_dict() d = super().to_dict()
d["color"] = list(self.color) d["color"] = list(self.color)
d["led_count"] = self.led_count
d["animation"] = self.animation d["animation"] = self.animation
return d return d
@@ -332,7 +326,7 @@ class GradientColorStripSource(ColorStripSource):
Each stop has a primary color; optionally a second "right" color to create Each stop has a primary color; optionally a second "right" color to create
a hard discontinuity (bidirectional stop) at that position. a hard discontinuity (bidirectional stop) at that position.
LED count auto-sizes from the connected device when led_count == 0. LED count auto-sizes from the connected device.
""" """
# Each stop: {"position": float, "color": [R,G,B], "color_right": [R,G,B] | null} # Each stop: {"position": float, "color": [R,G,B], "color_right": [R,G,B] | null}
@@ -340,13 +334,11 @@ class GradientColorStripSource(ColorStripSource):
{"position": 0.0, "color": [255, 0, 0]}, {"position": 0.0, "color": [255, 0, 0]},
{"position": 1.0, "color": [0, 0, 255]}, {"position": 1.0, "color": [0, 0, 255]},
]) ])
led_count: int = 0 # 0 = use device LED count
animation: Optional[dict] = None # {"enabled": bool, "type": str, "speed": float} or None animation: Optional[dict] = None # {"enabled": bool, "type": str, "speed": float} or None
def to_dict(self) -> dict: def to_dict(self) -> dict:
d = super().to_dict() d = super().to_dict()
d["stops"] = [dict(s) for s in self.stops] d["stops"] = [dict(s) for s in self.stops]
d["led_count"] = self.led_count
d["animation"] = self.animation d["animation"] = self.animation
return d return d
@@ -357,19 +349,17 @@ class ColorCycleColorStripSource(ColorStripSource):
All LEDs receive the same solid color at any point in time, smoothly All LEDs receive the same solid color at any point in time, smoothly
interpolating between the configured color stops in a continuous loop. interpolating between the configured color stops in a continuous loop.
LED count auto-sizes from the connected device when led_count == 0. LED count auto-sizes from the connected device.
""" """
colors: list = field(default_factory=lambda: [ colors: list = field(default_factory=lambda: [
[255, 0, 0], [255, 255, 0], [0, 255, 0], [255, 0, 0], [255, 255, 0], [0, 255, 0],
[0, 255, 255], [0, 0, 255], [255, 0, 255], [0, 255, 255], [0, 0, 255], [255, 0, 255],
]) ])
led_count: int = 0 # 0 = use device LED count
def to_dict(self) -> dict: def to_dict(self) -> dict:
d = super().to_dict() d = super().to_dict()
d["colors"] = [list(c) for c in self.colors] d["colors"] = [list(c) for c in self.colors]
d["led_count"] = self.led_count
return d return d
@@ -379,11 +369,10 @@ class EffectColorStripSource(ColorStripSource):
The effect_type field selects which algorithm to use: The effect_type field selects which algorithm to use:
fire, meteor, plasma, noise, aurora. fire, meteor, plasma, noise, aurora.
LED count auto-sizes from the connected device when led_count == 0. LED count auto-sizes from the connected device.
""" """
effect_type: str = "fire" # fire | meteor | plasma | noise | aurora effect_type: str = "fire" # fire | meteor | plasma | noise | aurora
led_count: int = 0 # 0 = use device LED count
palette: str = "fire" # named color palette palette: str = "fire" # named color palette
color: list = field(default_factory=lambda: [255, 80, 0]) # [R,G,B] for meteor head color: list = field(default_factory=lambda: [255, 80, 0]) # [R,G,B] for meteor head
intensity: float = 1.0 # effect-specific intensity (0.12.0) intensity: float = 1.0 # effect-specific intensity (0.12.0)
@@ -393,7 +382,6 @@ class EffectColorStripSource(ColorStripSource):
def to_dict(self) -> dict: def to_dict(self) -> dict:
d = super().to_dict() d = super().to_dict()
d["effect_type"] = self.effect_type d["effect_type"] = self.effect_type
d["led_count"] = self.led_count
d["palette"] = self.palette d["palette"] = self.palette
d["color"] = list(self.color) d["color"] = list(self.color)
d["intensity"] = self.intensity d["intensity"] = self.intensity

View File

@@ -155,7 +155,6 @@ class ColorStripStore:
description=description, description=description,
clock_id=clock_id, clock_id=clock_id,
color=rgb, color=rgb,
led_count=led_count,
animation=animation, animation=animation,
) )
elif source_type == "gradient": elif source_type == "gradient":
@@ -171,7 +170,6 @@ class ColorStripStore:
{"position": 0.0, "color": [255, 0, 0]}, {"position": 0.0, "color": [255, 0, 0]},
{"position": 1.0, "color": [0, 0, 255]}, {"position": 1.0, "color": [0, 0, 255]},
], ],
led_count=led_count,
animation=animation, animation=animation,
) )
elif source_type == "color_cycle": elif source_type == "color_cycle":
@@ -188,7 +186,6 @@ class ColorStripStore:
description=description, description=description,
clock_id=clock_id, clock_id=clock_id,
colors=colors if isinstance(colors, list) and len(colors) >= 2 else default_colors, colors=colors if isinstance(colors, list) and len(colors) >= 2 else default_colors,
led_count=led_count,
) )
elif source_type == "effect": elif source_type == "effect":
rgb = color if isinstance(color, list) and len(color) == 3 else [255, 80, 0] rgb = color if isinstance(color, list) and len(color) == 3 else [255, 80, 0]
@@ -201,7 +198,6 @@ class ColorStripStore:
description=description, description=description,
clock_id=clock_id, clock_id=clock_id,
effect_type=effect_type or "fire", effect_type=effect_type or "fire",
led_count=led_count,
palette=palette or "fire", palette=palette or "fire",
color=rgb, color=rgb,
intensity=float(intensity) if intensity else 1.0, intensity=float(intensity) if intensity else 1.0,
@@ -402,27 +398,19 @@ class ColorStripStore:
if color is not None: if color is not None:
if isinstance(color, list) and len(color) == 3: if isinstance(color, list) and len(color) == 3:
source.color = color source.color = color
if led_count is not None:
source.led_count = led_count
if animation is not None: if animation is not None:
source.animation = animation source.animation = animation
elif isinstance(source, GradientColorStripSource): elif isinstance(source, GradientColorStripSource):
if stops is not None and isinstance(stops, list): if stops is not None and isinstance(stops, list):
source.stops = stops source.stops = stops
if led_count is not None:
source.led_count = led_count
if animation is not None: if animation is not None:
source.animation = animation source.animation = animation
elif isinstance(source, ColorCycleColorStripSource): elif isinstance(source, ColorCycleColorStripSource):
if colors is not None and isinstance(colors, list) and len(colors) >= 2: if colors is not None and isinstance(colors, list) and len(colors) >= 2:
source.colors = colors source.colors = colors
if led_count is not None:
source.led_count = led_count
elif isinstance(source, EffectColorStripSource): elif isinstance(source, EffectColorStripSource):
if effect_type is not None: if effect_type is not None:
source.effect_type = effect_type source.effect_type = effect_type
if led_count is not None:
source.led_count = led_count
if palette is not None: if palette is not None:
source.palette = palette source.palette = palette
if color is not None and isinstance(color, list) and len(color) == 3: if color is not None and isinstance(color, list) and len(color) == 3: