2025-09-29 00:46:38 -07:00

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>