Add visual IconSelect selectors for effect, palette, gradient, waveform dropdowns
Replace plain <select> dropdowns with rich visual selectors: - Effect type: icon grid with descriptions - Effect/audio palette: gradient strip previews from color data - Gradient preset: gradient strip previews (13 presets) - Audio visualization: icon grid with descriptions - Notification effect: icon grid with descriptions - Waveform (value source): inline SVG shape previews Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
|||||||
ICON_AUDIO_LOOPBACK, ICON_TIMER, ICON_LINK_SOURCE, ICON_FILM,
|
ICON_AUDIO_LOOPBACK, ICON_TIMER, ICON_LINK_SOURCE, ICON_FILM,
|
||||||
ICON_LINK, ICON_SPARKLES, ICON_ACTIVITY, ICON_CLOCK, ICON_BELL,
|
ICON_LINK, ICON_SPARKLES, ICON_ACTIVITY, ICON_CLOCK, ICON_BELL,
|
||||||
} from '../core/icons.js';
|
} from '../core/icons.js';
|
||||||
|
import * as P from '../core/icon-paths.js';
|
||||||
import { wrapCard } from '../core/card-colors.js';
|
import { wrapCard } from '../core/card-colors.js';
|
||||||
import { attachProcessPicker } from '../core/process-picker.js';
|
import { attachProcessPicker } from '../core/process-picker.js';
|
||||||
import { IconSelect } from '../core/icon-select.js';
|
import { IconSelect } from '../core/icon-select.js';
|
||||||
@@ -128,8 +129,18 @@ export function onCSSTypeChange() {
|
|||||||
document.getElementById('css-editor-api-input-section').style.display = type === 'api_input' ? '' : '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';
|
document.getElementById('css-editor-notification-section').style.display = type === 'notification' ? '' : 'none';
|
||||||
|
|
||||||
if (type === 'effect') onEffectTypeChange();
|
if (type === 'effect') {
|
||||||
if (type === 'audio') onAudioVizChange();
|
_ensureEffectTypeIconSelect();
|
||||||
|
_ensureEffectPaletteIconSelect();
|
||||||
|
onEffectTypeChange();
|
||||||
|
}
|
||||||
|
if (type === 'audio') {
|
||||||
|
_ensureAudioVizIconSelect();
|
||||||
|
_ensureAudioPaletteIconSelect();
|
||||||
|
onAudioVizChange();
|
||||||
|
}
|
||||||
|
if (type === 'gradient') _ensureGradientPresetIconSelect();
|
||||||
|
if (type === 'notification') _ensureNotificationEffectIconSelect();
|
||||||
|
|
||||||
// Animation section — shown for static/gradient only
|
// Animation section — shown for static/gradient only
|
||||||
const animSection = document.getElementById('css-editor-animation-section');
|
const animSection = document.getElementById('css-editor-animation-section');
|
||||||
@@ -240,6 +251,110 @@ function _syncAnimationSpeedState() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Gradient strip preview helper ────────────────────────────── */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a small inline CSS gradient preview from palette color points.
|
||||||
|
* @param {Array<[number, string]>} pts – [[position, 'r,g,b'], ...]
|
||||||
|
* @param {number} [w=80] width in px
|
||||||
|
* @param {number} [h=16] height in px
|
||||||
|
* @returns {string} HTML string
|
||||||
|
*/
|
||||||
|
function _gradientStripHTML(pts, w = 80, h = 16) {
|
||||||
|
const stops = pts.map(([pos, rgb]) => `rgb(${rgb}) ${(pos * 100).toFixed(0)}%`).join(', ');
|
||||||
|
return `<span style="display:inline-block;width:${w}px;height:${h}px;border-radius:3px;background:linear-gradient(to right,${stops});flex-shrink:0"></span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a gradient preview from _GRADIENT_PRESETS entry (array of {position, color:[r,g,b]}).
|
||||||
|
*/
|
||||||
|
function _gradientPresetStripHTML(stops, w = 80, h = 16) {
|
||||||
|
const css = stops.map(s => `rgb(${s.color.join(',')}) ${(s.position * 100).toFixed(0)}%`).join(', ');
|
||||||
|
return `<span style="display:inline-block;width:${w}px;height:${h}px;border-radius:3px;background:linear-gradient(to right,${css});flex-shrink:0"></span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Effect / audio palette IconSelect instances ─────────────── */
|
||||||
|
|
||||||
|
let _effectTypeIconSelect = null;
|
||||||
|
let _effectPaletteIconSelect = null;
|
||||||
|
let _audioPaletteIconSelect = null;
|
||||||
|
let _audioVizIconSelect = null;
|
||||||
|
let _gradientPresetIconSelect = null;
|
||||||
|
let _notificationEffectIconSelect = null;
|
||||||
|
|
||||||
|
const _icon = (d) => `<svg class="icon" viewBox="0 0 24 24">${d}</svg>`;
|
||||||
|
|
||||||
|
function _ensureEffectTypeIconSelect() {
|
||||||
|
const sel = document.getElementById('css-editor-effect-type');
|
||||||
|
if (!sel) return;
|
||||||
|
const items = [
|
||||||
|
{ value: 'fire', icon: _icon(P.zap), label: t('color_strip.effect.fire'), desc: t('color_strip.effect.fire.desc') },
|
||||||
|
{ value: 'meteor', icon: _icon(P.sparkles), label: t('color_strip.effect.meteor'), desc: t('color_strip.effect.meteor.desc') },
|
||||||
|
{ value: 'plasma', icon: _icon(P.rainbow), label: t('color_strip.effect.plasma'), desc: t('color_strip.effect.plasma.desc') },
|
||||||
|
{ value: 'noise', icon: _icon(P.activity), label: t('color_strip.effect.noise'), desc: t('color_strip.effect.noise.desc') },
|
||||||
|
{ value: 'aurora', icon: _icon(P.sparkles), label: t('color_strip.effect.aurora'), desc: t('color_strip.effect.aurora.desc') },
|
||||||
|
];
|
||||||
|
if (_effectTypeIconSelect) { _effectTypeIconSelect.updateItems(items); return; }
|
||||||
|
_effectTypeIconSelect = new IconSelect({ target: sel, items, columns: 2 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function _ensureEffectPaletteIconSelect() {
|
||||||
|
const sel = document.getElementById('css-editor-effect-palette');
|
||||||
|
if (!sel) return;
|
||||||
|
const items = Object.entries(_PALETTE_COLORS).map(([key, pts]) => ({
|
||||||
|
value: key, icon: _gradientStripHTML(pts), label: t(`color_strip.palette.${key}`),
|
||||||
|
}));
|
||||||
|
if (_effectPaletteIconSelect) { _effectPaletteIconSelect.updateItems(items); return; }
|
||||||
|
_effectPaletteIconSelect = new IconSelect({ target: sel, items, columns: 2 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function _ensureAudioPaletteIconSelect() {
|
||||||
|
const sel = document.getElementById('css-editor-audio-palette');
|
||||||
|
if (!sel) return;
|
||||||
|
const items = Object.entries(_PALETTE_COLORS).map(([key, pts]) => ({
|
||||||
|
value: key, icon: _gradientStripHTML(pts), label: t(`color_strip.palette.${key}`),
|
||||||
|
}));
|
||||||
|
if (_audioPaletteIconSelect) { _audioPaletteIconSelect.updateItems(items); return; }
|
||||||
|
_audioPaletteIconSelect = new IconSelect({ target: sel, items, columns: 2 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function _ensureAudioVizIconSelect() {
|
||||||
|
const sel = document.getElementById('css-editor-audio-viz');
|
||||||
|
if (!sel) return;
|
||||||
|
const items = [
|
||||||
|
{ value: 'spectrum', icon: _icon(P.activity), label: t('color_strip.audio.viz.spectrum'), desc: t('color_strip.audio.viz.spectrum.desc') },
|
||||||
|
{ value: 'beat_pulse', icon: _icon(P.zap), label: t('color_strip.audio.viz.beat_pulse'), desc: t('color_strip.audio.viz.beat_pulse.desc') },
|
||||||
|
{ value: 'vu_meter', icon: _icon(P.trendingUp), label: t('color_strip.audio.viz.vu_meter'), desc: t('color_strip.audio.viz.vu_meter.desc') },
|
||||||
|
];
|
||||||
|
if (_audioVizIconSelect) { _audioVizIconSelect.updateItems(items); return; }
|
||||||
|
_audioVizIconSelect = new IconSelect({ target: sel, items, columns: 3 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function _ensureGradientPresetIconSelect() {
|
||||||
|
const sel = document.getElementById('css-editor-gradient-preset');
|
||||||
|
if (!sel) return;
|
||||||
|
const items = [
|
||||||
|
{ value: '', icon: _icon(P.palette), label: t('color_strip.gradient.preset.custom') },
|
||||||
|
...Object.entries(_GRADIENT_PRESETS).map(([key, stops]) => ({
|
||||||
|
value: key, icon: _gradientPresetStripHTML(stops), label: t(`color_strip.gradient.preset.${key}`),
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
if (_gradientPresetIconSelect) { _gradientPresetIconSelect.updateItems(items); return; }
|
||||||
|
_gradientPresetIconSelect = new IconSelect({ target: sel, items, columns: 3 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function _ensureNotificationEffectIconSelect() {
|
||||||
|
const sel = document.getElementById('css-editor-notification-effect');
|
||||||
|
if (!sel) return;
|
||||||
|
const items = [
|
||||||
|
{ value: 'flash', icon: _icon(P.zap), label: t('color_strip.notification.effect.flash'), desc: t('color_strip.notification.effect.flash.desc') },
|
||||||
|
{ value: 'pulse', icon: _icon(P.activity), label: t('color_strip.notification.effect.pulse'), desc: t('color_strip.notification.effect.pulse.desc') },
|
||||||
|
{ value: 'sweep', icon: _icon(P.fastForward), label: t('color_strip.notification.effect.sweep'), desc: t('color_strip.notification.effect.sweep.desc') },
|
||||||
|
];
|
||||||
|
if (_notificationEffectIconSelect) { _notificationEffectIconSelect.updateItems(items); return; }
|
||||||
|
_notificationEffectIconSelect = new IconSelect({ target: sel, items, columns: 3 });
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Effect type helpers ──────────────────────────────────────── */
|
/* ── Effect type helpers ──────────────────────────────────────── */
|
||||||
|
|
||||||
// Palette color control points — mirrors _PALETTE_DEFS in effect_stream.py
|
// Palette color control points — mirrors _PALETTE_DEFS in effect_stream.py
|
||||||
@@ -623,6 +738,7 @@ async function _loadAudioSources() {
|
|||||||
|
|
||||||
function _loadAudioState(css) {
|
function _loadAudioState(css) {
|
||||||
document.getElementById('css-editor-audio-viz').value = css.visualization_mode || 'spectrum';
|
document.getElementById('css-editor-audio-viz').value = css.visualization_mode || 'spectrum';
|
||||||
|
if (_audioVizIconSelect) _audioVizIconSelect.setValue(css.visualization_mode || 'spectrum');
|
||||||
onAudioVizChange();
|
onAudioVizChange();
|
||||||
|
|
||||||
const sensitivity = css.sensitivity ?? 1.0;
|
const sensitivity = css.sensitivity ?? 1.0;
|
||||||
@@ -634,6 +750,7 @@ function _loadAudioState(css) {
|
|||||||
document.getElementById('css-editor-audio-smoothing-val').textContent = parseFloat(smoothing).toFixed(2);
|
document.getElementById('css-editor-audio-smoothing-val').textContent = parseFloat(smoothing).toFixed(2);
|
||||||
|
|
||||||
document.getElementById('css-editor-audio-palette').value = css.palette || 'rainbow';
|
document.getElementById('css-editor-audio-palette').value = css.palette || 'rainbow';
|
||||||
|
if (_audioPaletteIconSelect) _audioPaletteIconSelect.setValue(css.palette || 'rainbow');
|
||||||
document.getElementById('css-editor-audio-color').value = rgbArrayToHex(css.color || [0, 255, 0]);
|
document.getElementById('css-editor-audio-color').value = rgbArrayToHex(css.color || [0, 255, 0]);
|
||||||
document.getElementById('css-editor-audio-color-peak').value = rgbArrayToHex(css.color_peak || [255, 0, 0]);
|
document.getElementById('css-editor-audio-color-peak').value = rgbArrayToHex(css.color_peak || [255, 0, 0]);
|
||||||
document.getElementById('css-editor-audio-mirror').checked = css.mirror || false;
|
document.getElementById('css-editor-audio-mirror').checked = css.mirror || false;
|
||||||
@@ -647,11 +764,13 @@ function _loadAudioState(css) {
|
|||||||
|
|
||||||
function _resetAudioState() {
|
function _resetAudioState() {
|
||||||
document.getElementById('css-editor-audio-viz').value = 'spectrum';
|
document.getElementById('css-editor-audio-viz').value = 'spectrum';
|
||||||
|
if (_audioVizIconSelect) _audioVizIconSelect.setValue('spectrum');
|
||||||
document.getElementById('css-editor-audio-sensitivity').value = 1.0;
|
document.getElementById('css-editor-audio-sensitivity').value = 1.0;
|
||||||
document.getElementById('css-editor-audio-sensitivity-val').textContent = '1.0';
|
document.getElementById('css-editor-audio-sensitivity-val').textContent = '1.0';
|
||||||
document.getElementById('css-editor-audio-smoothing').value = 0.3;
|
document.getElementById('css-editor-audio-smoothing').value = 0.3;
|
||||||
document.getElementById('css-editor-audio-smoothing-val').textContent = '0.30';
|
document.getElementById('css-editor-audio-smoothing-val').textContent = '0.30';
|
||||||
document.getElementById('css-editor-audio-palette').value = 'rainbow';
|
document.getElementById('css-editor-audio-palette').value = 'rainbow';
|
||||||
|
if (_audioPaletteIconSelect) _audioPaletteIconSelect.setValue('rainbow');
|
||||||
document.getElementById('css-editor-audio-color').value = '#00ff00';
|
document.getElementById('css-editor-audio-color').value = '#00ff00';
|
||||||
document.getElementById('css-editor-audio-color-peak').value = '#ff0000';
|
document.getElementById('css-editor-audio-color-peak').value = '#ff0000';
|
||||||
document.getElementById('css-editor-audio-mirror').checked = false;
|
document.getElementById('css-editor-audio-mirror').checked = false;
|
||||||
@@ -715,6 +834,7 @@ function _notificationGetAppColorsDict() {
|
|||||||
|
|
||||||
function _loadNotificationState(css) {
|
function _loadNotificationState(css) {
|
||||||
document.getElementById('css-editor-notification-effect').value = css.notification_effect || 'flash';
|
document.getElementById('css-editor-notification-effect').value = css.notification_effect || 'flash';
|
||||||
|
if (_notificationEffectIconSelect) _notificationEffectIconSelect.setValue(css.notification_effect || 'flash');
|
||||||
const dur = css.duration_ms ?? 1500;
|
const dur = css.duration_ms ?? 1500;
|
||||||
document.getElementById('css-editor-notification-duration').value = dur;
|
document.getElementById('css-editor-notification-duration').value = dur;
|
||||||
document.getElementById('css-editor-notification-duration-val').textContent = dur;
|
document.getElementById('css-editor-notification-duration-val').textContent = dur;
|
||||||
@@ -734,6 +854,7 @@ function _loadNotificationState(css) {
|
|||||||
|
|
||||||
function _resetNotificationState() {
|
function _resetNotificationState() {
|
||||||
document.getElementById('css-editor-notification-effect').value = 'flash';
|
document.getElementById('css-editor-notification-effect').value = 'flash';
|
||||||
|
if (_notificationEffectIconSelect) _notificationEffectIconSelect.setValue('flash');
|
||||||
document.getElementById('css-editor-notification-duration').value = 1500;
|
document.getElementById('css-editor-notification-duration').value = 1500;
|
||||||
document.getElementById('css-editor-notification-duration-val').textContent = '1500';
|
document.getElementById('css-editor-notification-duration-val').textContent = '1500';
|
||||||
document.getElementById('css-editor-notification-default-color').value = '#ffffff';
|
document.getElementById('css-editor-notification-default-color').value = '#ffffff';
|
||||||
@@ -993,6 +1114,7 @@ export async function showCSSEditor(cssId = null, cloneData = null) {
|
|||||||
_loadColorCycleState(css);
|
_loadColorCycleState(css);
|
||||||
} else if (sourceType === 'gradient') {
|
} else if (sourceType === 'gradient') {
|
||||||
document.getElementById('css-editor-gradient-preset').value = '';
|
document.getElementById('css-editor-gradient-preset').value = '';
|
||||||
|
if (_gradientPresetIconSelect) _gradientPresetIconSelect.setValue('');
|
||||||
gradientInit(css.stops || [
|
gradientInit(css.stops || [
|
||||||
{ 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] },
|
||||||
@@ -1000,8 +1122,10 @@ export async function showCSSEditor(cssId = null, cloneData = null) {
|
|||||||
_loadAnimationState(css.animation);
|
_loadAnimationState(css.animation);
|
||||||
} else if (sourceType === 'effect') {
|
} else if (sourceType === 'effect') {
|
||||||
document.getElementById('css-editor-effect-type').value = css.effect_type || 'fire';
|
document.getElementById('css-editor-effect-type').value = css.effect_type || 'fire';
|
||||||
|
if (_effectTypeIconSelect) _effectTypeIconSelect.setValue(css.effect_type || 'fire');
|
||||||
onEffectTypeChange();
|
onEffectTypeChange();
|
||||||
document.getElementById('css-editor-effect-palette').value = css.palette || 'fire';
|
document.getElementById('css-editor-effect-palette').value = css.palette || 'fire';
|
||||||
|
if (_effectPaletteIconSelect) _effectPaletteIconSelect.setValue(css.palette || 'fire');
|
||||||
document.getElementById('css-editor-effect-color').value = rgbArrayToHex(css.color || [255, 80, 0]);
|
document.getElementById('css-editor-effect-color').value = rgbArrayToHex(css.color || [255, 80, 0]);
|
||||||
document.getElementById('css-editor-effect-intensity').value = css.intensity ?? 1.0;
|
document.getElementById('css-editor-effect-intensity').value = css.intensity ?? 1.0;
|
||||||
document.getElementById('css-editor-effect-intensity-val').textContent = parseFloat(css.intensity ?? 1.0).toFixed(1);
|
document.getElementById('css-editor-effect-intensity-val').textContent = parseFloat(css.intensity ?? 1.0).toFixed(1);
|
||||||
|
|||||||
@@ -78,6 +78,27 @@ function _buildVSTypeItems() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let _vsTypeIconSelect = null;
|
let _vsTypeIconSelect = null;
|
||||||
|
let _waveformIconSelect = null;
|
||||||
|
|
||||||
|
const _WAVEFORM_SVG = {
|
||||||
|
sine: '<svg viewBox="0 0 60 24" width="60" height="24" fill="none" stroke="currentColor" stroke-width="2"><path d="M0 12 Q15 -4 30 12 Q45 28 60 12"/></svg>',
|
||||||
|
triangle: '<svg viewBox="0 0 60 24" width="60" height="24" fill="none" stroke="currentColor" stroke-width="2"><path d="M0 12 L15 2 L45 22 L60 12"/></svg>',
|
||||||
|
square: '<svg viewBox="0 0 60 24" width="60" height="24" fill="none" stroke="currentColor" stroke-width="2"><path d="M0 20 L0 4 L30 4 L30 20 L60 20 L60 4"/></svg>',
|
||||||
|
sawtooth: '<svg viewBox="0 0 60 24" width="60" height="24" fill="none" stroke="currentColor" stroke-width="2"><path d="M0 20 L30 4 L30 20 L60 4"/></svg>',
|
||||||
|
};
|
||||||
|
|
||||||
|
function _ensureWaveformIconSelect() {
|
||||||
|
const sel = document.getElementById('value-source-waveform');
|
||||||
|
if (!sel) return;
|
||||||
|
const items = [
|
||||||
|
{ value: 'sine', icon: _WAVEFORM_SVG.sine, label: t('value_source.waveform.sine') },
|
||||||
|
{ value: 'triangle', icon: _WAVEFORM_SVG.triangle, label: t('value_source.waveform.triangle') },
|
||||||
|
{ value: 'square', icon: _WAVEFORM_SVG.square, label: t('value_source.waveform.square') },
|
||||||
|
{ value: 'sawtooth', icon: _WAVEFORM_SVG.sawtooth, label: t('value_source.waveform.sawtooth') },
|
||||||
|
];
|
||||||
|
if (_waveformIconSelect) { _waveformIconSelect.updateItems(items); return; }
|
||||||
|
_waveformIconSelect = new IconSelect({ target: sel, items, columns: 4 });
|
||||||
|
}
|
||||||
|
|
||||||
function _ensureVSTypeIconSelect() {
|
function _ensureVSTypeIconSelect() {
|
||||||
const sel = document.getElementById('value-source-type');
|
const sel = document.getElementById('value-source-type');
|
||||||
@@ -111,6 +132,7 @@ export async function showValueSourceModal(editData) {
|
|||||||
_setSlider('value-source-value', editData.value ?? 1.0);
|
_setSlider('value-source-value', editData.value ?? 1.0);
|
||||||
} else if (editData.source_type === 'animated') {
|
} else if (editData.source_type === 'animated') {
|
||||||
document.getElementById('value-source-waveform').value = editData.waveform || 'sine';
|
document.getElementById('value-source-waveform').value = editData.waveform || 'sine';
|
||||||
|
if (_waveformIconSelect) _waveformIconSelect.setValue(editData.waveform || 'sine');
|
||||||
_setSlider('value-source-speed', editData.speed ?? 10);
|
_setSlider('value-source-speed', editData.speed ?? 10);
|
||||||
_setSlider('value-source-min-value', editData.min_value ?? 0);
|
_setSlider('value-source-min-value', editData.min_value ?? 0);
|
||||||
_setSlider('value-source-max-value', editData.max_value ?? 1);
|
_setSlider('value-source-max-value', editData.max_value ?? 1);
|
||||||
@@ -174,6 +196,7 @@ export function onValueSourceTypeChange() {
|
|||||||
if (_vsTypeIconSelect) _vsTypeIconSelect.setValue(type);
|
if (_vsTypeIconSelect) _vsTypeIconSelect.setValue(type);
|
||||||
document.getElementById('value-source-static-section').style.display = type === 'static' ? '' : 'none';
|
document.getElementById('value-source-static-section').style.display = type === 'static' ? '' : 'none';
|
||||||
document.getElementById('value-source-animated-section').style.display = type === 'animated' ? '' : 'none';
|
document.getElementById('value-source-animated-section').style.display = type === 'animated' ? '' : 'none';
|
||||||
|
if (type === 'animated') _ensureWaveformIconSelect();
|
||||||
document.getElementById('value-source-audio-section').style.display = type === 'audio' ? '' : 'none';
|
document.getElementById('value-source-audio-section').style.display = type === 'audio' ? '' : 'none';
|
||||||
document.getElementById('value-source-adaptive-time-section').style.display = type === 'adaptive_time' ? '' : 'none';
|
document.getElementById('value-source-adaptive-time-section').style.display = type === 'adaptive_time' ? '' : 'none';
|
||||||
document.getElementById('value-source-adaptive-scene-section').style.display = type === 'adaptive_scene' ? '' : 'none';
|
document.getElementById('value-source-adaptive-scene-section').style.display = type === 'adaptive_scene' ? '' : 'none';
|
||||||
|
|||||||
@@ -840,8 +840,11 @@
|
|||||||
"color_strip.notification.effect": "Effect:",
|
"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.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.flash": "Flash",
|
||||||
|
"color_strip.notification.effect.flash.desc": "Instant on, linear fade-out",
|
||||||
"color_strip.notification.effect.pulse": "Pulse",
|
"color_strip.notification.effect.pulse": "Pulse",
|
||||||
|
"color_strip.notification.effect.pulse.desc": "Smooth bell-curve glow",
|
||||||
"color_strip.notification.effect.sweep": "Sweep",
|
"color_strip.notification.effect.sweep": "Sweep",
|
||||||
|
"color_strip.notification.effect.sweep.desc": "Fills left-to-right then fades",
|
||||||
"color_strip.notification.duration": "Duration (ms):",
|
"color_strip.notification.duration": "Duration (ms):",
|
||||||
"color_strip.notification.duration.hint": "How long the notification effect plays, in milliseconds.",
|
"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": "Default Color:",
|
||||||
@@ -888,8 +891,11 @@
|
|||||||
"color_strip.audio.visualization": "Visualization:",
|
"color_strip.audio.visualization": "Visualization:",
|
||||||
"color_strip.audio.visualization.hint": "How audio data is rendered to LEDs.",
|
"color_strip.audio.visualization.hint": "How audio data is rendered to LEDs.",
|
||||||
"color_strip.audio.viz.spectrum": "Spectrum Analyzer",
|
"color_strip.audio.viz.spectrum": "Spectrum Analyzer",
|
||||||
|
"color_strip.audio.viz.spectrum.desc": "Frequency bars across the strip",
|
||||||
"color_strip.audio.viz.beat_pulse": "Beat Pulse",
|
"color_strip.audio.viz.beat_pulse": "Beat Pulse",
|
||||||
|
"color_strip.audio.viz.beat_pulse.desc": "All LEDs pulse on the beat",
|
||||||
"color_strip.audio.viz.vu_meter": "VU Meter",
|
"color_strip.audio.viz.vu_meter": "VU Meter",
|
||||||
|
"color_strip.audio.viz.vu_meter.desc": "Volume level fills the strip",
|
||||||
"color_strip.audio.source": "Audio Source:",
|
"color_strip.audio.source": "Audio Source:",
|
||||||
"color_strip.audio.source.hint": "Audio source for this visualization. Can be a multichannel (device) or mono (single channel) source. Create and manage audio sources in the Sources tab.",
|
"color_strip.audio.source.hint": "Audio source for this visualization. Can be a multichannel (device) or mono (single channel) source. Create and manage audio sources in the Sources tab.",
|
||||||
"color_strip.audio.sensitivity": "Sensitivity:",
|
"color_strip.audio.sensitivity": "Sensitivity:",
|
||||||
|
|||||||
@@ -840,8 +840,11 @@
|
|||||||
"color_strip.notification.effect": "Эффект:",
|
"color_strip.notification.effect": "Эффект:",
|
||||||
"color_strip.notification.effect.hint": "Визуальный эффект при уведомлении. Вспышка — линейное затухание, Пульс — плавная волна, Волна — заполнение и затухание.",
|
"color_strip.notification.effect.hint": "Визуальный эффект при уведомлении. Вспышка — линейное затухание, Пульс — плавная волна, Волна — заполнение и затухание.",
|
||||||
"color_strip.notification.effect.flash": "Вспышка",
|
"color_strip.notification.effect.flash": "Вспышка",
|
||||||
|
"color_strip.notification.effect.flash.desc": "Мгновенное включение, линейное затухание",
|
||||||
"color_strip.notification.effect.pulse": "Пульс",
|
"color_strip.notification.effect.pulse": "Пульс",
|
||||||
|
"color_strip.notification.effect.pulse.desc": "Плавное свечение колоколом",
|
||||||
"color_strip.notification.effect.sweep": "Волна",
|
"color_strip.notification.effect.sweep": "Волна",
|
||||||
|
"color_strip.notification.effect.sweep.desc": "Заполняет слева направо, затем гаснет",
|
||||||
"color_strip.notification.duration": "Длительность (мс):",
|
"color_strip.notification.duration": "Длительность (мс):",
|
||||||
"color_strip.notification.duration.hint": "Как долго длится эффект уведомления в миллисекундах.",
|
"color_strip.notification.duration.hint": "Как долго длится эффект уведомления в миллисекундах.",
|
||||||
"color_strip.notification.default_color": "Цвет по умолчанию:",
|
"color_strip.notification.default_color": "Цвет по умолчанию:",
|
||||||
@@ -888,8 +891,11 @@
|
|||||||
"color_strip.audio.visualization": "Визуализация:",
|
"color_strip.audio.visualization": "Визуализация:",
|
||||||
"color_strip.audio.visualization.hint": "Способ отображения аудиоданных на LED.",
|
"color_strip.audio.visualization.hint": "Способ отображения аудиоданных на LED.",
|
||||||
"color_strip.audio.viz.spectrum": "Анализатор спектра",
|
"color_strip.audio.viz.spectrum": "Анализатор спектра",
|
||||||
|
"color_strip.audio.viz.spectrum.desc": "Частотные полосы по ленте",
|
||||||
"color_strip.audio.viz.beat_pulse": "Пульс бита",
|
"color_strip.audio.viz.beat_pulse": "Пульс бита",
|
||||||
|
"color_strip.audio.viz.beat_pulse.desc": "Все LED пульсируют в такт",
|
||||||
"color_strip.audio.viz.vu_meter": "VU-метр",
|
"color_strip.audio.viz.vu_meter": "VU-метр",
|
||||||
|
"color_strip.audio.viz.vu_meter.desc": "Уровень громкости заполняет ленту",
|
||||||
"color_strip.audio.source": "Аудиоисточник:",
|
"color_strip.audio.source": "Аудиоисточник:",
|
||||||
"color_strip.audio.source.hint": "Аудиоисточник для визуализации. Может быть многоканальным (устройство) или моно (один канал). Создавайте и управляйте аудиоисточниками на вкладке Источники.",
|
"color_strip.audio.source.hint": "Аудиоисточник для визуализации. Может быть многоканальным (устройство) или моно (один канал). Создавайте и управляйте аудиоисточниками на вкладке Источники.",
|
||||||
"color_strip.audio.sensitivity": "Чувствительность:",
|
"color_strip.audio.sensitivity": "Чувствительность:",
|
||||||
|
|||||||
@@ -840,8 +840,11 @@
|
|||||||
"color_strip.notification.effect": "效果:",
|
"color_strip.notification.effect": "效果:",
|
||||||
"color_strip.notification.effect.hint": "通知触发时的视觉效果。闪烁线性衰减,脉冲平滑钟形曲线,扫描从左到右填充后衰减。",
|
"color_strip.notification.effect.hint": "通知触发时的视觉效果。闪烁线性衰减,脉冲平滑钟形曲线,扫描从左到右填充后衰减。",
|
||||||
"color_strip.notification.effect.flash": "闪烁",
|
"color_strip.notification.effect.flash": "闪烁",
|
||||||
|
"color_strip.notification.effect.flash.desc": "瞬时点亮,线性衰减",
|
||||||
"color_strip.notification.effect.pulse": "脉冲",
|
"color_strip.notification.effect.pulse": "脉冲",
|
||||||
|
"color_strip.notification.effect.pulse.desc": "平滑钟形发光",
|
||||||
"color_strip.notification.effect.sweep": "扫描",
|
"color_strip.notification.effect.sweep": "扫描",
|
||||||
|
"color_strip.notification.effect.sweep.desc": "从左到右填充然后消失",
|
||||||
"color_strip.notification.duration": "持续时间(毫秒):",
|
"color_strip.notification.duration": "持续时间(毫秒):",
|
||||||
"color_strip.notification.duration.hint": "通知效果播放的时长(毫秒)。",
|
"color_strip.notification.duration.hint": "通知效果播放的时长(毫秒)。",
|
||||||
"color_strip.notification.default_color": "默认颜色:",
|
"color_strip.notification.default_color": "默认颜色:",
|
||||||
@@ -888,8 +891,11 @@
|
|||||||
"color_strip.audio.visualization": "可视化:",
|
"color_strip.audio.visualization": "可视化:",
|
||||||
"color_strip.audio.visualization.hint": "音频数据如何渲染到 LED。",
|
"color_strip.audio.visualization.hint": "音频数据如何渲染到 LED。",
|
||||||
"color_strip.audio.viz.spectrum": "频谱分析",
|
"color_strip.audio.viz.spectrum": "频谱分析",
|
||||||
|
"color_strip.audio.viz.spectrum.desc": "频率条分布在灯带上",
|
||||||
"color_strip.audio.viz.beat_pulse": "节拍脉冲",
|
"color_strip.audio.viz.beat_pulse": "节拍脉冲",
|
||||||
|
"color_strip.audio.viz.beat_pulse.desc": "所有LED随节拍脉动",
|
||||||
"color_strip.audio.viz.vu_meter": "VU 表",
|
"color_strip.audio.viz.vu_meter": "VU 表",
|
||||||
|
"color_strip.audio.viz.vu_meter.desc": "音量填充灯带",
|
||||||
"color_strip.audio.source": "音频源:",
|
"color_strip.audio.source": "音频源:",
|
||||||
"color_strip.audio.source.hint": "此可视化的音频源。可以是多声道(设备)或单声道(单通道)源。在源标签页中创建和管理音频源。",
|
"color_strip.audio.source.hint": "此可视化的音频源。可以是多声道(设备)或单声道(单通道)源。在源标签页中创建和管理音频源。",
|
||||||
"color_strip.audio.sensitivity": "灵敏度:",
|
"color_strip.audio.sensitivity": "灵敏度:",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
* - Navigation: network-first with offline fallback
|
* - Navigation: network-first with offline fallback
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const CACHE_NAME = 'ledgrab-v18';
|
const CACHE_NAME = 'ledgrab-v19';
|
||||||
|
|
||||||
// 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.
|
||||||
|
|||||||
Reference in New Issue
Block a user