Files
id5100/panel.html
T

280 lines
13 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>ID-5100 · CI-V Remote</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Saira+Condensed:wght@400;600;800&family=Chivo+Mono:wght@400;600&display=swap" rel="stylesheet">
<style>
@font-face{
font-family:'DSEG7';
src:url('https://cdn.jsdelivr.net/npm/dseg@0.46.0/fonts/DSEG7-Classic/DSEG7Classic-Bold.woff2') format('woff2');
font-weight:700;font-display:swap;
}
:root{
--bg:#0b0a08; --metal-hi:#34302a; --metal-lo:#17150f; --groove:#0c0b08;
--amber:#ffb22e; --amber-soft:#c98a2a; --amber-glow:rgba(255,178,46,.55);
--accent:#d35400; --accent-hi:#ff7a1a;
--led-on:#54d171; --led-off:#243126; --red:#ff5436;
--ink:#cdbfa8; --ink-dim:#7c715c; --glass:#0a0d0a;
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%}
body{
background:
radial-gradient(120% 90% at 50% -10%, #211d16 0%, var(--bg) 55%, #050403 100%);
font-family:'Chivo Mono',monospace; color:var(--ink);
min-height:100%; display:grid; place-items:center; padding:26px 14px;
-webkit-tap-highlight-color:transparent;
}
body::after{ /* grain */
content:""; position:fixed; inset:0; pointer-events:none; opacity:.05; mix-blend-mode:overlay;
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}
.unit{
width:min(680px,100%);
background:linear-gradient(168deg,var(--metal-hi),var(--metal-lo) 60%,#100e09);
border-radius:16px; padding:22px;
border:1px solid #3c372e;
box-shadow:
0 1px 0 rgba(255,225,170,.10) inset,
0 -22px 50px rgba(0,0,0,.55) inset,
0 30px 70px rgba(0,0,0,.65),
0 2px 0 #000;
position:relative;
animation:power 1s cubic-bezier(.2,.8,.2,1) both;
}
@keyframes power{from{opacity:0;transform:translateY(14px) scale(.985);filter:brightness(.4)}to{opacity:1;transform:none;filter:none}}
.unit::before,.unit::after{ /* corner screws */
content:""; position:absolute; width:11px; height:11px; border-radius:50%;
background:radial-gradient(circle at 35% 30%,#5b5347,#181610 70%);
box-shadow:0 1px 1px rgba(0,0,0,.8), 0 0 0 1px #0c0b08;
}
.unit::before{top:12px;left:12px} .unit::after{top:12px;right:12px}
.topbar{display:flex;align-items:center;justify-content:center;gap:14px;
position:relative; padding:2px 26px 14px; }
.brand{font-family:'Saira Condensed',sans-serif;font-weight:800;
letter-spacing:.26em; font-size:15px; color:#e8dcc4; text-transform:uppercase}
.brand b{color:var(--accent-hi)}
.link{position:absolute;right:26px;top:0;display:flex;align-items:center;gap:7px;
font-size:10px;letter-spacing:.18em;color:var(--ink-dim);text-transform:uppercase}
.dot{width:9px;height:9px;border-radius:50%;background:var(--led-off);
box-shadow:0 0 0 1px #000 inset; transition:.3s}
.dot.on{background:var(--led-on);box-shadow:0 0 9px var(--led-on),0 0 0 1px #0a3 inset}
.dot.off{background:var(--red);box-shadow:0 0 9px var(--red)}
/* ---- VFD display ---- */
.display{
margin:6px 4px 16px; padding:18px 22px; border-radius:10px;
background:
linear-gradient(180deg,#060806,#0c100c) padding-box,
var(--glass);
border:1px solid #000;
box-shadow:0 0 0 1px #2c2820, 0 14px 26px rgba(0,0,0,.6) inset, 0 2px 0 #2a261e;
position:relative; overflow:hidden;
}
.display::after{ /* scanlines */
content:"";position:absolute;inset:0;pointer-events:none;opacity:.35;
background:repeating-linear-gradient(180deg,transparent 0 2px,rgba(0,0,0,.5) 2px 3px);
}
.row1{display:flex;align-items:baseline;justify-content:space-between;gap:12px}
.freq{
font-family:'DSEG7','Chivo Mono',monospace; font-weight:700;
font-size:clamp(38px,11vw,68px); line-height:1; color:var(--amber);
text-shadow:0 0 6px var(--amber-glow),0 0 22px rgba(255,150,20,.35);
letter-spacing:2px; animation:flick 6s infinite steps(1);
}
@keyframes flick{0%,97%,100%{opacity:1}98%{opacity:.86}}
.unitlbl{font-family:'Saira Condensed',sans-serif;font-weight:600;letter-spacing:.2em;
color:var(--amber-soft);font-size:15px;text-shadow:0 0 8px var(--amber-glow)}
.row2{display:flex;align-items:center;justify-content:space-between;margin-top:12px}
.modebadge{font-family:'Saira Condensed',sans-serif;font-weight:800;letter-spacing:.18em;
font-size:20px;color:var(--accent-hi);text-shadow:0 0 10px rgba(255,122,26,.5);min-width:54px}
.busy{display:flex;align-items:center;gap:8px;font-size:11px;letter-spacing:.18em;
color:var(--ink-dim);text-transform:uppercase}
.busy .dot.on{background:var(--accent-hi);box-shadow:0 0 10px var(--accent-hi)}
/* ---- S meter ---- */
.meter{margin:4px 4px 18px;padding:0 2px}
.bars{display:flex;gap:3px;height:26px;align-items:stretch}
.seg{flex:1;border-radius:2px;background:var(--led-off);
box-shadow:0 0 0 1px #000 inset; transition:background .12s, box-shadow .12s}
.seg.on{background:var(--amber);box-shadow:0 0 7px var(--amber-glow),0 0 0 1px #000 inset}
.seg.on.hot{background:var(--red);box-shadow:0 0 8px var(--red),0 0 0 1px #000 inset}
.scale{display:flex;justify-content:space-between;margin-top:6px;
font-size:9.5px;letter-spacing:.08em;color:var(--ink-dim)}
/* ---- controls ---- */
.panel{display:grid;gap:16px}
.card{background:linear-gradient(180deg,#221f19,#16140f);border:1px solid #332e26;
border-radius:10px;padding:14px 14px 16px;
box-shadow:0 1px 0 rgba(255,220,160,.06) inset,0 6px 16px rgba(0,0,0,.4)}
.label{font-family:'Saira Condensed',sans-serif;font-weight:600;letter-spacing:.22em;
text-transform:uppercase;font-size:11px;color:var(--ink-dim);margin-bottom:11px}
.freqset{display:flex;gap:8px;flex-wrap:wrap}
input[type=number]{flex:1;min-width:120px;background:#0a0907;border:1px solid #3a352b;
color:var(--amber);font-family:'Chivo Mono';font-size:18px;padding:9px 12px;border-radius:7px;
outline:none}
input[type=number]:focus{border-color:var(--accent);box-shadow:0 0 0 2px rgba(211,84,0,.25)}
button{font-family:'Saira Condensed',sans-serif;font-weight:800;letter-spacing:.1em;
text-transform:uppercase;cursor:pointer;border:none;color:#1a160f;
background:linear-gradient(180deg,var(--accent-hi),var(--accent));border-radius:7px;
padding:9px 16px;font-size:14px;box-shadow:0 2px 0 #5a2400,0 6px 12px rgba(0,0,0,.4);
transition:transform .06s, box-shadow .06s}
button:active{transform:translateY(2px);box-shadow:0 0 0 #5a2400}
.chips{display:flex;gap:7px;flex-wrap:wrap;margin-top:10px}
.chip{background:#0c0b08;border:1px solid #3a352b;color:var(--ink);
padding:7px 11px;border-radius:20px;font-size:12px;box-shadow:none}
.chip:active{transform:translateY(1px)}
.modes{display:grid;grid-template-columns:repeat(3,1fr);gap:9px}
.mode{padding:13px 0;border-radius:8px;background:#0d0c09;border:1px solid #34302a;
color:var(--ink-dim);font-size:16px;letter-spacing:.16em;box-shadow:0 2px 0 #000 inset}
.mode.active{color:#1a160f;background:linear-gradient(180deg,var(--amber),var(--amber-soft));
border-color:#000;box-shadow:0 0 16px var(--amber-glow),0 0 0 1px #000}
.fader{display:flex;align-items:center;gap:13px}
.fader .val{font-family:'DSEG7','Chivo Mono';color:var(--amber);font-size:20px;min-width:54px;
text-align:right;text-shadow:0 0 7px var(--amber-glow)}
input[type=range]{-webkit-appearance:none;appearance:none;flex:1;height:8px;border-radius:6px;
background:linear-gradient(90deg,var(--accent) 0%,var(--accent) var(--p,0%),#0c0b08 var(--p,0%),#0c0b08 100%);
border:1px solid #000;outline:none}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:22px;height:26px;border-radius:5px;
background:linear-gradient(180deg,#5b5346,#211d16);border:1px solid #000;cursor:grab;
box-shadow:0 2px 5px rgba(0,0,0,.6),0 1px 0 rgba(255,230,180,.25) inset}
input[type=range]::-moz-range-thumb{width:22px;height:26px;border-radius:5px;
background:linear-gradient(180deg,#5b5346,#211d16);border:1px solid #000;cursor:grab}
.stack{display:grid;gap:13px}
.err{color:var(--red);font-size:11px;letter-spacing:.05em;min-height:14px;text-align:center;
text-transform:uppercase}
.reveal{animation:rise .7s both}
@keyframes rise{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
</style>
</head>
<body>
<div class="unit">
<div class="topbar">
<div class="brand">ICOM <b>ID-5100</b></div>
<div class="link"><span id="led" class="dot off"></span><span id="linktxt">link…</span></div>
</div>
<div class="display reveal" style="animation-delay:.05s">
<div class="row1">
<div id="freq" class="freq">---.-----</div>
<div class="unitlbl">MHz</div>
</div>
<div class="row2">
<div id="mode" class="modebadge">--</div>
<div class="busy"><span id="busyled" class="dot"></span><span id="busytxt">SQ&nbsp;CLOSED</span></div>
</div>
</div>
<div class="meter reveal" style="animation-delay:.12s">
<div id="bars" class="bars"></div>
<div class="scale"><span>S1</span><span>3</span><span>5</span><span>7</span><span>9</span><span>+20</span><span>+40</span><span>+60</span></div>
</div>
<div class="panel">
<div class="card reveal" style="animation-delay:.19s">
<div class="label">Frequency · MHz</div>
<div class="freqset">
<input id="finp" type="number" step="0.005" inputmode="decimal" placeholder="146.520">
<button id="fset">Set</button>
</div>
<div class="chips" id="chips"></div>
</div>
<div class="card reveal" style="animation-delay:.26s">
<div class="label">Mode</div>
<div class="modes" id="modes"></div>
</div>
<div class="card reveal" style="animation-delay:.33s">
<div class="label">Levels</div>
<div class="stack">
<div class="fader"><input id="vol" type="range" min="0" max="255"><span id="volv" class="val">--</span></div>
<div class="fader"><input id="sql" type="range" min="0" max="255"><span id="sqlv" class="val">--</span></div>
</div>
</div>
</div>
<div id="err" class="err"></div>
</div>
<script>
const $=s=>document.querySelector(s);
const SEGS=32, MODES=['FM','AM','DV'];
const CHIPS=[['2m call','146.520'],['70cm call','446.000'],['Nat. simplex','147.300'],['70cm','446.500']];
// build segments
const bars=$('#bars');
for(let i=0;i<SEGS;i++){const d=document.createElement('div');d.className='seg';bars.appendChild(d);}
// build mode buttons
const modesEl=$('#modes');
MODES.forEach(m=>{const b=document.createElement('div');b.className='mode';b.textContent=m;
b.onclick=()=>post('/api/mode',{mode:m});modesEl.appendChild(b);});
// build chips
const chipsEl=$('#chips');
CHIPS.forEach(([lbl,mhz])=>{const c=document.createElement('button');c.className='chip';
c.textContent=lbl+' · '+mhz;c.onclick=()=>post('/api/freq',{mhz:+mhz});chipsEl.appendChild(c);});
let dragging=null;
function err(m){$('#err').textContent=m||'';}
async function post(path,body){
try{const r=await fetch(path,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
const j=await r.json(); if(!j.ok){err(j.error||'rejected');} else {err('');} refresh();
}catch(e){err('send failed');}
}
function fmtFreq(mhz){return Number(mhz).toFixed(5);}
function render(s){
$('#led').className='dot on'; $('#linktxt').textContent='online';
$('#freq').textContent=fmtFreq(s.freq_mhz);
$('#mode').textContent=s.mode;
// s-meter
const lit=Math.round(s.smeter/255*SEGS);
bars.querySelectorAll('.seg').forEach((seg,i)=>{
const on=i<lit; seg.className='seg'+(on?' on':'')+(on&&i>=SEGS*0.72?' hot':'');});
// squelch
const busy=s.squelch_open;
$('#busyled').className='dot'+(busy?' on':'');
$('#busytxt').innerHTML=busy?'SQ&nbsp;OPEN':'SQ&nbsp;CLOSED';
// modes active
modesEl.querySelectorAll('.mode').forEach(b=>b.classList.toggle('active',b.textContent===s.mode));
// faders (don't fight the user mid-drag)
if(dragging!=='vol'){const v=$('#vol');v.value=s.volume;v.style.setProperty('--p',(s.volume/255*100)+'%');$('#volv').textContent=s.volume;}
if(dragging!=='sql'){const v=$('#sql');v.value=s.squelch_level;v.style.setProperty('--p',(s.squelch_level/255*100)+'%');$('#sqlv').textContent=s.squelch_level;}
}
function offline(msg){$('#led').className='dot off';$('#linktxt').textContent='offline';err(msg||'no radio');}
async function refresh(){
try{const r=await fetch('/api/status');const s=await r.json();
if(s.ok){render(s);} else {offline(s.error);}
}catch(e){offline('server unreachable');}
}
// fader wiring with debounce
function wireFader(id,path,valEl){
const el=$('#'+id);
el.addEventListener('input',()=>{dragging=id;el.style.setProperty('--p',(el.value/255*100)+'%');$(valEl).textContent=el.value;});
let t;
const commit=()=>{clearTimeout(t);t=setTimeout(()=>{post(path,{value:+el.value});dragging=null;},120);};
el.addEventListener('change',commit);
el.addEventListener('pointerup',commit);
}
wireFader('vol','/api/vol','#volv');
wireFader('sql','/api/sql','#sqlv');
$('#fset').onclick=()=>{const v=parseFloat($('#finp').value);if(!isNaN(v))post('/api/freq',{mhz:v});};
$('#finp').addEventListener('keydown',e=>{if(e.key==='Enter')$('#fset').click();});
refresh();
setInterval(refresh,900);
</script>
</body>
</html>