Add type picker for entity creation, icon grid filter, and serial port placeholder
- Replace inline type selectors with pre-modal type picker grid for devices, color strip sources, and value sources - Add filterable search to icon grid when items > 9 (no auto-focus on touch) - Show disabled (grayed-out) filtered items instead of hiding them - Responsive grid columns (2-5 cols based on viewport width) - Add "Select a port..." placeholder to serial port dropdown - Update en/ru/zh locales with new keys Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -695,6 +695,80 @@ textarea:focus-visible {
|
|||||||
.icon-select-grid { gap: 4px; padding: 4px; }
|
.icon-select-grid { gap: 4px; padding: 4px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Type Picker Overlay ─────────────────────────────────── */
|
||||||
|
|
||||||
|
.type-picker-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 3000;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: 15vh;
|
||||||
|
background: rgba(0, 0, 0, 0);
|
||||||
|
backdrop-filter: blur(0px);
|
||||||
|
transition: background 0.2s, backdrop-filter 0.2s;
|
||||||
|
}
|
||||||
|
.type-picker-overlay.open {
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
|
.type-picker-dialog {
|
||||||
|
align-self: flex-start;
|
||||||
|
width: min(680px, 90vw);
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 16px 48px var(--shadow-color);
|
||||||
|
padding: 16px;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-12px) scale(0.98);
|
||||||
|
transition: opacity 0.2s, transform 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
.type-picker-overlay.open .type-picker-dialog {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
.type-picker-title {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.type-picker-filter {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
outline: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.type-picker-filter::placeholder {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
.type-picker-filter:focus {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
.type-picker-dialog .icon-select-grid {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
/* Override inline columns — use responsive auto-fill */
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)) !important;
|
||||||
|
}
|
||||||
|
.type-picker-dialog .icon-select-cell.disabled {
|
||||||
|
opacity: 0.25;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Entity Palette (command-palette style selector) ─────── */
|
/* ── Entity Palette (command-palette style selector) ─────── */
|
||||||
|
|
||||||
.entity-palette-overlay {
|
.entity-palette-overlay {
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
* Call sel.setValue(v) to change programmatically, sel.destroy() to remove.
|
* Call sel.setValue(v) to change programmatically, sel.destroy() to remove.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { desktopFocus } from './ui.js';
|
||||||
|
|
||||||
const POPUP_CLASS = 'icon-select-popup';
|
const POPUP_CLASS = 'icon-select-popup';
|
||||||
|
|
||||||
/** Close every open icon-select popup. */
|
/** Close every open icon-select popup. */
|
||||||
@@ -227,3 +229,79 @@ export class IconSelect {
|
|||||||
this._select.style.display = '';
|
this._select.style.display = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a standalone type-picker overlay.
|
||||||
|
*
|
||||||
|
* Displays a centered modal with an icon grid. When the user picks a type,
|
||||||
|
* the overlay closes and `onPick(value)` is called. Clicking the backdrop
|
||||||
|
* or pressing Escape dismisses without picking.
|
||||||
|
*
|
||||||
|
* @param {Object} opts
|
||||||
|
* @param {string} opts.title - heading text
|
||||||
|
* @param {Array<{value:string, icon:string, label:string, desc?:string}>} opts.items
|
||||||
|
* @param {Function} opts.onPick - called with the selected value
|
||||||
|
*/
|
||||||
|
export function showTypePicker({ title, items, onPick }) {
|
||||||
|
const showFilter = items.length > 9;
|
||||||
|
|
||||||
|
// Build cells
|
||||||
|
const cells = items.map(item =>
|
||||||
|
`<div class="icon-select-cell" data-value="${item.value}" data-search="${(item.label + ' ' + (item.desc || '')).toLowerCase()}">
|
||||||
|
<span class="icon-select-cell-icon">${item.icon}</span>
|
||||||
|
<span class="icon-select-cell-label">${item.label}</span>
|
||||||
|
${item.desc ? `<span class="icon-select-cell-desc">${item.desc}</span>` : ''}
|
||||||
|
</div>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
// Create overlay
|
||||||
|
const overlay = document.createElement('div');
|
||||||
|
overlay.className = 'type-picker-overlay';
|
||||||
|
overlay.innerHTML = `
|
||||||
|
<div class="type-picker-dialog">
|
||||||
|
<div class="type-picker-title">${title}</div>
|
||||||
|
${showFilter ? '<input class="type-picker-filter" type="text" placeholder="Filter…" autocomplete="off">' : ''}
|
||||||
|
<div class="icon-select-grid">${cells}</div>
|
||||||
|
</div>`;
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
|
const close = () => { overlay.remove(); document.removeEventListener('keydown', onKey); };
|
||||||
|
|
||||||
|
// Filter logic
|
||||||
|
if (showFilter) {
|
||||||
|
const input = overlay.querySelector('.type-picker-filter');
|
||||||
|
const allCells = overlay.querySelectorAll('.icon-select-cell');
|
||||||
|
input.addEventListener('input', () => {
|
||||||
|
const q = input.value.toLowerCase().trim();
|
||||||
|
allCells.forEach(cell => {
|
||||||
|
const match = !q || cell.dataset.search.includes(q);
|
||||||
|
cell.classList.toggle('disabled', !match);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// Auto-focus filter after animation (skip on touch devices to avoid keyboard popup)
|
||||||
|
requestAnimationFrame(() => setTimeout(() => desktopFocus(input), 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backdrop click
|
||||||
|
overlay.addEventListener('click', (e) => {
|
||||||
|
if (e.target === overlay) close();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Escape key
|
||||||
|
const onKey = (e) => {
|
||||||
|
if (e.key === 'Escape') close();
|
||||||
|
};
|
||||||
|
document.addEventListener('keydown', onKey);
|
||||||
|
|
||||||
|
// Cell clicks
|
||||||
|
overlay.querySelectorAll('.icon-select-cell').forEach(cell => {
|
||||||
|
cell.addEventListener('click', () => {
|
||||||
|
if (cell.classList.contains('disabled')) return;
|
||||||
|
close();
|
||||||
|
onPick(cell.dataset.value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Animate in
|
||||||
|
requestAnimationFrame(() => overlay.classList.add('open'));
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import * as P from '../core/icon-paths.js';
|
|||||||
import { wrapCard } from '../core/card-colors.js';
|
import { wrapCard } from '../core/card-colors.js';
|
||||||
import { TagInput, renderTagChips } from '../core/tag-input.js';
|
import { TagInput, renderTagChips } from '../core/tag-input.js';
|
||||||
import { attachProcessPicker } from '../core/process-picker.js';
|
import { attachProcessPicker } from '../core/process-picker.js';
|
||||||
import { IconSelect } from '../core/icon-select.js';
|
import { IconSelect, showTypePicker } from '../core/icon-select.js';
|
||||||
import { EntitySelect } from '../core/entity-palette.js';
|
import { EntitySelect } from '../core/entity-palette.js';
|
||||||
import {
|
import {
|
||||||
rgbArrayToHex, hexToRgbArray,
|
rgbArrayToHex, hexToRgbArray,
|
||||||
@@ -1315,7 +1315,16 @@ function _autoGenerateCSSName() {
|
|||||||
|
|
||||||
/* ── Editor open/close ────────────────────────────────────────── */
|
/* ── Editor open/close ────────────────────────────────────────── */
|
||||||
|
|
||||||
export async function showCSSEditor(cssId = null, cloneData = null) {
|
export async function showCSSEditor(cssId = null, cloneData = null, presetType = null) {
|
||||||
|
// When creating new: show type picker first, then re-enter with presetType
|
||||||
|
if (!cssId && !cloneData && !presetType) {
|
||||||
|
showTypePicker({
|
||||||
|
title: t('color_strip.select_type'),
|
||||||
|
items: _buildCSSTypeItems(),
|
||||||
|
onPick: (type) => showCSSEditor(null, null, type),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const sources = await streamsCache.fetch();
|
const sources = await streamsCache.fetch();
|
||||||
|
|
||||||
@@ -1449,8 +1458,8 @@ export async function showCSSEditor(cssId = null, cloneData = null) {
|
|||||||
// Initialize icon-grid type selector (idempotent)
|
// Initialize icon-grid type selector (idempotent)
|
||||||
_ensureCSSTypeIconSelect();
|
_ensureCSSTypeIconSelect();
|
||||||
|
|
||||||
// Hide type selector in edit mode (type is immutable)
|
// Hide type selector — type is chosen before the modal opens (or immutable in edit)
|
||||||
document.getElementById('css-editor-type-group').style.display = cssId ? 'none' : '';
|
document.getElementById('css-editor-type-group').style.display = 'none';
|
||||||
|
|
||||||
if (cssId) {
|
if (cssId) {
|
||||||
const cssSources = await colorStripSourcesCache.fetch();
|
const cssSources = await colorStripSourcesCache.fetch();
|
||||||
@@ -1481,7 +1490,7 @@ export async function showCSSEditor(cssId = null, cloneData = null) {
|
|||||||
} else {
|
} else {
|
||||||
document.getElementById('css-editor-id').value = '';
|
document.getElementById('css-editor-id').value = '';
|
||||||
document.getElementById('css-editor-name').value = '';
|
document.getElementById('css-editor-name').value = '';
|
||||||
document.getElementById('css-editor-type').value = 'picture';
|
document.getElementById('css-editor-type').value = presetType || 'picture';
|
||||||
onCSSTypeChange();
|
onCSSTypeChange();
|
||||||
document.getElementById('css-editor-interpolation').value = 'average';
|
document.getElementById('css-editor-interpolation').value = 'average';
|
||||||
if (_interpolationIconSelect) _interpolationIconSelect.setValue('average');
|
if (_interpolationIconSelect) _interpolationIconSelect.setValue('average');
|
||||||
@@ -1527,7 +1536,8 @@ export async function showCSSEditor(cssId = null, cloneData = null) {
|
|||||||
document.getElementById('css-editor-candlelight-num-candles').value = 3;
|
document.getElementById('css-editor-candlelight-num-candles').value = 3;
|
||||||
document.getElementById('css-editor-candlelight-speed').value = 1.0;
|
document.getElementById('css-editor-candlelight-speed').value = 1.0;
|
||||||
document.getElementById('css-editor-candlelight-speed-val').textContent = '1.0';
|
document.getElementById('css-editor-candlelight-speed-val').textContent = '1.0';
|
||||||
document.getElementById('css-editor-title').innerHTML = `${ICON_FILM} ${t('color_strip.add')}`;
|
const typeIcon = getColorStripIcon(presetType || 'picture');
|
||||||
|
document.getElementById('css-editor-title').innerHTML = `${typeIcon} ${t('color_strip.add')}: ${t(`color_strip.type.${presetType || 'picture'}`)}`;
|
||||||
document.getElementById('css-editor-gradient-preset').value = '';
|
document.getElementById('css-editor-gradient-preset').value = '';
|
||||||
gradientInit([
|
gradientInit([
|
||||||
{ position: 0.0, color: [255, 0, 0] },
|
{ position: 0.0, color: [255, 0, 0] },
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { showToast, desktopFocus } from '../core/ui.js';
|
|||||||
import { Modal } from '../core/modal.js';
|
import { Modal } from '../core/modal.js';
|
||||||
import { _computeMaxFps, _renderFpsHint } from './devices.js';
|
import { _computeMaxFps, _renderFpsHint } from './devices.js';
|
||||||
import { getDeviceTypeIcon, ICON_RADIO, ICON_GLOBE, ICON_CPU, ICON_KEYBOARD, ICON_MOUSE, ICON_HEADPHONES, ICON_PLUG, ICON_TARGET_ICON, ICON_ACTIVITY } from '../core/icons.js';
|
import { getDeviceTypeIcon, ICON_RADIO, ICON_GLOBE, ICON_CPU, ICON_KEYBOARD, ICON_MOUSE, ICON_HEADPHONES, ICON_PLUG, ICON_TARGET_ICON, ICON_ACTIVITY } from '../core/icons.js';
|
||||||
import { IconSelect } from '../core/icon-select.js';
|
import { IconSelect, showTypePicker } from '../core/icon-select.js';
|
||||||
|
|
||||||
class AddDeviceModal extends Modal {
|
class AddDeviceModal extends Modal {
|
||||||
constructor() { super('add-device-modal'); }
|
constructor() { super('add-device-modal'); }
|
||||||
@@ -262,6 +262,7 @@ export function onDeviceTypeChanged() {
|
|||||||
opt.value = '';
|
opt.value = '';
|
||||||
opt.textContent = t('device.serial_port.hint') || 'Click to discover ports...';
|
opt.textContent = t('device.serial_port.hint') || 'Click to discover ports...';
|
||||||
opt.disabled = true;
|
opt.disabled = true;
|
||||||
|
opt.selected = true;
|
||||||
serialSelect.appendChild(opt);
|
serialSelect.appendChild(opt);
|
||||||
}
|
}
|
||||||
updateBaudFpsHint();
|
updateBaudFpsHint();
|
||||||
@@ -328,6 +329,7 @@ export function onDeviceTypeChanged() {
|
|||||||
opt.value = '';
|
opt.value = '';
|
||||||
opt.textContent = t('device.serial_port.hint') || 'Click to discover ports...';
|
opt.textContent = t('device.serial_port.hint') || 'Click to discover ports...';
|
||||||
opt.disabled = true;
|
opt.disabled = true;
|
||||||
|
opt.selected = true;
|
||||||
serialSelect.appendChild(opt);
|
serialSelect.appendChild(opt);
|
||||||
}
|
}
|
||||||
} else if (isHueDevice(deviceType)) {
|
} else if (isHueDevice(deviceType)) {
|
||||||
@@ -525,6 +527,14 @@ function _populateSerialPortDropdown(devices) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Default placeholder
|
||||||
|
const placeholder = document.createElement('option');
|
||||||
|
placeholder.value = '';
|
||||||
|
placeholder.textContent = t('device.serial_port.select') || 'Select a port...';
|
||||||
|
placeholder.disabled = true;
|
||||||
|
placeholder.selected = true;
|
||||||
|
select.appendChild(placeholder);
|
||||||
|
|
||||||
devices.forEach(device => {
|
devices.forEach(device => {
|
||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = device.url;
|
opt.value = device.url;
|
||||||
@@ -544,7 +554,17 @@ export function onSerialPortFocus() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showAddDevice() {
|
export function showAddDevice(presetType = null) {
|
||||||
|
// When no type specified: show type picker first
|
||||||
|
if (!presetType) {
|
||||||
|
showTypePicker({
|
||||||
|
title: t('device.select_type'),
|
||||||
|
items: _buildDeviceTypeItems(),
|
||||||
|
onPick: (type) => showAddDevice(type),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const form = document.getElementById('add-device-form');
|
const form = document.getElementById('add-device-form');
|
||||||
const error = document.getElementById('add-device-error');
|
const error = document.getElementById('add-device-error');
|
||||||
form.reset();
|
form.reset();
|
||||||
@@ -563,6 +583,14 @@ export function showAddDevice() {
|
|||||||
const scanBtn = document.getElementById('scan-network-btn');
|
const scanBtn = document.getElementById('scan-network-btn');
|
||||||
if (scanBtn) scanBtn.disabled = false;
|
if (scanBtn) scanBtn.disabled = false;
|
||||||
_ensureDeviceTypeIconSelect();
|
_ensureDeviceTypeIconSelect();
|
||||||
|
|
||||||
|
// Pre-select type and hide the type selector (already chosen)
|
||||||
|
document.getElementById('device-type').value = presetType;
|
||||||
|
document.getElementById('device-type-group').style.display = 'none';
|
||||||
|
const typeIcon = getDeviceTypeIcon(presetType);
|
||||||
|
const typeName = t(`device.type.${presetType}`);
|
||||||
|
document.getElementById('add-device-modal-title').innerHTML = `${typeIcon} ${t('devices.add')}: ${typeName}`;
|
||||||
|
|
||||||
addDeviceModal.open();
|
addDeviceModal.open();
|
||||||
onDeviceTypeChanged();
|
onDeviceTypeChanged();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
} from '../core/icons.js';
|
} from '../core/icons.js';
|
||||||
import { wrapCard } from '../core/card-colors.js';
|
import { wrapCard } from '../core/card-colors.js';
|
||||||
import { TagInput, renderTagChips } from '../core/tag-input.js';
|
import { TagInput, renderTagChips } from '../core/tag-input.js';
|
||||||
import { IconSelect } from '../core/icon-select.js';
|
import { IconSelect, showTypePicker } from '../core/icon-select.js';
|
||||||
import { EntitySelect } from '../core/entity-palette.js';
|
import { EntitySelect } from '../core/entity-palette.js';
|
||||||
import { loadPictureSources } from './streams.js';
|
import { loadPictureSources } from './streams.js';
|
||||||
|
|
||||||
@@ -165,13 +165,27 @@ function _ensureVSTypeIconSelect() {
|
|||||||
|
|
||||||
// ── Modal ─────────────────────────────────────────────────────
|
// ── Modal ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
export async function showValueSourceModal(editData) {
|
export async function showValueSourceModal(editData, presetType = null) {
|
||||||
|
// When creating new: show type picker first, then re-enter with presetType
|
||||||
|
if (!editData && !presetType) {
|
||||||
|
showTypePicker({
|
||||||
|
title: t('value_source.select_type'),
|
||||||
|
items: _buildVSTypeItems(),
|
||||||
|
onPick: (type) => showValueSourceModal(null, type),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const hasId = editData?.id;
|
const hasId = editData?.id;
|
||||||
const isEdit = !!hasId;
|
const isEdit = !!hasId;
|
||||||
const titleKey = isEdit ? 'value_source.edit' : 'value_source.add';
|
|
||||||
|
|
||||||
const titleIcon = editData ? getValueSourceIcon(editData.source_type) : getValueSourceIcon('static');
|
const sourceType = editData?.source_type || presetType || 'static';
|
||||||
document.getElementById('value-source-modal-title').innerHTML = `${titleIcon} ${t(titleKey)}`;
|
const titleIcon = getValueSourceIcon(sourceType);
|
||||||
|
const titleKey = isEdit ? 'value_source.edit' : 'value_source.add';
|
||||||
|
const typeName = t(`value_source.type.${sourceType}`);
|
||||||
|
document.getElementById('value-source-modal-title').innerHTML = isEdit
|
||||||
|
? `${titleIcon} ${t(titleKey)}`
|
||||||
|
: `${titleIcon} ${t(titleKey)}: ${typeName}`;
|
||||||
document.getElementById('value-source-id').value = isEdit ? editData.id : '';
|
document.getElementById('value-source-id').value = isEdit ? editData.id : '';
|
||||||
document.getElementById('value-source-error').style.display = 'none';
|
document.getElementById('value-source-error').style.display = 'none';
|
||||||
|
|
||||||
@@ -180,7 +194,8 @@ export async function showValueSourceModal(editData) {
|
|||||||
|
|
||||||
_ensureVSTypeIconSelect();
|
_ensureVSTypeIconSelect();
|
||||||
const typeSelect = document.getElementById('value-source-type');
|
const typeSelect = document.getElementById('value-source-type');
|
||||||
document.getElementById('value-source-type-group').style.display = isEdit ? 'none' : '';
|
// Type is chosen before the modal opens — always hide selector
|
||||||
|
document.getElementById('value-source-type-group').style.display = 'none';
|
||||||
|
|
||||||
if (editData) {
|
if (editData) {
|
||||||
document.getElementById('value-source-name').value = editData.name || '';
|
document.getElementById('value-source-name').value = editData.name || '';
|
||||||
@@ -227,7 +242,7 @@ export async function showValueSourceModal(editData) {
|
|||||||
} else {
|
} else {
|
||||||
document.getElementById('value-source-name').value = '';
|
document.getElementById('value-source-name').value = '';
|
||||||
document.getElementById('value-source-description').value = '';
|
document.getElementById('value-source-description').value = '';
|
||||||
typeSelect.value = 'static';
|
typeSelect.value = presetType || 'static';
|
||||||
onValueSourceTypeChange();
|
onValueSourceTypeChange();
|
||||||
_setSlider('value-source-value', 1.0);
|
_setSlider('value-source-value', 1.0);
|
||||||
_setSlider('value-source-speed', 10);
|
_setSlider('value-source-speed', 10);
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
"templates.test.error.no_display": "Please select a display",
|
"templates.test.error.no_display": "Please select a display",
|
||||||
"templates.test.error.failed": "Test failed",
|
"templates.test.error.failed": "Test failed",
|
||||||
"devices.title": "Devices",
|
"devices.title": "Devices",
|
||||||
|
"device.select_type": "Select Device Type",
|
||||||
"devices.add": "Add New Device",
|
"devices.add": "Add New Device",
|
||||||
"devices.loading": "Loading devices...",
|
"devices.loading": "Loading devices...",
|
||||||
"devices.none": "No devices configured",
|
"devices.none": "No devices configured",
|
||||||
@@ -211,6 +212,7 @@
|
|||||||
"device.serial_port": "Serial Port:",
|
"device.serial_port": "Serial Port:",
|
||||||
"device.serial_port.hint": "Select the COM port of the Adalight device",
|
"device.serial_port.hint": "Select the COM port of the Adalight device",
|
||||||
"device.serial_port.none": "No serial ports found",
|
"device.serial_port.none": "No serial ports found",
|
||||||
|
"device.serial_port.select": "Select a port...",
|
||||||
"device.led_count_manual.hint": "Number of LEDs on the strip (must match your Arduino sketch)",
|
"device.led_count_manual.hint": "Number of LEDs on the strip (must match your Arduino sketch)",
|
||||||
"device.baud_rate": "Baud Rate:",
|
"device.baud_rate": "Baud Rate:",
|
||||||
"device.baud_rate.hint": "Serial communication speed. Higher = more FPS but requires matching Arduino sketch.",
|
"device.baud_rate.hint": "Serial communication speed. Higher = more FPS but requires matching Arduino sketch.",
|
||||||
@@ -827,6 +829,7 @@
|
|||||||
"aria.previous": "Previous",
|
"aria.previous": "Previous",
|
||||||
"aria.next": "Next",
|
"aria.next": "Next",
|
||||||
"aria.hint": "Show hint",
|
"aria.hint": "Show hint",
|
||||||
|
"color_strip.select_type": "Select Color Strip Type",
|
||||||
"color_strip.add": "Add Color Strip Source",
|
"color_strip.add": "Add Color Strip Source",
|
||||||
"color_strip.edit": "Edit Color Strip Source",
|
"color_strip.edit": "Edit Color Strip Source",
|
||||||
"color_strip.name": "Name:",
|
"color_strip.name": "Name:",
|
||||||
@@ -1179,6 +1182,7 @@
|
|||||||
"tree.group.picture": "Picture",
|
"tree.group.picture": "Picture",
|
||||||
"tree.group.utility": "Utility",
|
"tree.group.utility": "Utility",
|
||||||
"value_source.group.title": "Value Sources",
|
"value_source.group.title": "Value Sources",
|
||||||
|
"value_source.select_type": "Select Value Source Type",
|
||||||
"value_source.add": "Add Value Source",
|
"value_source.add": "Add Value Source",
|
||||||
"value_source.edit": "Edit Value Source",
|
"value_source.edit": "Edit Value Source",
|
||||||
"value_source.name": "Name:",
|
"value_source.name": "Name:",
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
"templates.test.error.no_display": "Пожалуйста, выберите дисплей",
|
"templates.test.error.no_display": "Пожалуйста, выберите дисплей",
|
||||||
"templates.test.error.failed": "Тест не удался",
|
"templates.test.error.failed": "Тест не удался",
|
||||||
"devices.title": "Устройства",
|
"devices.title": "Устройства",
|
||||||
|
"device.select_type": "Выберите тип устройства",
|
||||||
"devices.add": "Добавить Новое Устройство",
|
"devices.add": "Добавить Новое Устройство",
|
||||||
"devices.loading": "Загрузка устройств...",
|
"devices.loading": "Загрузка устройств...",
|
||||||
"devices.none": "Устройства не настроены",
|
"devices.none": "Устройства не настроены",
|
||||||
@@ -160,6 +161,7 @@
|
|||||||
"device.serial_port": "Серийный порт:",
|
"device.serial_port": "Серийный порт:",
|
||||||
"device.serial_port.hint": "Выберите COM порт устройства Adalight",
|
"device.serial_port.hint": "Выберите COM порт устройства Adalight",
|
||||||
"device.serial_port.none": "Серийные порты не найдены",
|
"device.serial_port.none": "Серийные порты не найдены",
|
||||||
|
"device.serial_port.select": "Выберите порт...",
|
||||||
"device.led_count_manual.hint": "Количество светодиодов на ленте (должно совпадать с вашим скетчем Arduino)",
|
"device.led_count_manual.hint": "Количество светодиодов на ленте (должно совпадать с вашим скетчем Arduino)",
|
||||||
"device.baud_rate": "Скорость порта:",
|
"device.baud_rate": "Скорость порта:",
|
||||||
"device.baud_rate.hint": "Скорость серийного соединения. Выше = больше FPS, но требует соответствия скетчу Arduino.",
|
"device.baud_rate.hint": "Скорость серийного соединения. Выше = больше FPS, но требует соответствия скетчу Arduino.",
|
||||||
@@ -776,6 +778,7 @@
|
|||||||
"aria.previous": "Назад",
|
"aria.previous": "Назад",
|
||||||
"aria.next": "Вперёд",
|
"aria.next": "Вперёд",
|
||||||
"aria.hint": "Показать подсказку",
|
"aria.hint": "Показать подсказку",
|
||||||
|
"color_strip.select_type": "Выберите тип цветовой полосы",
|
||||||
"color_strip.add": "Добавить источник цветовой полосы",
|
"color_strip.add": "Добавить источник цветовой полосы",
|
||||||
"color_strip.edit": "Редактировать источник цветовой полосы",
|
"color_strip.edit": "Редактировать источник цветовой полосы",
|
||||||
"color_strip.name": "Название:",
|
"color_strip.name": "Название:",
|
||||||
@@ -1128,6 +1131,7 @@
|
|||||||
"tree.group.picture": "Изображения",
|
"tree.group.picture": "Изображения",
|
||||||
"tree.group.utility": "Утилиты",
|
"tree.group.utility": "Утилиты",
|
||||||
"value_source.group.title": "Источники значений",
|
"value_source.group.title": "Источники значений",
|
||||||
|
"value_source.select_type": "Выберите тип источника значений",
|
||||||
"value_source.add": "Добавить источник значений",
|
"value_source.add": "Добавить источник значений",
|
||||||
"value_source.edit": "Редактировать источник значений",
|
"value_source.edit": "Редактировать источник значений",
|
||||||
"value_source.name": "Название:",
|
"value_source.name": "Название:",
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
"templates.test.error.no_display": "请选择显示器",
|
"templates.test.error.no_display": "请选择显示器",
|
||||||
"templates.test.error.failed": "测试失败",
|
"templates.test.error.failed": "测试失败",
|
||||||
"devices.title": "设备",
|
"devices.title": "设备",
|
||||||
|
"device.select_type": "选择设备类型",
|
||||||
"devices.add": "添加新设备",
|
"devices.add": "添加新设备",
|
||||||
"devices.loading": "正在加载设备...",
|
"devices.loading": "正在加载设备...",
|
||||||
"devices.none": "尚未配置设备",
|
"devices.none": "尚未配置设备",
|
||||||
@@ -160,6 +161,7 @@
|
|||||||
"device.serial_port": "串口:",
|
"device.serial_port": "串口:",
|
||||||
"device.serial_port.hint": "选择 Adalight 设备的 COM 端口",
|
"device.serial_port.hint": "选择 Adalight 设备的 COM 端口",
|
||||||
"device.serial_port.none": "未找到串口",
|
"device.serial_port.none": "未找到串口",
|
||||||
|
"device.serial_port.select": "选择端口...",
|
||||||
"device.led_count_manual.hint": "灯带上的 LED 数量(必须与 Arduino 程序匹配)",
|
"device.led_count_manual.hint": "灯带上的 LED 数量(必须与 Arduino 程序匹配)",
|
||||||
"device.baud_rate": "波特率:",
|
"device.baud_rate": "波特率:",
|
||||||
"device.baud_rate.hint": "串口通信速率。越高 FPS 越高,但需要与 Arduino 程序匹配。",
|
"device.baud_rate.hint": "串口通信速率。越高 FPS 越高,但需要与 Arduino 程序匹配。",
|
||||||
@@ -776,6 +778,7 @@
|
|||||||
"aria.previous": "上一个",
|
"aria.previous": "上一个",
|
||||||
"aria.next": "下一个",
|
"aria.next": "下一个",
|
||||||
"aria.hint": "显示提示",
|
"aria.hint": "显示提示",
|
||||||
|
"color_strip.select_type": "选择色带类型",
|
||||||
"color_strip.add": "添加色带源",
|
"color_strip.add": "添加色带源",
|
||||||
"color_strip.edit": "编辑色带源",
|
"color_strip.edit": "编辑色带源",
|
||||||
"color_strip.name": "名称:",
|
"color_strip.name": "名称:",
|
||||||
@@ -1128,6 +1131,7 @@
|
|||||||
"tree.group.picture": "图片",
|
"tree.group.picture": "图片",
|
||||||
"tree.group.utility": "工具",
|
"tree.group.utility": "工具",
|
||||||
"value_source.group.title": "值源",
|
"value_source.group.title": "值源",
|
||||||
|
"value_source.select_type": "选择值源类型",
|
||||||
"value_source.add": "添加值源",
|
"value_source.add": "添加值源",
|
||||||
"value_source.edit": "编辑值源",
|
"value_source.edit": "编辑值源",
|
||||||
"value_source.name": "名称:",
|
"value_source.name": "名称:",
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<hr class="modal-divider">
|
<hr class="modal-divider">
|
||||||
</div>
|
</div>
|
||||||
<form id="add-device-form">
|
<form id="add-device-form">
|
||||||
<div class="form-group">
|
<div class="form-group" id="device-type-group">
|
||||||
<div class="label-row">
|
<div class="label-row">
|
||||||
<label for="device-type" data-i18n="device.type">Device Type:</label>
|
<label for="device-type" data-i18n="device.type">Device Type:</label>
|
||||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user