Add 6 new device providers, IconSelect grids, and UI fixes
New device providers: ESP-NOW, Philips Hue, USB HID, SPI Direct, Razer Chroma SDK, and SteelSeries GameSense — each with client, provider, full backend registration, schemas, routes, and frontend support including discovery, form fields, and i18n. Add IconSelect grids for SPI LED chipset selector and GameSense peripheral type selector with new Lucide icons (cpu, keyboard, mouse, headphones). Replace emoji graph overlay buttons (eye, bell) with proper SVG path icons for consistent cross-platform rendering. Fix connection overlay causing horizontal scroll by adding overflow: hidden. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,13 +6,13 @@ import {
|
||||
_discoveryScanRunning, set_discoveryScanRunning,
|
||||
_discoveryCache, set_discoveryCache,
|
||||
} from '../core/state.js';
|
||||
import { API_BASE, fetchWithAuth, isSerialDevice, isMockDevice, isMqttDevice, isWsDevice, isOpenrgbDevice, isDmxDevice, escapeHtml } from '../core/api.js';
|
||||
import { API_BASE, fetchWithAuth, isSerialDevice, isMockDevice, isMqttDevice, isWsDevice, isOpenrgbDevice, isDmxDevice, isEspnowDevice, isHueDevice, isUsbhidDevice, isSpiDevice, isChromaDevice, isGameSenseDevice, escapeHtml } from '../core/api.js';
|
||||
import { devicesCache } from '../core/state.js';
|
||||
import { t } from '../core/i18n.js';
|
||||
import { showToast, desktopFocus } from '../core/ui.js';
|
||||
import { Modal } from '../core/modal.js';
|
||||
import { _computeMaxFps, _renderFpsHint } from './devices.js';
|
||||
import { getDeviceTypeIcon, ICON_RADIO, ICON_GLOBE } 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';
|
||||
|
||||
class AddDeviceModal extends Modal {
|
||||
@@ -41,7 +41,7 @@ const addDeviceModal = new AddDeviceModal();
|
||||
|
||||
/* ── Icon-grid type selector ──────────────────────────────────── */
|
||||
|
||||
const DEVICE_TYPE_KEYS = ['wled', 'adalight', 'ambiled', 'mqtt', 'ws', 'openrgb', 'dmx', 'mock'];
|
||||
const DEVICE_TYPE_KEYS = ['wled', 'adalight', 'ambiled', 'mqtt', 'ws', 'openrgb', 'dmx', 'espnow', 'hue', 'usbhid', 'spi', 'chroma', 'gamesense', 'mock'];
|
||||
|
||||
function _buildDeviceTypeItems() {
|
||||
return DEVICE_TYPE_KEYS.map(key => ({
|
||||
@@ -93,6 +93,76 @@ export function destroyDmxProtocolIconSelect(selectId) {
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Icon-grid SPI LED chipset selector ──────────────────────── */
|
||||
|
||||
function _buildSpiLedTypeItems() {
|
||||
return [
|
||||
{ value: 'WS2812B', icon: ICON_CPU, label: 'WS2812B', desc: t('device.spi.led_type.ws2812b.desc') },
|
||||
{ value: 'WS2812', icon: ICON_CPU, label: 'WS2812', desc: t('device.spi.led_type.ws2812.desc') },
|
||||
{ value: 'WS2811', icon: ICON_CPU, label: 'WS2811', desc: t('device.spi.led_type.ws2811.desc') },
|
||||
{ value: 'SK6812', icon: ICON_CPU, label: 'SK6812 (RGB)', desc: t('device.spi.led_type.sk6812.desc') },
|
||||
{ value: 'SK6812_RGBW', icon: ICON_CPU, label: 'SK6812 (RGBW)', desc: t('device.spi.led_type.sk6812_rgbw.desc') },
|
||||
];
|
||||
}
|
||||
|
||||
const _spiLedTypeIconSelects = {};
|
||||
|
||||
export function ensureSpiLedTypeIconSelect(selectId) {
|
||||
const sel = document.getElementById(selectId);
|
||||
if (!sel) return;
|
||||
if (_spiLedTypeIconSelects[selectId]) {
|
||||
_spiLedTypeIconSelects[selectId].updateItems(_buildSpiLedTypeItems());
|
||||
return;
|
||||
}
|
||||
_spiLedTypeIconSelects[selectId] = new IconSelect({
|
||||
target: sel,
|
||||
items: _buildSpiLedTypeItems(),
|
||||
columns: 3,
|
||||
});
|
||||
}
|
||||
|
||||
export function destroySpiLedTypeIconSelect(selectId) {
|
||||
if (_spiLedTypeIconSelects[selectId]) {
|
||||
_spiLedTypeIconSelects[selectId].destroy();
|
||||
delete _spiLedTypeIconSelects[selectId];
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Icon-grid GameSense peripheral type selector ────────────── */
|
||||
|
||||
function _buildGameSenseDeviceTypeItems() {
|
||||
return [
|
||||
{ value: 'keyboard', icon: ICON_KEYBOARD, label: t('device.gamesense.peripheral.keyboard'), desc: t('device.gamesense.peripheral.keyboard.desc') },
|
||||
{ value: 'mouse', icon: ICON_MOUSE, label: t('device.gamesense.peripheral.mouse'), desc: t('device.gamesense.peripheral.mouse.desc') },
|
||||
{ value: 'headset', icon: ICON_HEADPHONES, label: t('device.gamesense.peripheral.headset'), desc: t('device.gamesense.peripheral.headset.desc') },
|
||||
{ value: 'mousepad', icon: ICON_PLUG, label: t('device.gamesense.peripheral.mousepad'), desc: t('device.gamesense.peripheral.mousepad.desc') },
|
||||
{ value: 'indicator', icon: ICON_ACTIVITY, label: t('device.gamesense.peripheral.indicator'), desc: t('device.gamesense.peripheral.indicator.desc') },
|
||||
];
|
||||
}
|
||||
|
||||
const _gameSenseDeviceTypeIconSelects = {};
|
||||
|
||||
export function ensureGameSenseDeviceTypeIconSelect(selectId) {
|
||||
const sel = document.getElementById(selectId);
|
||||
if (!sel) return;
|
||||
if (_gameSenseDeviceTypeIconSelects[selectId]) {
|
||||
_gameSenseDeviceTypeIconSelects[selectId].updateItems(_buildGameSenseDeviceTypeItems());
|
||||
return;
|
||||
}
|
||||
_gameSenseDeviceTypeIconSelects[selectId] = new IconSelect({
|
||||
target: sel,
|
||||
items: _buildGameSenseDeviceTypeItems(),
|
||||
columns: 3,
|
||||
});
|
||||
}
|
||||
|
||||
export function destroyGameSenseDeviceTypeIconSelect(selectId) {
|
||||
if (_gameSenseDeviceTypeIconSelects[selectId]) {
|
||||
_gameSenseDeviceTypeIconSelects[selectId].destroy();
|
||||
delete _gameSenseDeviceTypeIconSelects[selectId];
|
||||
}
|
||||
}
|
||||
|
||||
export function onDeviceTypeChanged() {
|
||||
const deviceType = document.getElementById('device-type').value;
|
||||
if (_deviceTypeIconSelect) _deviceTypeIconSelect.setValue(deviceType);
|
||||
@@ -126,6 +196,13 @@ export function onDeviceTypeChanged() {
|
||||
if (dmxStartUniverseGroup) dmxStartUniverseGroup.style.display = 'none';
|
||||
if (dmxStartChannelGroup) dmxStartChannelGroup.style.display = 'none';
|
||||
|
||||
// Hide new device type fields by default
|
||||
_showEspnowFields(false);
|
||||
_showHueFields(false);
|
||||
_showSpiFields(false);
|
||||
_showChromaFields(false);
|
||||
_showGameSenseFields(false);
|
||||
|
||||
if (isMqttDevice(deviceType)) {
|
||||
// MQTT: show URL (topic), LED count; hide serial/baud/led-type/latency/discovery
|
||||
urlGroup.style.display = '';
|
||||
@@ -228,6 +305,126 @@ export function onDeviceTypeChanged() {
|
||||
} else {
|
||||
scanForDevices();
|
||||
}
|
||||
} else if (isEspnowDevice(deviceType)) {
|
||||
// ESP-NOW: serial port for gateway, LED count, baud rate, + ESP-NOW fields
|
||||
urlGroup.style.display = 'none';
|
||||
urlInput.removeAttribute('required');
|
||||
serialGroup.style.display = '';
|
||||
serialSelect.setAttribute('required', '');
|
||||
ledCountGroup.style.display = '';
|
||||
baudRateGroup.style.display = '';
|
||||
if (ledTypeGroup) ledTypeGroup.style.display = 'none';
|
||||
if (sendLatencyGroup) sendLatencyGroup.style.display = 'none';
|
||||
if (discoverySection) discoverySection.style.display = 'none';
|
||||
if (scanBtn) scanBtn.style.display = 'none';
|
||||
// Show ESP-NOW specific fields
|
||||
_showEspnowFields(true);
|
||||
// Populate serial ports
|
||||
if (deviceType in _discoveryCache) {
|
||||
_populateSerialPortDropdown(_discoveryCache[deviceType]);
|
||||
} else {
|
||||
serialSelect.innerHTML = '';
|
||||
const opt = document.createElement('option');
|
||||
opt.value = '';
|
||||
opt.textContent = t('device.serial_port.hint') || 'Click to discover ports...';
|
||||
opt.disabled = true;
|
||||
serialSelect.appendChild(opt);
|
||||
}
|
||||
} else if (isHueDevice(deviceType)) {
|
||||
// Hue: show URL (bridge IP), LED count, + Hue auth fields
|
||||
urlGroup.style.display = '';
|
||||
urlInput.setAttribute('required', '');
|
||||
serialGroup.style.display = 'none';
|
||||
serialSelect.removeAttribute('required');
|
||||
ledCountGroup.style.display = '';
|
||||
baudRateGroup.style.display = 'none';
|
||||
if (ledTypeGroup) ledTypeGroup.style.display = 'none';
|
||||
if (sendLatencyGroup) sendLatencyGroup.style.display = 'none';
|
||||
if (scanBtn) scanBtn.style.display = '';
|
||||
_showHueFields(true);
|
||||
if (urlLabel) urlLabel.textContent = t('device.hue.url') || 'Bridge IP';
|
||||
if (urlHint) urlHint.textContent = t('device.hue.url.hint') || 'IP address of your Hue bridge';
|
||||
urlInput.placeholder = 'hue://192.168.1.2';
|
||||
if (deviceType in _discoveryCache) {
|
||||
_renderDiscoveryList();
|
||||
} else {
|
||||
scanForDevices();
|
||||
}
|
||||
} else if (isUsbhidDevice(deviceType)) {
|
||||
// USB HID: show URL (VID:PID), LED count
|
||||
urlGroup.style.display = '';
|
||||
urlInput.setAttribute('required', '');
|
||||
serialGroup.style.display = 'none';
|
||||
serialSelect.removeAttribute('required');
|
||||
ledCountGroup.style.display = '';
|
||||
baudRateGroup.style.display = 'none';
|
||||
if (ledTypeGroup) ledTypeGroup.style.display = 'none';
|
||||
if (sendLatencyGroup) sendLatencyGroup.style.display = 'none';
|
||||
if (scanBtn) scanBtn.style.display = '';
|
||||
if (urlLabel) urlLabel.textContent = t('device.usbhid.url') || 'VID:PID';
|
||||
if (urlHint) urlHint.textContent = t('device.usbhid.url.hint') || 'USB Vendor:Product ID in hex';
|
||||
urlInput.placeholder = 'hid://1532:0084';
|
||||
if (deviceType in _discoveryCache) {
|
||||
_renderDiscoveryList();
|
||||
} else {
|
||||
scanForDevices();
|
||||
}
|
||||
} else if (isSpiDevice(deviceType)) {
|
||||
// SPI Direct: show URL (gpio/spidev), LED count, + SPI fields
|
||||
urlGroup.style.display = '';
|
||||
urlInput.setAttribute('required', '');
|
||||
serialGroup.style.display = 'none';
|
||||
serialSelect.removeAttribute('required');
|
||||
ledCountGroup.style.display = '';
|
||||
baudRateGroup.style.display = 'none';
|
||||
if (ledTypeGroup) ledTypeGroup.style.display = 'none';
|
||||
if (sendLatencyGroup) sendLatencyGroup.style.display = 'none';
|
||||
if (scanBtn) scanBtn.style.display = '';
|
||||
_showSpiFields(true);
|
||||
ensureSpiLedTypeIconSelect('device-spi-led-type');
|
||||
if (urlLabel) urlLabel.textContent = t('device.spi.url') || 'GPIO/SPI Path';
|
||||
if (urlHint) urlHint.textContent = t('device.spi.url.hint') || 'GPIO pin or SPI device path';
|
||||
urlInput.placeholder = 'spi://gpio:18';
|
||||
if (deviceType in _discoveryCache) {
|
||||
_renderDiscoveryList();
|
||||
} else {
|
||||
scanForDevices();
|
||||
}
|
||||
} else if (isChromaDevice(deviceType)) {
|
||||
// Razer Chroma: auto URL, LED count, + peripheral type selector
|
||||
urlGroup.style.display = 'none';
|
||||
urlInput.removeAttribute('required');
|
||||
serialGroup.style.display = 'none';
|
||||
serialSelect.removeAttribute('required');
|
||||
ledCountGroup.style.display = '';
|
||||
baudRateGroup.style.display = 'none';
|
||||
if (ledTypeGroup) ledTypeGroup.style.display = 'none';
|
||||
if (sendLatencyGroup) sendLatencyGroup.style.display = 'none';
|
||||
if (scanBtn) scanBtn.style.display = '';
|
||||
_showChromaFields(true);
|
||||
if (deviceType in _discoveryCache) {
|
||||
_renderDiscoveryList();
|
||||
} else {
|
||||
scanForDevices();
|
||||
}
|
||||
} else if (isGameSenseDevice(deviceType)) {
|
||||
// SteelSeries GameSense: auto URL, LED count, + device type selector
|
||||
urlGroup.style.display = 'none';
|
||||
urlInput.removeAttribute('required');
|
||||
serialGroup.style.display = 'none';
|
||||
serialSelect.removeAttribute('required');
|
||||
ledCountGroup.style.display = '';
|
||||
baudRateGroup.style.display = 'none';
|
||||
if (ledTypeGroup) ledTypeGroup.style.display = 'none';
|
||||
if (sendLatencyGroup) sendLatencyGroup.style.display = 'none';
|
||||
if (scanBtn) scanBtn.style.display = '';
|
||||
_showGameSenseFields(true);
|
||||
ensureGameSenseDeviceTypeIconSelect('device-gamesense-device-type');
|
||||
if (deviceType in _discoveryCache) {
|
||||
_renderDiscoveryList();
|
||||
} else {
|
||||
scanForDevices();
|
||||
}
|
||||
} else {
|
||||
urlGroup.style.display = '';
|
||||
urlInput.setAttribute('required', '');
|
||||
@@ -476,8 +673,13 @@ export async function handleAddDevice(event) {
|
||||
url = 'mock://';
|
||||
} else if (isWsDevice(deviceType)) {
|
||||
url = 'ws://';
|
||||
} else if (isSerialDevice(deviceType)) {
|
||||
} else if (isSerialDevice(deviceType) || isEspnowDevice(deviceType)) {
|
||||
url = document.getElementById('device-serial-port').value;
|
||||
} else if (isChromaDevice(deviceType)) {
|
||||
const chromaType = document.getElementById('device-chroma-device-type')?.value || 'chromalink';
|
||||
url = `chroma://${chromaType}`;
|
||||
} else if (isGameSenseDevice(deviceType)) {
|
||||
url = 'gamesense://auto';
|
||||
} else {
|
||||
url = document.getElementById('device-url').value.trim();
|
||||
}
|
||||
@@ -525,6 +727,26 @@ export async function handleAddDevice(event) {
|
||||
body.dmx_start_universe = parseInt(document.getElementById('device-dmx-start-universe')?.value || '0', 10);
|
||||
body.dmx_start_channel = parseInt(document.getElementById('device-dmx-start-channel')?.value || '1', 10);
|
||||
}
|
||||
if (isEspnowDevice(deviceType)) {
|
||||
body.espnow_peer_mac = document.getElementById('device-espnow-peer-mac')?.value || '';
|
||||
body.espnow_channel = parseInt(document.getElementById('device-espnow-channel')?.value || '1', 10);
|
||||
body.baud_rate = parseInt(document.getElementById('device-baud-rate')?.value || '921600', 10);
|
||||
}
|
||||
if (isHueDevice(deviceType)) {
|
||||
body.hue_username = document.getElementById('device-hue-username')?.value || '';
|
||||
body.hue_client_key = document.getElementById('device-hue-client-key')?.value || '';
|
||||
body.hue_entertainment_group_id = document.getElementById('device-hue-group-id')?.value || '';
|
||||
}
|
||||
if (isSpiDevice(deviceType)) {
|
||||
body.spi_speed_hz = parseInt(document.getElementById('device-spi-speed')?.value || '800000', 10);
|
||||
body.spi_led_type = document.getElementById('device-spi-led-type')?.value || 'WS2812B';
|
||||
}
|
||||
if (isChromaDevice(deviceType)) {
|
||||
body.chroma_device_type = document.getElementById('device-chroma-device-type')?.value || 'chromalink';
|
||||
}
|
||||
if (isGameSenseDevice(deviceType)) {
|
||||
body.gamesense_device_type = document.getElementById('device-gamesense-device-type')?.value || 'keyboard';
|
||||
}
|
||||
if (lastTemplateId) body.capture_template_id = lastTemplateId;
|
||||
|
||||
const response = await fetchWithAuth('/devices', {
|
||||
@@ -666,3 +888,39 @@ export function _getZoneMode(radioName = 'device-zone-mode') {
|
||||
const radio = document.querySelector(`input[name="${radioName}"]:checked`);
|
||||
return radio ? radio.value : 'combined';
|
||||
}
|
||||
|
||||
/* ── New device type field visibility helpers ──────────────────── */
|
||||
|
||||
function _showEspnowFields(show) {
|
||||
const ids = ['device-espnow-peer-mac-group', 'device-espnow-channel-group'];
|
||||
ids.forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.style.display = show ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function _showHueFields(show) {
|
||||
const ids = ['device-hue-username-group', 'device-hue-client-key-group', 'device-hue-group-id-group'];
|
||||
ids.forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.style.display = show ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function _showSpiFields(show) {
|
||||
const ids = ['device-spi-speed-group', 'device-spi-led-type-group'];
|
||||
ids.forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.style.display = show ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function _showChromaFields(show) {
|
||||
const el = document.getElementById('device-chroma-device-type-group');
|
||||
if (el) el.style.display = show ? '' : 'none';
|
||||
}
|
||||
|
||||
function _showGameSenseFields(show) {
|
||||
const el = document.getElementById('device-gamesense-device-type-group');
|
||||
if (el) el.style.display = show ? '' : 'none';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user