Improve property description hints for dialogs

This commit is contained in:
2026-02-12 00:31:44 +03:00
parent ebec1bd16e
commit d229c9a0d5
5 changed files with 136 additions and 26 deletions

View File

@@ -2,6 +2,16 @@ const API_BASE = '/api/v1';
let refreshInterval = null; let refreshInterval = null;
let apiKey = null; let apiKey = null;
// Toggle hint description visibility next to a label
function toggleHint(btn) {
const hint = btn.closest('.label-row').nextElementSibling;
if (hint && hint.classList.contains('input-hint')) {
const visible = hint.style.display !== 'none';
hint.style.display = visible ? 'none' : 'block';
btn.classList.toggle('active', !visible);
}
}
// Backdrop click helper: only closes modal if both mousedown and mouseup were on the backdrop itself. // Backdrop click helper: only closes modal if both mousedown and mouseup were on the backdrop itself.
// Prevents accidental close when user drags text selection outside the dialog. // Prevents accidental close when user drags text selection outside the dialog.
function setupBackdropClose(modal, closeFn) { function setupBackdropClose(modal, closeFn) {

View File

@@ -203,15 +203,21 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="label-row">
<label for="settings-device-url" data-i18n="device.url">WLED URL:</label> <label for="settings-device-url" data-i18n="device.url">WLED URL:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="settings.url.hint">IP address or hostname of your WLED device</small>
<input type="url" id="settings-device-url" data-i18n-placeholder="device.url.placeholder" placeholder="http://192.168.1.100" required> <input type="url" id="settings-device-url" data-i18n-placeholder="device.url.placeholder" placeholder="http://192.168.1.100" required>
<small class="input-hint" data-i18n="settings.url.hint">IP address or hostname of your WLED device</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="label-row">
<label for="settings-health-interval" data-i18n="settings.health_interval">Health Check Interval (s):</label> <label for="settings-health-interval" data-i18n="settings.health_interval">Health Check Interval (s):</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="settings.health_interval.hint">How often to check the WLED device status (5-600 seconds)</small>
<input type="number" id="settings-health-interval" min="5" max="600" value="30"> <input type="number" id="settings-health-interval" min="5" max="600" value="30">
<small class="input-hint" data-i18n="settings.health_interval.hint">How often to check the WLED device status (5-600 seconds)</small>
</div> </div>
<div id="settings-error" class="error-message" style="display: none;"></div> <div id="settings-error" class="error-message" style="display: none;"></div>
@@ -236,35 +242,47 @@
<input type="hidden" id="stream-selector-device-id"> <input type="hidden" id="stream-selector-device-id">
<div class="form-group"> <div class="form-group">
<div class="label-row">
<label for="stream-selector-stream" data-i18n="device.stream_selector.label">Stream:</label> <label for="stream-selector-stream" data-i18n="device.stream_selector.label">Stream:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="device.stream_selector.hint">Select a stream that defines what this device captures and processes</small>
<select id="stream-selector-stream"></select> <select id="stream-selector-stream"></select>
<div id="stream-selector-info" class="stream-info-panel" style="display: none;"></div> <div id="stream-selector-info" class="stream-info-panel" style="display: none;"></div>
<small class="input-hint" data-i18n="device.stream_selector.hint">Select a stream that defines what this device captures and processes</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="label-row">
<label for="stream-selector-border-width" data-i18n="device.stream_settings.border_width">Border Width (px):</label> <label for="stream-selector-border-width" data-i18n="device.stream_settings.border_width">Border Width (px):</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="device.stream_settings.border_width_hint">How many pixels from the screen edge to sample for LED colors (1-100)</small>
<input type="number" id="stream-selector-border-width" min="1" max="100" value="10"> <input type="number" id="stream-selector-border-width" min="1" max="100" value="10">
<small class="input-hint" data-i18n="device.stream_settings.border_width_hint">How many pixels from the screen edge to sample for LED colors (1-100)</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="label-row">
<label for="stream-selector-interpolation" data-i18n="device.stream_settings.interpolation">Interpolation Mode:</label> <label for="stream-selector-interpolation" data-i18n="device.stream_settings.interpolation">Interpolation Mode:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="device.stream_settings.interpolation_hint">How to calculate LED color from sampled pixels</small>
<select id="stream-selector-interpolation"> <select id="stream-selector-interpolation">
<option value="average" data-i18n="device.stream_settings.interpolation.average">Average</option> <option value="average" data-i18n="device.stream_settings.interpolation.average">Average</option>
<option value="median" data-i18n="device.stream_settings.interpolation.median">Median</option> <option value="median" data-i18n="device.stream_settings.interpolation.median">Median</option>
<option value="dominant" data-i18n="device.stream_settings.interpolation.dominant">Dominant</option> <option value="dominant" data-i18n="device.stream_settings.interpolation.dominant">Dominant</option>
</select> </select>
<small class="input-hint" data-i18n="device.stream_settings.interpolation_hint">How to calculate LED color from sampled pixels</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="label-row">
<label for="stream-selector-smoothing"> <label for="stream-selector-smoothing">
<span data-i18n="device.stream_settings.smoothing">Smoothing:</span> <span data-i18n="device.stream_settings.smoothing">Smoothing:</span>
<span id="stream-selector-smoothing-value">0.3</span> <span id="stream-selector-smoothing-value">0.3</span>
</label> </label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="device.stream_settings.smoothing_hint">Temporal blending between frames (0=none, 1=full). Reduces flicker.</small>
<input type="range" id="stream-selector-smoothing" min="0.0" max="1.0" step="0.05" value="0.3" oninput="document.getElementById('stream-selector-smoothing-value').textContent = this.value"> <input type="range" id="stream-selector-smoothing" min="0.0" max="1.0" step="0.05" value="0.3" oninput="document.getElementById('stream-selector-smoothing-value').textContent = this.value">
<small class="input-hint" data-i18n="device.stream_settings.smoothing_hint">Temporal blending between frames (0=none, 1=full). Reduces flicker.</small>
</div> </div>
<div id="stream-selector-error" class="error-message" style="display: none;"></div> <div id="stream-selector-error" class="error-message" style="display: none;"></div>
@@ -290,7 +308,11 @@
Please enter your API key to authenticate and access the WLED Grab. Please enter your API key to authenticate and access the WLED Grab.
</p> </p>
<div class="form-group"> <div class="form-group">
<div class="label-row">
<label for="api-key-input" data-i18n="auth.label">API Key:</label> <label for="api-key-input" data-i18n="auth.label">API Key:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="auth.hint">Your API key will be stored securely in your browser's local storage.</small>
<div class="password-input-wrapper"> <div class="password-input-wrapper">
<input <input
type="password" type="password"
@@ -303,7 +325,6 @@
👁️ 👁️
</button> </button>
</div> </div>
<small class="input-hint" data-i18n="auth.hint">Your API key will be stored securely in your browser's local storage.</small>
</div> </div>
<div id="api-key-error" class="error-message" style="display: none;"></div> <div id="api-key-error" class="error-message" style="display: none;"></div>
</div> </div>
@@ -380,7 +401,11 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="label-row">
<label for="template-engine" data-i18n="templates.engine">Capture Engine:</label> <label for="template-engine" data-i18n="templates.engine">Capture Engine:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="templates.engine.hint">Select the screen capture technology to use</small>
<select id="template-engine" onchange="onEngineChange()" required> <select id="template-engine" onchange="onEngineChange()" required>
<option value="" data-i18n="templates.engine.select">Select an engine...</option> <option value="" data-i18n="templates.engine.select">Select an engine...</option>
</select> </select>
@@ -506,18 +531,30 @@
<!-- Raw stream fields --> <!-- Raw stream fields -->
<div id="stream-raw-fields"> <div id="stream-raw-fields">
<div class="form-group"> <div class="form-group">
<div class="label-row">
<label data-i18n="streams.display">Display:</label> <label data-i18n="streams.display">Display:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="streams.display.hint">Which screen to capture</small>
<input type="hidden" id="stream-display-index" value=""> <input type="hidden" id="stream-display-index" value="">
<button type="button" class="btn btn-display-picker" id="stream-display-picker-btn" onclick="openDisplayPicker(onStreamDisplaySelected, document.getElementById('stream-display-index').value)"> <button type="button" class="btn btn-display-picker" id="stream-display-picker-btn" onclick="openDisplayPicker(onStreamDisplaySelected, document.getElementById('stream-display-index').value)">
<span id="stream-display-picker-label" data-i18n="displays.picker.select">Select display...</span> <span id="stream-display-picker-label" data-i18n="displays.picker.select">Select display...</span>
</button> </button>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="label-row">
<label for="stream-capture-template" data-i18n="streams.capture_template">Capture Template:</label> <label for="stream-capture-template" data-i18n="streams.capture_template">Capture Template:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="streams.capture_template.hint">Engine template defining how the screen is captured</small>
<select id="stream-capture-template"></select> <select id="stream-capture-template"></select>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="label-row">
<label for="stream-target-fps" data-i18n="streams.target_fps">Target FPS:</label> <label for="stream-target-fps" data-i18n="streams.target_fps">Target FPS:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="streams.target_fps.hint">Target frames per second for capture (10-90)</small>
<div class="slider-row"> <div class="slider-row">
<input type="range" id="stream-target-fps" min="10" max="90" value="30" oninput="document.getElementById('stream-target-fps-value').textContent = this.value"> <input type="range" id="stream-target-fps" min="10" max="90" value="30" oninput="document.getElementById('stream-target-fps-value').textContent = this.value">
<span id="stream-target-fps-value" class="slider-value">30</span> <span id="stream-target-fps-value" class="slider-value">30</span>
@@ -528,11 +565,19 @@
<!-- Processed stream fields --> <!-- Processed stream fields -->
<div id="stream-processed-fields" style="display: none;"> <div id="stream-processed-fields" style="display: none;">
<div class="form-group"> <div class="form-group">
<div class="label-row">
<label for="stream-source" data-i18n="streams.source">Source Stream:</label> <label for="stream-source" data-i18n="streams.source">Source Stream:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="streams.source.hint">The stream to apply processing filters to</small>
<select id="stream-source"></select> <select id="stream-source"></select>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="label-row">
<label for="stream-pp-template" data-i18n="streams.pp_template">Processing Template:</label> <label for="stream-pp-template" data-i18n="streams.pp_template">Processing Template:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="streams.pp_template.hint">Filter template to apply to the source stream</small>
<select id="stream-pp-template"></select> <select id="stream-pp-template"></select>
</div> </div>
</div> </div>
@@ -540,9 +585,12 @@
<!-- Static image fields --> <!-- Static image fields -->
<div id="stream-static-image-fields" style="display: none;"> <div id="stream-static-image-fields" style="display: none;">
<div class="form-group"> <div class="form-group">
<div class="label-row">
<label for="stream-image-source" data-i18n="streams.image_source">Image Source:</label> <label for="stream-image-source" data-i18n="streams.image_source">Image Source:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="streams.image_source.hint">Enter a URL (http/https) or local file path to an image</small>
<input type="text" id="stream-image-source" data-i18n-placeholder="streams.image_source.placeholder" placeholder="https://example.com/image.jpg or C:\path\to\image.png"> <input type="text" id="stream-image-source" data-i18n-placeholder="streams.image_source.placeholder" placeholder="https://example.com/image.jpg or C:\path\to\image.png">
<small class="form-hint" data-i18n="streams.image_source.hint">Enter a URL (http/https) or local file path to an image</small>
</div> </div>
<div id="stream-image-preview-container" class="image-preview-container" style="display: none;"> <div id="stream-image-preview-container" class="image-preview-container" style="display: none;">
<img id="stream-image-preview" class="stream-image-preview" src="" alt="Preview"> <img id="stream-image-preview" class="stream-image-preview" src="" alt="Preview">

View File

@@ -46,6 +46,7 @@
"templates.description.label": "Description (optional):", "templates.description.label": "Description (optional):",
"templates.description.placeholder": "Describe this template...", "templates.description.placeholder": "Describe this template...",
"templates.engine": "Capture Engine:", "templates.engine": "Capture Engine:",
"templates.engine.hint": "Select the screen capture technology to use",
"templates.engine.select": "Select an engine...", "templates.engine.select": "Select an engine...",
"templates.engine.unavailable": "Unavailable", "templates.engine.unavailable": "Unavailable",
"templates.engine.unavailable.hint": "This engine is not available on your system", "templates.engine.unavailable.hint": "This engine is not available on your system",
@@ -215,10 +216,15 @@
"streams.type.raw": "Screen Capture", "streams.type.raw": "Screen Capture",
"streams.type.processed": "Processed", "streams.type.processed": "Processed",
"streams.display": "Display:", "streams.display": "Display:",
"streams.display.hint": "Which screen to capture",
"streams.capture_template": "Engine Template:", "streams.capture_template": "Engine Template:",
"streams.capture_template.hint": "Engine template defining how the screen is captured",
"streams.target_fps": "Target FPS:", "streams.target_fps": "Target FPS:",
"streams.target_fps.hint": "Target frames per second for capture (10-90)",
"streams.source": "Source Stream:", "streams.source": "Source Stream:",
"streams.source.hint": "The stream to apply processing filters to",
"streams.pp_template": "Filter Template:", "streams.pp_template": "Filter Template:",
"streams.pp_template.hint": "Filter template to apply to the source stream",
"streams.description_label": "Description (optional):", "streams.description_label": "Description (optional):",
"streams.description_placeholder": "Describe this stream...", "streams.description_placeholder": "Describe this stream...",
"streams.created": "Stream created successfully", "streams.created": "Stream created successfully",

View File

@@ -46,6 +46,7 @@
"templates.description.label": "Описание (необязательно):", "templates.description.label": "Описание (необязательно):",
"templates.description.placeholder": "Опишите этот шаблон...", "templates.description.placeholder": "Опишите этот шаблон...",
"templates.engine": "Движок Захвата:", "templates.engine": "Движок Захвата:",
"templates.engine.hint": "Выберите технологию захвата экрана",
"templates.engine.select": "Выберите движок...", "templates.engine.select": "Выберите движок...",
"templates.engine.unavailable": "Недоступен", "templates.engine.unavailable": "Недоступен",
"templates.engine.unavailable.hint": "Этот движок недоступен в вашей системе", "templates.engine.unavailable.hint": "Этот движок недоступен в вашей системе",
@@ -215,10 +216,15 @@
"streams.type.raw": "Захват экрана", "streams.type.raw": "Захват экрана",
"streams.type.processed": "Обработанный", "streams.type.processed": "Обработанный",
"streams.display": "Дисплей:", "streams.display": "Дисплей:",
"streams.display.hint": "Какой экран захватывать",
"streams.capture_template": "Шаблон Движка:", "streams.capture_template": "Шаблон Движка:",
"streams.capture_template.hint": "Шаблон движка, определяющий способ захвата экрана",
"streams.target_fps": "Целевой FPS:", "streams.target_fps": "Целевой FPS:",
"streams.target_fps.hint": "Целевое количество кадров в секунду (10-90)",
"streams.source": "Исходный Поток:", "streams.source": "Исходный Поток:",
"streams.source.hint": "Поток, к которому применяются фильтры обработки",
"streams.pp_template": "Шаблон Фильтра:", "streams.pp_template": "Шаблон Фильтра:",
"streams.pp_template.hint": "Шаблон фильтра для применения к исходному потоку",
"streams.description_label": "Описание (необязательно):", "streams.description_label": "Описание (необязательно):",
"streams.description_placeholder": "Опишите этот поток...", "streams.description_placeholder": "Опишите этот поток...",
"streams.created": "Поток успешно создан", "streams.created": "Поток успешно создан",

View File

@@ -966,9 +966,49 @@ input:-webkit-autofill:focus {
opacity: 1; opacity: 1;
} }
.label-row {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 5px;
}
.label-row label {
margin-bottom: 0;
}
.hint-toggle {
background: none;
border: 1px solid var(--border-color);
border-radius: 50%;
width: 18px;
height: 18px;
font-size: 0.7rem;
line-height: 1;
color: var(--text-secondary, #888);
cursor: pointer;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
opacity: 0.6;
transition: opacity 0.2s;
flex-shrink: 0;
}
.hint-toggle:hover {
opacity: 1;
}
.hint-toggle.active {
opacity: 1;
color: var(--primary-color, #4CAF50);
border-color: var(--primary-color, #4CAF50);
}
.input-hint { .input-hint {
display: block; display: block;
margin-top: 8px; margin: 0 0 6px 0;
color: #666; color: #666;
font-size: 0.85rem; font-size: 0.85rem;
} }