This commit is contained in:
@@ -134,6 +134,62 @@
|
||||
.icon-btn:hover { border-color: var(--accent); color: var(--accent); }
|
||||
.icon-btn.del:hover { border-color: var(--danger); color: var(--danger); }
|
||||
|
||||
/* ── Brightness Slider ── */
|
||||
.brightness-control {
|
||||
background: var(--bg);
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.brightness-slider {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
background: linear-gradient(to right, var(--off) 0%, var(--on) 100%);
|
||||
outline: none;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.brightness-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
cursor: pointer;
|
||||
border: 2px solid var(--card);
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||
transition: all .15s;
|
||||
}
|
||||
.brightness-slider::-webkit-slider-thumb:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 3px 6px rgba(0,0,0,0.3);
|
||||
}
|
||||
.brightness-slider::-moz-range-thumb {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
cursor: pointer;
|
||||
border: 2px solid var(--card);
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||
transition: all .15s;
|
||||
}
|
||||
.brightness-slider::-moz-range-thumb:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 3px 6px rgba(0,0,0,0.3);
|
||||
}
|
||||
.brightness-slider:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.brightness-value {
|
||||
font-weight: 600;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* ── Modal ── */
|
||||
.modal-backdrop { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.55);
|
||||
z-index: 200; align-items: flex-end; justify-content: center; }
|
||||
@@ -196,8 +252,9 @@
|
||||
<div id="page-devices" class="page active">
|
||||
<div class="toolbar">
|
||||
<h2>Devices</h2>
|
||||
<div style="flex:1"></div>
|
||||
<button class="btn btn-ghost btn-sm" onclick="openAddDeviceModal()">+ Add IP</button>
|
||||
<button class="btn btn-primary" id="btn-discover" onclick="discoverDevices()">⟳ Scan</button>
|
||||
<button class="btn btn-secondary btn-sm" id="btn-manual-add" onclick="openManualModal()" title="Add device manually" style="width:auto;padding:9px 12px;">+</button>
|
||||
</div>
|
||||
<div id="page-header-devices" class="page-header">
|
||||
<span class="ws-dot" id="ws-dot-d"></span>
|
||||
@@ -258,7 +315,10 @@
|
||||
<table style="font-size:12px;color:var(--text2);width:100%;border-collapse:collapse;line-height:1.8;">
|
||||
<tr><td style="padding-right:8px;white-space:nowrap;color:var(--accent);font-family:monospace;">GET /api/devices</td><td>List devices</td></tr>
|
||||
<tr><td style="padding-right:8px;white-space:nowrap;color:var(--accent);font-family:monospace;">POST /api/devices/discover</td><td>Scan network</td></tr>
|
||||
<tr><td style="padding-right:8px;white-space:nowrap;color:var(--accent);font-family:monospace;">POST /api/devices/add</td><td>Add device manually <code style="background:var(--bg);padding:1px 4px;border-radius:3px;">{"host":"192.168.1.100","port":49153}</code></td></tr>
|
||||
<tr><td style="padding-right:8px;white-space:nowrap;color:var(--accent);font-family:monospace;">POST /api/devices/:ip/:port/state</td><td>Toggle power <code style="background:var(--bg);padding:1px 4px;border-radius:3px;">{"on":true}</code></td></tr>
|
||||
<tr><td style="padding-right:8px;white-space:nowrap;color:var(--accent);font-family:monospace;">GET /api/devices/:ip/:port/brightness</td><td>Get brightness (dimmer only)</td></tr>
|
||||
<tr><td style="padding-right:8px;white-space:nowrap;color:var(--accent);font-family:monospace;">POST /api/devices/:ip/:port/brightness</td><td>Set brightness <code style="background:var(--bg);padding:1px 4px;border-radius:3px;">{"brightness":50}</code></td></tr>
|
||||
<tr><td style="padding-right:8px;white-space:nowrap;color:var(--accent);font-family:monospace;">GET /api/dwm-rules</td><td>List DWM rules</td></tr>
|
||||
<tr><td style="padding-right:8px;white-space:nowrap;color:var(--accent);font-family:monospace;">POST /api/dwm-rules</td><td>Create rule</td></tr>
|
||||
<tr><td style="padding-right:8px;white-space:nowrap;color:var(--accent);font-family:monospace;">PUT /api/dwm-rules/:id</td><td>Update rule</td></tr>
|
||||
@@ -437,25 +497,30 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Manual Add Modal ── -->
|
||||
<div class="modal-backdrop" id="modal-manual" onclick="closeManualModal(event)">
|
||||
<!-- ── Add Device Modal ── -->
|
||||
<div class="modal-backdrop" id="modal-add-device" onclick="closeAddDeviceModal(event)">
|
||||
<div class="modal" onclick="event.stopPropagation()">
|
||||
<div class="modal-header">
|
||||
<span class="modal-title">Add Device Manually</span>
|
||||
<button class="modal-close" onclick="closeManualModal()">✕</button>
|
||||
<span class="modal-title">Add Wemo Device Manually</span>
|
||||
<button class="modal-close" onclick="closeAddDeviceModal()">×</button>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">IP Address</label>
|
||||
<input class="form-input" id="f-manual-host" type="text" placeholder="192.168.1.100" autofocus />
|
||||
<input class="form-input" id="add-device-host" type="text" placeholder="192.168.1.100" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Port</label>
|
||||
<input class="form-input" id="f-manual-port" type="number" placeholder="49153" value="49153" />
|
||||
<label class="form-label">Port (optional)</label>
|
||||
<input class="form-input" id="add-device-port" type="number" placeholder="49153" min="1" max="65535" />
|
||||
<div style="font-size:12px;color:var(--text2);margin-top:4px;">Default: 49153 (try 49152-49156 if needed)</div>
|
||||
</div>
|
||||
<div id="modal-manual-error" style="display:none;color:var(--danger);font-size:13px;margin-bottom:10px;"></div>
|
||||
|
||||
<div id="add-device-error" style="display:none;color:var(--danger);font-size:13px;margin-bottom:10px;"></div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-ghost" onclick="closeManualModal()">Cancel</button>
|
||||
<button class="btn btn-primary" id="btn-add-manual" onclick="addManualDevice()">Add</button>
|
||||
<button class="btn btn-ghost" onclick="closeAddDeviceModal()">Cancel</button>
|
||||
<button class="btn btn-primary" id="btn-add-device" onclick="addDeviceManually()">Add Device</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -585,14 +650,29 @@ function renderDevices() {
|
||||
}
|
||||
el.innerHTML = devices.map((d, i) => {
|
||||
const name = d.friendlyName || d.name || d.host;
|
||||
const isDimmer = d.isDimmer || false;
|
||||
const icon = isDimmer ? '🔆' : '💡';
|
||||
return `
|
||||
<div class="card" id="dev-${i}">
|
||||
<div style="font-size:24px;flex-shrink:0">💡</div>
|
||||
<div style="font-size:24px;flex-shrink:0">${icon}</div>
|
||||
<div class="card-body">
|
||||
<div class="card-name">${esc(name)}</div>
|
||||
<div class="card-meta">${esc(d.host)}:${d.port}
|
||||
${d.productModel ? ' · ' + esc(d.productModel) : ''}
|
||||
${isDimmer ? ' · Dimmer' : ''}
|
||||
</div>
|
||||
${isDimmer ? `
|
||||
<div class="brightness-control" style="margin-top:8px;">
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<span style="font-size:12px;color:var(--text2);">🔅</span>
|
||||
<input type="range" class="brightness-slider" id="bright-${i}"
|
||||
min="0" max="100" value="50"
|
||||
oninput="updateBrightnessPreview(${i}, this.value)"
|
||||
onchange="setBrightness(${i}, this.value)">
|
||||
<span class="brightness-value" id="bright-val-${i}" style="font-size:12px;color:var(--text2);min-width:30px;">50%</span>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
<label class="toggle" id="dtog-${i}" onclick="toggleDevice(${i},event)">
|
||||
<input type="checkbox" id="dchk-${i}">
|
||||
@@ -601,11 +681,29 @@ function renderDevices() {
|
||||
</label>
|
||||
</div>`;
|
||||
}).join('');
|
||||
// fetch current state for each
|
||||
|
||||
// fetch current state for each device
|
||||
devices.forEach((d, i) => {
|
||||
api('GET', `/api/devices/${d.host}/${d.port}/state`)
|
||||
.then((on) => { const c = document.getElementById('dchk-'+i); if (c) c.checked = !!on; })
|
||||
.then((on) => {
|
||||
const c = document.getElementById('dchk-'+i);
|
||||
if (c) c.checked = !!on;
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
// fetch brightness for dimmer devices
|
||||
if (d.isDimmer) {
|
||||
api('GET', `/api/devices/${d.host}/${d.port}/brightness`)
|
||||
.then((data) => {
|
||||
const slider = document.getElementById('bright-'+i);
|
||||
const value = document.getElementById('bright-val-'+i);
|
||||
if (slider && value && data.brightness !== undefined) {
|
||||
slider.value = data.brightness;
|
||||
value.textContent = Math.round(data.brightness) + '%';
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -627,64 +725,34 @@ async function toggleDevice(i, e) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Manual Add ─────────────────────────────────────────────────────────────
|
||||
function openManualModal() {
|
||||
document.getElementById('f-manual-host').value = '';
|
||||
document.getElementById('f-manual-port').value = '49153';
|
||||
document.getElementById('modal-manual-error').style.display = 'none';
|
||||
document.getElementById('modal-manual').classList.add('open');
|
||||
setTimeout(() => document.getElementById('f-manual-host').focus(), 100);
|
||||
}
|
||||
|
||||
function closeManualModal(e) {
|
||||
if (e && e.target !== document.getElementById('modal-manual')) return;
|
||||
document.getElementById('modal-manual').classList.remove('open');
|
||||
}
|
||||
|
||||
async function addManualDevice() {
|
||||
const errEl = document.getElementById('modal-manual-error');
|
||||
errEl.style.display = 'none';
|
||||
|
||||
const host = document.getElementById('f-manual-host').value.trim();
|
||||
const port = parseInt(document.getElementById('f-manual-port').value, 10) || 49153;
|
||||
|
||||
if (!host) {
|
||||
errEl.textContent = '⚠ Enter an IP address';
|
||||
errEl.style.display = 'block';
|
||||
return;
|
||||
// ── Brightness control ───────────────────────────────────────────────────────
|
||||
function updateBrightnessPreview(i, value) {
|
||||
const valueEl = document.getElementById('bright-val-' + i);
|
||||
if (valueEl) {
|
||||
valueEl.textContent = Math.round(value) + '%';
|
||||
}
|
||||
}
|
||||
|
||||
const btn = document.getElementById('btn-add-manual');
|
||||
btn.disabled = true; btn.textContent = 'Adding…';
|
||||
|
||||
async function setBrightness(i, value) {
|
||||
const dev = devices[i];
|
||||
const slider = document.getElementById('bright-' + i);
|
||||
if (!dev || !slider) return;
|
||||
|
||||
slider.disabled = true;
|
||||
try {
|
||||
const result = await api('POST', '/api/devices/discover', {
|
||||
manualEntries: [{ host, port }]
|
||||
});
|
||||
await api('POST', `/api/devices/${dev.host}/${dev.port}/brightness`, { brightness: Number(value) });
|
||||
|
||||
if (!result || result.length === 0) {
|
||||
errEl.textContent = '⚠ No device found at that address';
|
||||
errEl.style.display = 'block';
|
||||
btn.disabled = false; btn.textContent = 'Add';
|
||||
return;
|
||||
}
|
||||
|
||||
const device = result.find((d) => d.host === host);
|
||||
if (device) {
|
||||
const friendlyName = device.friendlyName || device.name || host;
|
||||
toast(`Added ${friendlyName}`, 'success');
|
||||
devices = result;
|
||||
renderDevices();
|
||||
closeManualModal();
|
||||
} else {
|
||||
errEl.textContent = '⚠ Device added but not found in results';
|
||||
errEl.style.display = 'block';
|
||||
// Update power toggle state based on brightness
|
||||
const chk = document.getElementById('dchk-' + i);
|
||||
if (chk) {
|
||||
chk.checked = Number(value) > 0;
|
||||
}
|
||||
|
||||
toast(`Brightness set to ${Math.round(value)}%`, 'success');
|
||||
} catch (err) {
|
||||
errEl.textContent = `⚠ ${err.message}`;
|
||||
errEl.style.display = 'block';
|
||||
toast(`Failed to set brightness: ${err.message}`, 'error');
|
||||
} finally {
|
||||
btn.disabled = false; btn.textContent = 'Add';
|
||||
slider.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -927,6 +995,67 @@ function closeRuleModal(e) {
|
||||
document.getElementById('modal-rule').classList.remove('open');
|
||||
}
|
||||
|
||||
// ── Add Device Modal ───────────────────────────────────────────────────────
|
||||
function openAddDeviceModal() {
|
||||
document.getElementById('modal-add-device').classList.add('open');
|
||||
document.getElementById('add-device-error').style.display = 'none';
|
||||
document.getElementById('add-device-host').value = '';
|
||||
document.getElementById('add-device-port').value = '';
|
||||
setTimeout(() => document.getElementById('add-device-host').focus(), 100);
|
||||
}
|
||||
|
||||
function closeAddDeviceModal(e) {
|
||||
if (e && e.target !== document.getElementById('modal-add-device')) return;
|
||||
document.getElementById('modal-add-device').classList.remove('open');
|
||||
}
|
||||
|
||||
async function addDeviceManually() {
|
||||
const errEl = document.getElementById('add-device-error');
|
||||
const btn = document.getElementById('btn-add-device');
|
||||
|
||||
errEl.style.display = 'none';
|
||||
|
||||
const host = document.getElementById('add-device-host').value.trim();
|
||||
const port = document.getElementById('add-device-port').value.trim();
|
||||
|
||||
if (!host) {
|
||||
errEl.textContent = 'IP address is required';
|
||||
errEl.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
// Basic IP validation
|
||||
const ipPattern = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
||||
if (!ipPattern.test(host)) {
|
||||
errEl.textContent = 'Please enter a valid IP address';
|
||||
errEl.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
const portNum = port ? parseInt(port, 10) : 49153;
|
||||
if (portNum < 1 || portNum > 65535) {
|
||||
errEl.textContent = 'Port must be between 1 and 65535';
|
||||
errEl.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Adding…';
|
||||
|
||||
try {
|
||||
const device = await api('POST', '/api/devices/add', { host, port: portNum });
|
||||
toast(`Successfully added ${device.friendlyName || device.host}`, 'success');
|
||||
closeAddDeviceModal();
|
||||
await loadDevices(); // Refresh device list
|
||||
} catch (err) {
|
||||
errEl.textContent = err.message || 'Failed to add device';
|
||||
errEl.style.display = 'block';
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Add Device';
|
||||
}
|
||||
}
|
||||
|
||||
async function saveRule() {
|
||||
const errEl = document.getElementById('modal-rule-error');
|
||||
errEl.style.display = 'none';
|
||||
|
||||
Reference in New Issue
Block a user