371 lines
19 KiB
HTML
371 lines
19 KiB
HTML
|
|
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>Animation Config</title>
|
|
<link rel="stylesheet" href="/static/fontawesome/css/all.min.css">
|
|
<style>
|
|
:root{
|
|
--bg:#0b0f14; --panel:#0f1720; --muted:#8b97a3; --accent:#7c3aed; --glass: rgba(255,255,255,0.04);
|
|
}
|
|
/* fixed-width label used for the three left-aligned names */
|
|
.label-name{display:inline-block; min-width:140px; width:140px; text-align:left}
|
|
/* mode dropdown styling: dark background and light text */
|
|
.mode-select{margin-left:10px;padding:8px;border-radius:8px;background:#0b1218;color:#e6eef6;border:1px solid rgba(255,255,255,0.04);min-width:120px}
|
|
.mode-select:focus{outline:none; box-shadow:0 0 0 3px rgba(124,58,237,0.12)}
|
|
body{background:linear-gradient(180deg,#07090b 0%, #0b0f14 100%); color:#e6eef6; font-family:Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; margin:0;}
|
|
.container{max-width:920px; margin:36px auto; padding:28px; background:var(--panel); border-radius:12px; box-shadow:0 10px 30px rgba(2,6,23,0.6); display:grid; grid-template-columns:160px 1fr; gap:20px; align-items:start}
|
|
.container{position:relative}
|
|
.logo{display:flex; align-items:center; gap:10px}
|
|
.logo img{width:56px; height:56px; object-fit:contain; border-radius:8px}
|
|
h1{margin:0; font-size:20px}
|
|
p.lead{margin:6px 0 0; color:var(--muted); font-size:13px}
|
|
form{display:flex; flex-direction:column; gap:12px}
|
|
.field{display:flex; gap:12px; align-items:center}
|
|
label{flex:1; display:flex; gap:12px; align-items:center}
|
|
.field input[type=number]{width:6.5rem; padding:8px 10px; background:var(--glass); border:1px solid rgba(255,255,255,0.04); color:inherit; border-radius:8px}
|
|
.note{color:var(--muted); font-size:13px}
|
|
.row{display:flex; gap:12px}
|
|
.save{background:linear-gradient(90deg,var(--accent), #5b21b6); border:none; padding:10px 16px; color:white; border-radius:10px; cursor:pointer; display:inline-flex; gap:8px; align-items:center}
|
|
.card{background:linear-gradient(180deg, rgba(255,255,255,0.02), transparent); padding:14px; border-radius:10px}
|
|
/* tabs */
|
|
.tabs{display:flex;gap:12px;border-bottom:1px solid rgba(255,255,255,0.03);margin-bottom:12px}
|
|
.tab{padding:8px 12px;cursor:pointer;border-radius:8px 8px 0 0;background:transparent;color:var(--muted)}
|
|
.tab.active{background:linear-gradient(90deg,var(--accent), #5b21b6);color:white}
|
|
.tab-panel{display:none}
|
|
.tab-panel.active{display:block}
|
|
.meta{font-size:13px; color:var(--muted)}
|
|
/* modal styles for BLE scan */
|
|
.modal-backdrop{position:fixed;inset:0;background:rgba(0,0,0,0.6);display:none;align-items:center;justify-content:center;z-index:9999}
|
|
.modal{background:var(--panel);border-radius:10px;padding:16px;max-width:680px;width:90%;color:inherit}
|
|
.modal h3{margin:0 0 8px}
|
|
.device-list{max-height:320px;overflow:auto;margin-top:8px}
|
|
.device-item{padding:8px;border-radius:8px;cursor:pointer;border:1px solid rgba(255,255,255,0.03);margin:6px 0}
|
|
.device-item:hover{background:rgba(255,255,255,0.02)}
|
|
.modal-show{display:flex}
|
|
/* comm status top-right */
|
|
.comm-status{position:fixed;top:14px;right:18px;display:flex;align-items:center;gap:8px;background:rgba(11,18,24,0.85);padding:8px 12px;border-radius:10px;border:1px solid rgba(255,255,255,0.04);z-index:1200}
|
|
.comm-status .label{font-size:12px;color:var(--muted);margin-right:6px}
|
|
.status-dot{width:12px;height:12px;border-radius:50%;display:inline-block}
|
|
.status-green{background:#28a745}
|
|
.status-red{background:#e55353}
|
|
footer{margin-top:18px; color:var(--muted); font-size:13px}
|
|
@media (max-width:720px){.container{grid-template-columns:1fr; padding:18px}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- Prominent communication status in the top-right -->
|
|
<div class="comm-status" role="status" aria-live="polite">
|
|
<span class="label">Comm</span>
|
|
<span id="comm-dot" class="status-dot status-red" aria-hidden="true"></span>
|
|
<span id="comm-text" class="meta">Disconnected</span>
|
|
<span id="comm-last" class="meta" style="margin-left:8px"></span>
|
|
</div>
|
|
<div class="container">
|
|
<div class="logo">
|
|
<img src="/static/images/ata_logo.png" alt="ATA Logo" />
|
|
<div>
|
|
<h1>DSLR Director</h1>
|
|
<p class="lead">Configure LED animations and communication.</p>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div class="tabs">
|
|
<div class="tab active" data-target="panel-animations">Animations</div>
|
|
<div class="tab" data-target="panel-comm">Communication</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div id="panel-animations" class="tab-panel active">
|
|
<form method="post" action="/update">
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
|
|
<div style="display:flex;gap:12px;align-items:center">
|
|
<div class="meta">Current values shown. Leave blank to keep existing.</div>
|
|
</div>
|
|
<div style="display:flex;gap:8px">
|
|
<button class="save" type="submit"><i class="fas fa-save"></i> Save</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label>
|
|
<i class="fas fa-bolt" style="width:18px;text-align:center"></i>
|
|
<span style="display:inline-flex;align-items:center;gap:8px">
|
|
<span class="label-name">Home Screen</span>
|
|
</span>
|
|
<!-- Mode selector and color picker for Home Screen -->
|
|
<select id="home-mode" name="home_mode" class="mode-select">
|
|
<option value="animation">Animation</option>
|
|
<option value="solid">Solid Color</option>
|
|
</select>
|
|
<input id="home_animation" type="number" name="home_animation" value="" style="margin-left:8px" />
|
|
<input id="home_color" type="color" name="home_color" value="#ffffff" title="Pick a color" style="margin-left:8px;border-radius:6px;width:44px;height:36px;padding:4px;border:none;background:transparent" />
|
|
</label>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label>
|
|
<i class="fas fa-stopwatch" style="width:18px;text-align:center"></i>
|
|
<span style="display:inline-flex;align-items:center;gap:8px">
|
|
<span class="label-name">Countdown</span>
|
|
</span>
|
|
<!-- Mode selector and color picker for Countdown -->
|
|
<select id="countdown-mode" name="countdown_mode" class="mode-select" hidden>
|
|
<option value="animation">Animation</option>
|
|
<option value="solid">Solid Color</option>
|
|
</select>
|
|
<input id="countdown_animation" type="number" name="countdown_animation" value="" style="margin-left:8px" hidden/>
|
|
<input id="countdown_color" type="color" name="countdown_color" value="#ffffff" title="Pick a color" hidden style="margin-left:8px;border-radius:6px;width:44px;height:36px;padding:4px;border:none;background:transparent" />
|
|
</label>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label>
|
|
<i class="fas fa-tv" style="width:18px;text-align:center"></i>
|
|
<span style="display:inline-flex;align-items:center;gap:8px">
|
|
<span class="label-name">Sharing</span>
|
|
</span>
|
|
<!-- Mode selector and color picker for Sharing -->
|
|
<select id="sharing-mode" name="sharing_mode" class="mode-select">
|
|
<option value="animation">Animation</option>
|
|
<option value="solid">Solid Color</option>
|
|
</select>
|
|
<input id="sharing_animation" type="number" name="sharing_animation" value="" style="margin-left:8px" />
|
|
<input id="sharing_color" type="color" name="sharing_color" value="#ffffff" title="Pick a color" style="margin-left:8px;border-radius:6px;width:44px;height:36px;padding:4px;border:none;background:transparent" />
|
|
</label>
|
|
</div>
|
|
|
|
<footer>
|
|
<div class="note">Tip: Animation IDs are integers. Choose 'Solid Color' and pick a color to send a static color command. Use the Save button to persist changes.</div>
|
|
</footer>
|
|
</form>
|
|
</div>
|
|
|
|
<div id="panel-comm" class="tab-panel">
|
|
<div style="display:flex;flex-direction:column;gap:12px">
|
|
<div class="field">
|
|
<label style="align-items:center;gap:8px">
|
|
<span class="label-name">ID</span>
|
|
<input type="text" id="comm-id" name="comm_id" value="" style="margin-left:8px;padding:8px;border-radius:8px;background:var(--glass);border:1px solid rgba(255,255,255,0.04);color:inherit" />
|
|
</label>
|
|
</div>
|
|
<div class="field">
|
|
<label style="align-items:center;gap:8px">
|
|
<span class="label-name">Device Name</span>
|
|
<input type="text" id="comm-name" name="comm_name" value="" style="margin-left:8px;padding:8px;border-radius:8px;background:var(--glass);border:1px solid rgba(255,255,255,0.04);color:inherit" />
|
|
</label>
|
|
</div>
|
|
<div class="field">
|
|
<label style="align-items:center;gap:8px">
|
|
<span class="label-name">Filter</span>
|
|
<input type="text" id="comm-filter" name="comm_filter" value="" style="margin-left:8px;padding:8px;border-radius:8px;background:var(--glass);border:1px solid rgba(255,255,255,0.04);color:inherit" />
|
|
<button type="button" id="comm-scan" class="save" style="margin-left:8px;padding:8px 10px">Scan</button>
|
|
</label>
|
|
</div>
|
|
<div class="field">
|
|
<label style="align-items:center;gap:8px">
|
|
<span class="label-name">Auto Connect</span>
|
|
<input type="checkbox" id="comm-auto" name="comm_auto" style="margin-left:8px" />
|
|
</label>
|
|
</div>
|
|
<div class="field">
|
|
<label style="align-items:center;gap:8px">
|
|
<span class="label-name">Action</span>
|
|
<button type="button" id="comm-toggle" class="save" style="margin-left:8px">Connect</button>
|
|
</label>
|
|
</div>
|
|
<div class="note">Use Connect to start BLE connection from the web UI. Endpoints /ble/connect and /ble/disconnect should be implemented server-side.</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- BLE Scan modal (moved here so it's outside header/logo regions) -->
|
|
<div id="scan-backdrop" class="modal-backdrop" role="dialog" aria-modal="true">
|
|
<div class="modal" role="document">
|
|
<h3>Scan Results</h3>
|
|
<div style="display:flex;gap:8px;align-items:center">
|
|
<input id="scan-filter" placeholder="filter" style="flex:1;padding:8px;border-radius:6px;background:var(--glass);border:1px solid rgba(255,255,255,0.04);color:inherit" />
|
|
<button id="scan-refresh" class="save">Refresh</button>
|
|
<button id="scan-close" class="save" style="background:transparent;border:1px solid rgba(255,255,255,0.06);color:var(--muted)">Close</button>
|
|
</div>
|
|
<div id="device-list" class="device-list"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// BLE scan modal handlers
|
|
function showScanModal(){
|
|
document.getElementById('scan-backdrop').classList.add('modal-show');
|
|
document.getElementById('scan-filter').value = document.getElementById('comm-filter') ? document.getElementById('comm-filter').value : '';
|
|
doScan();
|
|
}
|
|
function hideScanModal(){ document.getElementById('scan-backdrop').classList.remove('modal-show'); }
|
|
|
|
async function doScan(){
|
|
const listEl = document.getElementById('device-list');
|
|
listEl.innerHTML = '<div class="note">Scanning...</div>';
|
|
const prefix = document.getElementById('scan-filter').value || '';
|
|
try{
|
|
const resp = await fetch('/ble/scan?prefix=' + encodeURIComponent(prefix));
|
|
if(!resp.ok) throw new Error('HTTP ' + resp.status);
|
|
const devices = await resp.json();
|
|
if(!Array.isArray(devices) || devices.length === 0){ listEl.innerHTML = '<div class="note">No devices found</div>'; return; }
|
|
listEl.innerHTML = '';
|
|
devices.forEach(d => {
|
|
const div = document.createElement('div');
|
|
div.className = 'device-item';
|
|
div.textContent = (d.name || '<unknown>') + ' — ' + (d.address || d.id || '');
|
|
div.addEventListener('click', ()=>{
|
|
if(document.getElementById('comm-id')) document.getElementById('comm-id').value = d.address || d.id || '';
|
|
if(document.getElementById('comm-name')) document.getElementById('comm-name').value = d.name || '';
|
|
hideScanModal();
|
|
});
|
|
listEl.appendChild(div);
|
|
});
|
|
}catch(err){ listEl.innerHTML = '<div class="note">Scan failed: '+ (err.message||err) +'</div>'; }
|
|
}
|
|
document.getElementById('comm-scan').addEventListener('click', showScanModal);
|
|
document.getElementById('scan-close').addEventListener('click', hideScanModal);
|
|
document.getElementById('scan-refresh').addEventListener('click', doScan);
|
|
|
|
// Poll /ble/status and update the Comm UI
|
|
async function fetchBleStatus(){
|
|
try{
|
|
const resp = await fetch('/ble/status');
|
|
if(!resp.ok) throw new Error('HTTP ' + resp.status);
|
|
const data = await resp.json();
|
|
const dot = document.getElementById('comm-dot');
|
|
const text = document.getElementById('comm-text');
|
|
const last = document.getElementById('comm-last');
|
|
if(data.connected){
|
|
dot.classList.remove('status-red'); dot.classList.add('status-green');
|
|
text.textContent = 'Connected';
|
|
} else {
|
|
dot.classList.remove('status-green'); dot.classList.add('status-red');
|
|
text.textContent = 'Disconnected';
|
|
}
|
|
if(data.last_connected_ts){
|
|
// last_connected_ts expected in ISO or unix ms; try to parse
|
|
let d = new Date(data.last_connected_ts);
|
|
if(isNaN(d)){
|
|
// maybe millis
|
|
d = new Date(Number(data.last_connected_ts));
|
|
}
|
|
if(!isNaN(d)) last.textContent = 'Last: ' + d.toLocaleString();
|
|
else last.textContent = '';
|
|
} else {
|
|
last.textContent = '';
|
|
}
|
|
}catch(err){
|
|
// network error or endpoint missing
|
|
const dot = document.getElementById('comm-dot');
|
|
const text = document.getElementById('comm-text');
|
|
const last = document.getElementById('comm-last');
|
|
if(dot){ dot.classList.remove('status-green'); dot.classList.add('status-red'); }
|
|
if(text) text.textContent = 'Unknown';
|
|
if(last) last.textContent = '';
|
|
console.debug('ble status fetch failed', err);
|
|
}
|
|
}
|
|
|
|
// initial fetch and interval
|
|
fetchBleStatus();
|
|
setInterval(fetchBleStatus, 5000);
|
|
|
|
// Tabs behavior
|
|
document.querySelectorAll('.tab').forEach(t => t.addEventListener('click', e => {
|
|
document.querySelectorAll('.tab').forEach(x=>x.classList.remove('active'));
|
|
document.querySelectorAll('.tab-panel').forEach(p=>p.classList.remove('active'));
|
|
t.classList.add('active');
|
|
const target = t.getAttribute('data-target');
|
|
const panel = document.getElementById(target);
|
|
if(panel) panel.classList.add('active');
|
|
}));
|
|
|
|
// Connect/Disconnect button
|
|
document.getElementById('comm-toggle').addEventListener('click', async function(){
|
|
const btn = this;
|
|
// Decide action by the button label: if it says 'Connect' -> POST /ble/connect, otherwise POST /ble/disconnect.
|
|
try{
|
|
const label = (btn.textContent || '').trim().toLowerCase();
|
|
const isConnectAction = label.startsWith('connect');
|
|
const url = isConnectAction ? '/ble/connect' : '/ble/disconnect';
|
|
const payload = { id: document.getElementById('comm-id').value, name: document.getElementById('comm-name').value, filter: document.getElementById('comm-filter').value, auto: document.getElementById('comm-auto').checked, action: isConnectAction ? 'connect' : 'disconnect' };
|
|
console.debug('comm-toggle: sending', url, payload);
|
|
const resp = await fetch(url, {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload)});
|
|
if(!resp.ok) throw new Error('HTTP ' + resp.status);
|
|
const data = await resp.json().catch(()=>({}));
|
|
// optimistic update
|
|
const textEl = document.getElementById('comm-text');
|
|
if(isConnectAction){
|
|
if(textEl) textEl.textContent = 'Connected';
|
|
btn.textContent = 'Disconnect';
|
|
document.getElementById('comm-dot').classList.remove('status-red');
|
|
document.getElementById('comm-dot').classList.add('status-green');
|
|
} else {
|
|
if(textEl) textEl.textContent = 'Disconnected';
|
|
btn.textContent = 'Connect';
|
|
document.getElementById('comm-dot').classList.remove('status-green');
|
|
document.getElementById('comm-dot').classList.add('status-red');
|
|
}
|
|
}catch(err){
|
|
alert('Connect action failed: ' + err.message + '\nMake sure /ble/connect and /ble/disconnect are implemented.');
|
|
}
|
|
});
|
|
|
|
// Fetch initial config for animation controls and communication controls
|
|
async function loadInitialConfig(){
|
|
// animations
|
|
try{
|
|
const resp = await fetch('/config-animation');
|
|
if(resp.ok){
|
|
const cfg = await resp.json();
|
|
if(cfg.home_mode !== undefined && document.getElementById('home-mode')) document.getElementById('home-mode').value = cfg.home_mode;
|
|
if(cfg.home_anim !== undefined && document.getElementById('home_animation')) document.getElementById('home_animation').value = cfg.home_anim;
|
|
if(cfg.home_color !== undefined && document.getElementById('home_color')) document.getElementById('home_color').value = cfg.home_color;
|
|
|
|
if(cfg.countdown_mode !== undefined && document.getElementById('countdown-mode')) document.getElementById('countdown-mode').value = cfg.countdown_mode;
|
|
if(cfg.countdown_anim !== undefined && document.getElementById('countdown_animation')) document.getElementById('countdown_animation').value = cfg.countdown_anim;
|
|
if(cfg.countdown_color !== undefined && document.getElementById('countdown_color')) document.getElementById('countdown_color').value = cfg.countdown_color;
|
|
|
|
if(cfg.sharing_mode !== undefined && document.getElementById('sharing-mode')) document.getElementById('sharing-mode').value = cfg.sharing_mode;
|
|
if(cfg.sharing_anim !== undefined && document.getElementById('sharing_animation')) document.getElementById('sharing_animation').value = cfg.sharing_anim;
|
|
if(cfg.sharing_color !== undefined && document.getElementById('sharing_color')) document.getElementById('sharing_color').value = cfg.sharing_color;
|
|
}
|
|
}catch(err){ console.debug('loadInitialConfig animations failed', err); }
|
|
|
|
// communication
|
|
try{
|
|
const resp2 = await fetch('/comm-status');
|
|
if(resp2.ok){
|
|
const c = await resp2.json();
|
|
if(c.id !== undefined && document.getElementById('comm-id')) document.getElementById('comm-id').value = c.id;
|
|
if(c.name !== undefined && document.getElementById('comm-name')) document.getElementById('comm-name').value = c.name;
|
|
if(c.filter !== undefined && document.getElementById('comm-filter')) document.getElementById('comm-filter').value = c.filter;
|
|
if(c.auto !== undefined && document.getElementById('comm-auto')) document.getElementById('comm-auto').checked = !!c.auto;
|
|
// update connection UI
|
|
const textEl = document.getElementById('comm-text');
|
|
const dot = document.getElementById('comm-dot');
|
|
const btn = document.getElementById('comm-toggle');
|
|
if(c.connected){ if(textEl) textEl.textContent='Connected'; if(dot){ dot.classList.remove('status-red'); dot.classList.add('status-green'); } if(btn) btn.textContent='Disconnect'; }
|
|
else { if(textEl) textEl.textContent='Disconnected'; if(dot){ dot.classList.remove('status-green'); dot.classList.add('status-red'); } if(btn) btn.textContent='Connect'; }
|
|
if(c.last_connected_ts){ const d = new Date(c.last_connected_ts); if(!isNaN(d)) document.getElementById('comm-last').textContent = 'Last: ' + d.toLocaleString(); }
|
|
}
|
|
}catch(err){ console.debug('loadInitialConfig comm failed', err); }
|
|
|
|
console.debug('Initial config loaded');
|
|
}
|
|
|
|
// load initial config once DOM is ready
|
|
document.addEventListener('DOMContentLoaded', loadInitialConfig);
|
|
</script>
|
|
</body>
|
|
</html>
|