Comprehensive WebUI review: 41 UX/feature/CSS improvements
Safety & Correctness: - Add confirmation dialogs to Stop All, turnOffDevice - i18n confirm dialog (title, yes, no buttons) - Fix duplicate tutorial-overlay ID - Define missing CSS variables (--radius, --text-primary, --hover-bg, --input-bg) - Fix toast z-index conflict with confirm dialog (2500 → 3000) UX Consistency: - Add backdrop-close to test modals - Add device clone feature (only entity without it) - Add sync clocks to command palette - Replace 20+ hardcoded accent colors with CSS vars/color-mix() - Remove dead .badge duplicate from components.css - Make calibration elements keyboard-accessible (div → button) - Add aria-labels to color picker swatches - Fix pattern canvas mobile horizontal scroll - Fix graph editor mobile bottom clipping Polish: - Add empty-state messages to all CardSection instances - Convert 21 px font-sizes to rem - Add scroll-behavior: smooth with reduced-motion override - Add @media print styles - Add :focus-visible to 4 missing interactive elements - Fix settings modal close label (Cancel → Close) - Fix api-key submit button i18n New Features: - Command palette actions: start/stop targets, activate scenes, enable/disable - Bulk start/stop API endpoints (POST /output-targets/bulk/start|stop) - OS notification history viewer modal - Scene "used by" automation reference count on cards - Clock elapsed time display on Streams tab cards - Device "last seen" relative timestamp on cards - Audio device refresh button in edit modal - Composite layer drag-to-reorder - MQTT settings panel (broker config with JSON persistence) - WebSocket log viewer with level filtering and ring buffer - Runtime log-level adjustment (GET/PUT endpoints + settings UI) - Animated value source waveform canvas preview - Gradient custom preset save/delete (localStorage) - API key read-only display in settings - Backup metadata (file size, auto/manual badges) - Server restart button with confirm + overlay - Partial config export/import per entity type - Progressive disclosure in target editor (Advanced section) CSS Architecture: - Define radius scale tokens (--radius-sm/md/lg/pill) - Scope .cs-filter selectors to remove 7 !important overrides - Consolidate duplicate toggle switch (filter-list → settings-toggle) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -167,6 +167,7 @@
|
||||
{% include 'modals/target-editor.html' %}
|
||||
{% include 'modals/css-editor.html' %}
|
||||
{% include 'modals/test-css-source.html' %}
|
||||
{% include 'modals/notification-history.html' %}
|
||||
{% include 'modals/kc-editor.html' %}
|
||||
{% include 'modals/pattern-template.html' %}
|
||||
{% include 'modals/api-key.html' %}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-icon btn-secondary" onclick="closeApiKeyModal()" id="modal-cancel-btn" title="Cancel" data-i18n-aria-label="aria.cancel">✕</button>
|
||||
<button type="submit" class="btn btn-icon btn-primary" title="Login" data-i18n-aria-label="aria.save">✓</button>
|
||||
<button type="submit" class="btn btn-icon btn-primary" data-i18n-title="api_key.login" title="Login" data-i18n-aria-label="aria.save">✓</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -44,9 +44,12 @@
|
||||
<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="audio_source.device.hint">Audio input source. Loopback devices capture system audio output; input devices capture microphone or line-in.</small>
|
||||
<select id="audio-source-device">
|
||||
<!-- populated dynamically -->
|
||||
</select>
|
||||
<div class="select-with-action">
|
||||
<select id="audio-source-device">
|
||||
<!-- populated dynamically -->
|
||||
</select>
|
||||
<button type="button" class="btn btn-secondary btn-sm" id="audio-source-refresh-devices" onclick="refreshAudioDevices()" data-i18n-title="audio_source.refresh_devices" title="Refresh devices">↻</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -81,16 +81,16 @@
|
||||
</div>
|
||||
|
||||
<!-- Edge test toggle zones (outside container border, in tick area) -->
|
||||
<div class="edge-toggle toggle-top" onclick="toggleTestEdge('top')"></div>
|
||||
<div class="edge-toggle toggle-right" onclick="toggleTestEdge('right')"></div>
|
||||
<div class="edge-toggle toggle-bottom" onclick="toggleTestEdge('bottom')"></div>
|
||||
<div class="edge-toggle toggle-left" onclick="toggleTestEdge('left')"></div>
|
||||
<button type="button" class="edge-toggle toggle-top" onclick="toggleTestEdge('top')"></button>
|
||||
<button type="button" class="edge-toggle toggle-right" onclick="toggleTestEdge('right')"></button>
|
||||
<button type="button" class="edge-toggle toggle-bottom" onclick="toggleTestEdge('bottom')"></button>
|
||||
<button type="button" class="edge-toggle toggle-left" onclick="toggleTestEdge('left')"></button>
|
||||
|
||||
<!-- Corner start position buttons -->
|
||||
<div class="preview-corner corner-top-left" onclick="setStartPosition('top_left')">●</div>
|
||||
<div class="preview-corner corner-top-right" onclick="setStartPosition('top_right')">●</div>
|
||||
<div class="preview-corner corner-bottom-left" onclick="setStartPosition('bottom_left')">●</div>
|
||||
<div class="preview-corner corner-bottom-right" onclick="setStartPosition('bottom_right')">●</div>
|
||||
<button type="button" class="preview-corner corner-top-left" onclick="setStartPosition('top_left')">●</button>
|
||||
<button type="button" class="preview-corner corner-top-right" onclick="setStartPosition('top_right')">●</button>
|
||||
<button type="button" class="preview-corner corner-bottom-left" onclick="setStartPosition('bottom_left')">●</button>
|
||||
<button type="button" class="preview-corner corner-bottom-right" onclick="setStartPosition('bottom_right')">●</button>
|
||||
|
||||
<!-- Canvas overlay for ticks, arrows, start label -->
|
||||
<canvas id="calibration-preview-canvas"></canvas>
|
||||
@@ -141,7 +141,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Tutorial Overlay -->
|
||||
<div id="tutorial-overlay" class="tutorial-overlay">
|
||||
<div id="calibration-tutorial-overlay" class="tutorial-overlay">
|
||||
<div class="tutorial-backdrop"></div>
|
||||
<div class="tutorial-ring"></div>
|
||||
<div class="tutorial-tooltip">
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
<div id="confirm-modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="confirm-modal-title">
|
||||
<div class="modal-content" style="max-width: 450px;">
|
||||
<div class="modal-header">
|
||||
<h2 id="confirm-modal-title">Confirm Action</h2>
|
||||
<h2 id="confirm-modal-title" data-i18n="confirm.title">Confirm Action</h2>
|
||||
<button class="modal-close-btn" onclick="closeConfirmModal(false)" title="Close" data-i18n-aria-label="aria.close">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p id="confirm-message" class="modal-description"></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" id="confirm-no-btn" onclick="closeConfirmModal(false)">No</button>
|
||||
<button class="btn btn-danger" id="confirm-yes-btn" onclick="closeConfirmModal(true)">Yes</button>
|
||||
<button class="btn btn-secondary" id="confirm-no-btn" onclick="closeConfirmModal(false)" data-i18n="confirm.no">No</button>
|
||||
<button class="btn btn-danger" id="confirm-yes-btn" onclick="closeConfirmModal(true)" data-i18n="confirm.yes">Yes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -109,7 +109,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="color_strip.gradient.preset.hint">Load a predefined gradient palette. Selecting a preset replaces the current stops.</small>
|
||||
<select id="css-editor-gradient-preset" onchange="applyGradientPreset(this.value)">
|
||||
<select id="css-editor-gradient-preset" onchange="onGradientPresetChange(this.value)">
|
||||
<option value="" data-i18n="color_strip.gradient.preset.custom">— Custom —</option>
|
||||
<option value="rainbow" data-i18n="color_strip.gradient.preset.rainbow">Rainbow</option>
|
||||
<option value="sunset" data-i18n="color_strip.gradient.preset.sunset">Sunset</option>
|
||||
@@ -124,6 +124,12 @@
|
||||
<option value="neon" data-i18n="color_strip.gradient.preset.neon">Neon</option>
|
||||
<option value="pastel" data-i18n="color_strip.gradient.preset.pastel">Pastel</option>
|
||||
</select>
|
||||
<div style="margin-top:6px;">
|
||||
<button type="button" class="btn btn-secondary btn-sm"
|
||||
onclick="promptAndSaveGradientPreset()"
|
||||
data-i18n="color_strip.gradient.preset.save_button">Save as preset…</button>
|
||||
</div>
|
||||
<div id="css-editor-custom-presets-list" class="custom-presets-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<!-- OS Notification History Modal -->
|
||||
<div id="notification-history-modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="notification-history-modal-title" style="display:none">
|
||||
<div class="modal-content" style="max-width:520px;width:100%">
|
||||
<div class="modal-header">
|
||||
<h2 id="notification-history-modal-title" data-i18n="color_strip.notification.history.title">Notification History</h2>
|
||||
<button class="modal-close-btn" onclick="closeNotificationHistory()" title="Close" data-i18n-aria-label="aria.close">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="form-hint" data-i18n="color_strip.notification.history.hint">Recent OS notifications captured by the listener (newest first). Up to 50 entries.</p>
|
||||
<div id="notification-history-status" style="display:none;color:var(--text-muted);font-size:0.85rem;margin-bottom:0.5rem"></div>
|
||||
<div id="notification-history-list" style="max-height:340px;overflow-y:auto;border:1px solid var(--border-color);border-radius:4px;padding:0.25rem 0"></div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-secondary" onclick="refreshNotificationHistory()" data-i18n="color_strip.notification.history.refresh">Refresh</button>
|
||||
<button class="btn btn-secondary" onclick="closeNotificationHistory()" data-i18n="settings.button.cancel">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -6,6 +6,16 @@
|
||||
<button class="modal-close-btn" onclick="closeSettingsModal()" title="Close" data-i18n-aria-label="aria.close">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- API Keys section (read-only) -->
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label data-i18n="settings.api_keys.label">API Keys</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="settings.api_keys.hint">API keys are defined in the server config file (config.yaml). Restart the server after editing the file to apply changes.</small>
|
||||
<div id="settings-api-keys-list" style="font-size:0.85rem;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Backup section -->
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
@@ -27,6 +37,43 @@
|
||||
<button class="btn btn-danger" onclick="document.getElementById('settings-restore-input').click()" style="width:100%" data-i18n="settings.restore.button">Restore from Backup</button>
|
||||
</div>
|
||||
|
||||
<!-- Partial Export/Import section -->
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label data-i18n="settings.partial.label">Partial Export / Import</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="settings.partial.hint">Export or import a single entity type. Import replaces or merges existing data and restarts the server.</small>
|
||||
|
||||
<div style="display:flex;gap:0.5rem;margin-bottom:0.5rem;">
|
||||
<select id="settings-partial-store" style="flex:1">
|
||||
<option value="devices" data-i18n="settings.partial.store.devices">Devices</option>
|
||||
<option value="output_targets" data-i18n="settings.partial.store.output_targets">LED Targets</option>
|
||||
<option value="color_strip_sources" data-i18n="settings.partial.store.color_strip_sources">Color Strips</option>
|
||||
<option value="picture_sources" data-i18n="settings.partial.store.picture_sources">Picture Sources</option>
|
||||
<option value="audio_sources" data-i18n="settings.partial.store.audio_sources">Audio Sources</option>
|
||||
<option value="audio_templates" data-i18n="settings.partial.store.audio_templates">Audio Templates</option>
|
||||
<option value="capture_templates" data-i18n="settings.partial.store.capture_templates">Capture Templates</option>
|
||||
<option value="postprocessing_templates" data-i18n="settings.partial.store.postprocessing_templates">Post-processing Templates</option>
|
||||
<option value="color_strip_processing_templates" data-i18n="settings.partial.store.color_strip_processing_templates">CSS Processing Templates</option>
|
||||
<option value="pattern_templates" data-i18n="settings.partial.store.pattern_templates">Pattern Templates</option>
|
||||
<option value="value_sources" data-i18n="settings.partial.store.value_sources">Value Sources</option>
|
||||
<option value="sync_clocks" data-i18n="settings.partial.store.sync_clocks">Sync Clocks</option>
|
||||
<option value="automations" data-i18n="settings.partial.store.automations">Automations</option>
|
||||
<option value="scene_presets" data-i18n="settings.partial.store.scene_presets">Scene Presets</option>
|
||||
</select>
|
||||
<button class="btn btn-secondary" onclick="downloadPartialExport()" data-i18n="settings.partial.export_button">Export</button>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.5rem;">
|
||||
<input type="checkbox" id="settings-partial-merge">
|
||||
<label for="settings-partial-merge" style="margin:0;font-size:0.85rem;" data-i18n="settings.partial.merge_label">Merge (add/overwrite, keep existing)</label>
|
||||
</div>
|
||||
|
||||
<input type="file" id="settings-partial-import-input" accept=".json" style="display:none" onchange="handlePartialImportFileSelected(this)">
|
||||
<button class="btn btn-secondary" onclick="document.getElementById('settings-partial-import-input').click()" style="width:100%" data-i18n="settings.partial.import_button">Import from File</button>
|
||||
</div>
|
||||
|
||||
<!-- Auto-Backup section -->
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
@@ -73,10 +120,109 @@
|
||||
<div id="saved-backups-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- MQTT section -->
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label data-i18n="settings.mqtt.label">MQTT</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="settings.mqtt.hint">Configure MQTT broker connection for automation conditions and triggers.</small>
|
||||
|
||||
<div style="display:flex; align-items:center; gap:0.5rem; margin-bottom:0.75rem;">
|
||||
<input type="checkbox" id="mqtt-enabled">
|
||||
<label for="mqtt-enabled" style="margin:0" data-i18n="settings.mqtt.enabled">Enable MQTT</label>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; gap:0.5rem; margin-bottom:0.5rem;">
|
||||
<div style="flex:1">
|
||||
<label for="mqtt-host" style="font-size:0.85rem" data-i18n="settings.mqtt.host_label">Broker Host</label>
|
||||
<input type="text" id="mqtt-host" placeholder="localhost" style="width:100%">
|
||||
</div>
|
||||
<div style="width:90px">
|
||||
<label for="mqtt-port" style="font-size:0.85rem" data-i18n="settings.mqtt.port_label">Port</label>
|
||||
<input type="number" id="mqtt-port" min="1" max="65535" value="1883" style="width:100%">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; gap:0.5rem; margin-bottom:0.5rem;">
|
||||
<div style="flex:1">
|
||||
<label for="mqtt-username" style="font-size:0.85rem" data-i18n="settings.mqtt.username_label">Username</label>
|
||||
<input type="text" id="mqtt-username" placeholder="" autocomplete="off" style="width:100%">
|
||||
</div>
|
||||
<div style="flex:1">
|
||||
<label for="mqtt-password" style="font-size:0.85rem" data-i18n="settings.mqtt.password_label">Password</label>
|
||||
<input type="password" id="mqtt-password" placeholder="" autocomplete="new-password" style="width:100%">
|
||||
<small id="mqtt-password-hint" style="display:none;font-size:0.75rem;color:var(--text-muted)" data-i18n="settings.mqtt.password_set_hint">Password is set — leave blank to keep</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; gap:0.5rem; margin-bottom:0.75rem;">
|
||||
<div style="flex:1">
|
||||
<label for="mqtt-client-id" style="font-size:0.85rem" data-i18n="settings.mqtt.client_id_label">Client ID</label>
|
||||
<input type="text" id="mqtt-client-id" placeholder="ledgrab" style="width:100%">
|
||||
</div>
|
||||
<div style="flex:1">
|
||||
<label for="mqtt-base-topic" style="font-size:0.85rem" data-i18n="settings.mqtt.base_topic_label">Base Topic</label>
|
||||
<input type="text" id="mqtt-base-topic" placeholder="ledgrab" style="width:100%">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" onclick="saveMqttSettings()" style="width:100%" data-i18n="settings.mqtt.save">Save MQTT Settings</button>
|
||||
</div>
|
||||
|
||||
<!-- Server Logs section -->
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label data-i18n="settings.logs.label">Server Logs</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="settings.logs.hint">Stream live server log output. Use the filter to show only relevant log levels.</small>
|
||||
|
||||
<div style="display:flex; gap:0.5rem; align-items:center; margin-bottom:0.5rem;">
|
||||
<button id="log-viewer-connect-btn" class="btn btn-secondary" onclick="connectLogViewer()" data-i18n="settings.logs.connect">Connect</button>
|
||||
<button class="btn btn-secondary" onclick="clearLogViewer()" data-i18n="settings.logs.clear">Clear</button>
|
||||
<select id="log-viewer-filter" onchange="applyLogFilter()" style="flex:1; font-size:0.85rem;">
|
||||
<option value="all" data-i18n="settings.logs.filter.all">All</option>
|
||||
<option value="INFO" data-i18n="settings.logs.filter.info">Info+</option>
|
||||
<option value="WARNING" data-i18n="settings.logs.filter.warning">Warning+</option>
|
||||
<option value="ERROR" data-i18n="settings.logs.filter.error">Error only</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<pre id="log-viewer-output" class="log-viewer-output"></pre>
|
||||
</div>
|
||||
|
||||
<!-- Log Level section -->
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label data-i18n="settings.log_level.label">Log Level</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="settings.log_level.hint">Change the server log verbosity at runtime. DEBUG shows the most detail; CRITICAL shows only fatal errors.</small>
|
||||
<div style="display:flex;gap:0.5rem;">
|
||||
<select id="settings-log-level" style="flex:1">
|
||||
<option value="DEBUG">DEBUG</option>
|
||||
<option value="INFO">INFO</option>
|
||||
<option value="WARNING">WARNING</option>
|
||||
<option value="ERROR">ERROR</option>
|
||||
<option value="CRITICAL">CRITICAL</option>
|
||||
</select>
|
||||
<button class="btn btn-primary" onclick="setLogLevel()" data-i18n="settings.log_level.save">Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Restart section -->
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label data-i18n="settings.restart_server">Restart Server</label>
|
||||
</div>
|
||||
<button class="btn btn-secondary" onclick="restartServer()" style="width:100%" data-i18n="settings.restart_server">Restart Server</button>
|
||||
</div>
|
||||
|
||||
<div id="settings-error" class="error-message" style="display:none;"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-icon btn-secondary" onclick="closeSettingsModal()" title="Close" data-i18n-title="settings.button.cancel" data-i18n-aria-label="aria.close">✕</button>
|
||||
<button class="btn btn-icon btn-secondary" onclick="closeSettingsModal()" title="Close" data-i18n-title="settings.button.close" data-i18n-aria-label="aria.close">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -61,33 +61,33 @@
|
||||
<small id="target-editor-fps-rec" class="input-hint" style="display:none"></small>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="target-editor-brightness-threshold-group">
|
||||
<div class="label-row">
|
||||
<label for="target-editor-brightness-threshold">
|
||||
<span data-i18n="targets.min_brightness_threshold">Min Brightness Threshold:</span>
|
||||
<span id="target-editor-brightness-threshold-value">0</span>
|
||||
</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="targets.min_brightness_threshold.hint">Effective output brightness (pixel brightness × device/source brightness) below this value turns LEDs off completely (0 = disabled)</small>
|
||||
<input type="range" id="target-editor-brightness-threshold" min="0" max="254" value="0" oninput="document.getElementById('target-editor-brightness-threshold-value').textContent = this.value">
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="target-editor-adaptive-fps-group">
|
||||
<div class="label-row">
|
||||
<label for="target-editor-adaptive-fps" data-i18n="targets.adaptive_fps">Adaptive FPS:</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="targets.adaptive_fps.hint">Automatically reduce send rate when the device becomes unresponsive, and gradually recover when it stabilizes. Recommended for WiFi devices with weak signal.</small>
|
||||
<label class="settings-toggle">
|
||||
<input type="checkbox" id="target-editor-adaptive-fps">
|
||||
<span class="settings-toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<details class="form-collapse" id="target-editor-device-settings">
|
||||
<summary data-i18n="targets.section.specific_settings">Specific Settings</summary>
|
||||
<details class="form-collapse" id="target-editor-advanced-settings">
|
||||
<summary data-i18n="targets.section.advanced">Advanced</summary>
|
||||
<div class="form-collapse-body">
|
||||
<div class="form-group" id="target-editor-brightness-threshold-group">
|
||||
<div class="label-row">
|
||||
<label for="target-editor-brightness-threshold">
|
||||
<span data-i18n="targets.min_brightness_threshold">Min Brightness Threshold:</span>
|
||||
<span id="target-editor-brightness-threshold-value">0</span>
|
||||
</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="targets.min_brightness_threshold.hint">Effective output brightness (pixel brightness × device/source brightness) below this value turns LEDs off completely (0 = disabled)</small>
|
||||
<input type="range" id="target-editor-brightness-threshold" min="0" max="254" value="0" oninput="document.getElementById('target-editor-brightness-threshold-value').textContent = this.value">
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="target-editor-adaptive-fps-group">
|
||||
<div class="label-row">
|
||||
<label for="target-editor-adaptive-fps" data-i18n="targets.adaptive_fps">Adaptive FPS:</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="targets.adaptive_fps.hint">Automatically reduce send rate when the device becomes unresponsive, and gradually recover when it stabilizes. Recommended for WiFi devices with weak signal.</small>
|
||||
<label class="settings-toggle">
|
||||
<input type="checkbox" id="target-editor-adaptive-fps">
|
||||
<span class="settings-toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="target-editor-protocol-group">
|
||||
<div class="label-row">
|
||||
<label for="target-editor-protocol" data-i18n="targets.protocol">Protocol:</label>
|
||||
|
||||
@@ -66,6 +66,8 @@
|
||||
<option value="square" data-i18n="value_source.waveform.square">Square</option>
|
||||
<option value="sawtooth" data-i18n="value_source.waveform.sawtooth">Sawtooth</option>
|
||||
</select>
|
||||
<canvas id="value-source-waveform-preview" width="200" height="60"
|
||||
style="display:block;margin-top:8px;border-radius:6px;background:var(--surface-2,#1e1e2e);width:100%;max-width:200px;height:60px;"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
|
||||
Reference in New Issue
Block a user