diff --git a/console/client.js b/console/client.js index 0155de3..23161f7 100644 --- a/console/client.js +++ b/console/client.js @@ -10,6 +10,7 @@ const defaultConfig = { Radios: [], Autoconnect: false, ClockFormat: "UTC", + CallLogFormat: "Card", Audio: { ButtonSounds: true, UnselectedVol: -9.0, @@ -247,6 +248,7 @@ function pageLoad() { // Setup clock timer setInterval(updateClock, 100); + } /** @@ -493,7 +495,177 @@ window.electronAPI.saveMidiConfig((event, data) => { /*********************************************************************************** Radio UI Functions ***********************************************************************************/ +/** + * Setup the caller log toggles + * @param {HTMLElement[]} radios the radio cards to setup + * @returns {void} + * */ +function setupCallerLog(radios){ + if (config.CallLogFormat == "Drawer") { + $("#navbar-call-log").show(); + //hide the caller-log-wrap + radios.forEach((radio, idx) => { + $(`#radio${idx} .caller-log-wrap`).removeClass('expanded'); + $(`#radio${idx}`).removeClass('log-open'); + $(`#radio${idx} .caller-log-wrap`).hide(); + $(`#radio${idx} .caller-log-toggle`).hide(); + $(`#radio${idx} .upper-content .icon-stack`).removeClass('call-chevron'); + }); + } else { + $("#navbar-call-log").hide(); + $("#caller-drawer").removeClass('open'); + radios.forEach((radio, idx) => { + $(`#radio${idx} .caller-log-wrap`).show(); + $(`#radio${idx} .caller-log-toggle`).show(); + $(`#radio${idx} .upper-content .icon-stack`).addClass('call-chevron'); + }); + } +} + +/** + * Toggle the caller log on the radio card + * @param {HTMLElement} toggleAnchor the anchor element that was clicked + * @returns {void} + * */ +function toggleCallerLog(toggleAnchor){ + const cardEl = toggleAnchor.closest('.radio-card'); + + /* — use bottom drawer — */ + if (config.CallLogFormat === "Drawer") { + + const drawer = document.getElementById('caller-drawer'); + + //check if the drawer is open + if (drawer.classList.contains('open')){ + //if it is, close it + closeCallerDrawer(); + //set the toggle text + toggleAnchor.title = 'Show caller log'; + return; + } + //open the drawer + openCallerDrawer(cardEl); + //set the toggle text + toggleAnchor.title = 'Hide caller log'; + return; // skip inline behaviour + } + /* — use inline log — */ + const wrap = toggleAnchor.previousElementSibling; + + wrap.classList.toggle('expanded'); + cardEl.classList.toggle('log-open', wrap.classList.contains('expanded')); + + // tooltip text + toggleAnchor.title = wrap.classList.contains('expanded') + ? 'Hide caller log' : 'Show caller log'; +} + +/************************************************************************ + * Caller-log overlay drawer + ***********************************************************************/ + +// holds a reference to the card whose log is showing – or null +let drawerCard = null; + +function refreshCallerDrawer(cardEl){ + if (config.CallLogFormat !== "Drawer") return; // feature not enabled + if (!drawerCard || drawerCard !== cardEl) return; // other card / closed + + const body = document.getElementById('drawer-body'); + const drawerTitle = document.getElementById('drawer-title'); + const radioName = cardEl.querySelector('.radio-name').textContent; + const history = cardEl.querySelector('.caller-log'); + + body.innerHTML = ''; + body.appendChild(history.cloneNode(true)); + + drawerTitle.innerHTML = ` ${radioName}`; +} + +function selectDrawerCard(cardEl){ + if (config.CallLogFormat !== "Drawer") return; // feature not enabled + drawerCard = cardEl; + //check if the drawer is open + const drawer = document.getElementById('caller-drawer'); + if (drawer.classList.contains('open')){ + // if it is, refresh the drawer + refreshCallerDrawer(cardEl); + } +} + +function openCallerDrawer(cardEl){ + drawerCard = cardEl; + refreshCallerDrawer(cardEl); // ← just do this + document.getElementById('caller-drawer').classList.add('open'); +} + +/** close the drawer */ +function closeCallerDrawer(){ + document.getElementById('caller-drawer').classList.remove('open'); + drawerCard = null; +} +document.getElementById('drawer-close').onclick = closeCallerDrawer; + +/** + * Show live caller-ID while the radio is Receiving. + * Only when the call *finishes* does the ID get added to the scroll-back. + * + * @param {HTMLElement} cardEl – the .radio-card element + * @param {string|null} idStr – the current caller-ID (null/"" when idle) + */ +function updateCallerId(cardEl, idStr){ + // this object lives for the lifetime of the element + if (!cardEl._cidState){ + cardEl._cidState = { + liveId : "", // ID currently shown in live line + active : false // true while RX is up + }; + } + const st = cardEl._cidState; + const live = cardEl.querySelector('.callerid-live'); + const log = cardEl.querySelector('.caller-log'); + + /* ── radio is Receiving ─────────────────────────────────────────── */ + if (idStr){ // non-empty ⇒ still RX’ing + live.textContent = idStr; // update live line + st.liveId = idStr; // remember it + st.active = true; // mark "call in progress" + + /* ── radio just went idle – append the *previous* ID ────────────── */ + } else if (st.active){ // we *were* RXing and just stopped + st.active = false; + + const timeStr = + (config?.ClockFormat === "UTC") + ? getTimeUTC("HH:mm:ss") + : getTimeLocal("HH:mm:ss"); + + // build one
  • row and stick it on top of the UL + const li = document.createElement('li'); + li.innerHTML = + `${st.liveId} + ${timeStr} + PTT ID`; + log.prepend(li); + + + var logLength = 3; + if (config.CallLogFormat === "Drawer"){ + logLength = 10; + } + while (log.children.length > logLength){ + log.removeChild(log.lastChild); + } + + // clear the live line + live.textContent = ""; + st.liveId = ""; + + // update the drawer if it's open + refreshCallerDrawer(cardEl); + } +} /** * Select a radio * @param {string} id the id of the radio to select @@ -521,6 +693,8 @@ function selectRadio(id) { selectedRadioIdx = null; // Update stream volumes updateRadioAudio(); + // Update the call log drawer + selectDrawerCard($(`#${id}`)[0]); } else { // Deselect all radio cards deselectRadios(); @@ -532,6 +706,7 @@ function selectRadio(id) { // Update controls updateRadioControls(); updateRadioAudio(); + selectDrawerCard($(`#${id}`)[0]); } // Update the extension exUpdateSelected(); @@ -888,6 +1063,10 @@ function updateRadioCard(idx) { const shortChan = radio.status.ChannelName.substring(0,19); radioCard.find("#channel-text").html(shortChan); } + // Update caller ID + if (radio.status.CallerId != null) { + updateCallerId(radioCard[0], radio.status.CallerId); + } // Remove all current classes setTimeout(function() { @@ -1620,6 +1799,8 @@ async function readConfig() { $("#daemon-autoconnect").prop('checked', config.Autoconnect); // Clock Format $("#client-timeformat").val(config.ClockFormat); + // Call log format + $("#client-call-log").val(config.CallLogFormat); // Audio stuff $("#client-rxagc").prop("checked", config.Audio.UseAGC); // Unselected Volume @@ -1676,16 +1857,23 @@ async function readConfig() { if (config.Autoconnect) { connectAllButton(); } + + //setup call log + setupCallerLog(radios); + } async function saveConfig() { // Client config values const clockFormat = $("#client-timeformat").val(); + const callLogFormat = $("#client-call-log").val(); const useAgc = $("#client-rxagc").is(":checked"); const unselectedVol = $("#unselected-vol").val(); const toneVol = $("#tone-vol").val(); - config.ClockFormat = clockFormat; + + config.ClockFormat = clockFormat; + config.CallLogFormat = callLogFormat; config.Audio.UseAGC = useAgc; config.Audio.UnselectedVol = parseFloat(unselectedVol); config.Audio.ToneVolume = parseFloat(toneVol); @@ -1705,6 +1893,8 @@ async function saveConfig() { if (audio.context) { updateRadioAudio(); } + // Update the call log format + setupCallerLog(config.Radios); const result = await window.electronAPI.saveConfig(JSON.stringify(config, null, 4)); @@ -1712,6 +1902,9 @@ async function saveConfig() { { alert("Failed to save config: " + result); } + + + } function newRadioClear() { diff --git a/console/css/custom.css b/console/css/custom.css index b7e882a..0183155 100644 --- a/console/css/custom.css +++ b/console/css/custom.css @@ -311,6 +311,7 @@ ion-icon { #navbar-connect, #navbar-ext, +#navbar-call-log, #navbar-edit { float: right; padding-left: 0; @@ -455,18 +456,25 @@ ion-icon { margin-left: 8px; margin-top: 8px; width: 300px; - height: 124px; + min-height:134px; + vertical-align: top; + overflow: hidden; + transition:max-height .25s ease; font-family: "Iosevka Bold"; /* Clip Path for Angles */ clip-path: polygon(0 0, 100% 0, 100% 83%, 93% 100%, 0 100%); background-color: rgba(0,0,0,0); user-select: none; } +.radio-card.log-open { + min-height: 188px; + clip-path: polygon(0 0, 100% 0, 100% 81%, 93% 100%, 0 100%); +} .radio-card .card { /* Width is overall card width -4 px for border */ width: 296px; - height: 120px; + height: auto; position: absolute; } @@ -815,7 +823,8 @@ ion-icon { .radio-card .content { position: absolute; width: 100%; - height: 90px; + min-height:90px; + height:auto; top: 32px; left: 2px; box-sizing: border-box; @@ -829,6 +838,17 @@ ion-icon { height: 40% } +.radio-card .upper-content .no-toggle{ + position: absolute; + right: 0px; + top: 2px; +} +.radio-card .upper-content .call-chevron { + position: absolute; + right: 15px; + top: 2px; +} + .radio-card .lower-content { width: 100%; height: 40%; @@ -920,7 +940,7 @@ ion-icon { .radio-card #zone-text { font-family: "Iosevka Bold"; font-size: 22px; - float: left; + /* float: left; */ } .radio-card #channel-text { @@ -1010,12 +1030,152 @@ ion-icon { color: var(--color-card-purple-mid); } +/* live line ----------------------------------------------------*/ +.callerid-live{ + height:18px; + line-height:18px; + font-size:18px; + font-family:"Iosevka Bold"; + color:var(--color-txt-light); + /* little breathing-room under the live line */ + margin-bottom:2px; + overflow:hidden; + white-space:nowrap; + text-overflow:ellipsis; +} + +/* scrolling list ----------------------------------------------*/ +.caller-log{ + list-style:none; + margin:0; + padding:0; + font-size:16px; + line-height:18px; + overflow:hidden; +} + +.caller-log .cidtime{opacity:.75} /* dimmed time column */ +.caller-log .cid { overflow:hidden; text-overflow:ellipsis; } + +/* wrapper for the live line and log */ +.caller-log-wrap{ + position:relative; + margin-top:2px; + overflow-y:hidden; + background:rgba(0,0,0,.25); + border:1px solid rgba(255,255,255,.08); + transition:max-height .25s ease; + max-height:0; + width: 90%; +} + +/* expanded state */ +.caller-log-wrap.expanded{ + max-height:54px; +} + +/* hide empty rows */ +.caller-log li.stub{visibility:hidden} + +/* every row = three-column grid (ID | time | label) */ +.caller-log li{ + display:grid; + grid-template-columns: 1fr 65px 45px; + font-size:14px; line-height:18px; + color:var(--color-txt-light); + border-bottom:1px solid rgba(255,255,255,.1); +} + +/* chevron toggle */ +.caller-log-toggle{ + position:absolute; + right:6px; top:2px; + font-size:16px; + color:var(--color-txt-mid); + cursor:pointer; + user-select:none; + z-index:2; +} + +.caller-log-wrap.expanded + .caller-log-toggle ion-icon{ + transform:rotate(180deg); /* points up when open */ +} + +/* drawer container left */ +/*#caller-drawer{ + position:fixed; + top:48px; + right:0; + width:320px; + height:calc(100% - 48px); + background:var(--color-bg-mid); + box-shadow:-6px 0 14px rgba(0,0,0,.6); + transform:translateX(100%); + transition:transform .25s ease; + z-index:50; + display:flex; + flex-direction:column; +} +#caller-drawer.open{ transform:translateX(0); } */ + + +/* Bottom drawer, centered above the control bar */ +/* base, hidden state */ +#caller-drawer { + position: fixed; + bottom: 60px; + right: 0; + width: 280px; + max-height: 50vh; + background: var(--color-bg-mid); + box-shadow: 0 -6px 14px rgba(0,0,0,.6); + z-index: 50; + transform: translateY(100%); + opacity: 0; + transition: transform 0.3s ease, opacity 0.3s ease; + pointer-events: none; + display: flex; + flex-direction: column; + } + + #caller-drawer.open { + transform: translateY(0); + opacity: 1; + pointer-events: auto; + } + +/* header bar */ +#caller-drawer header{ + height:34px; + padding:4px 8px; + background:var(--color-accent-mid); + color:var(--color-txt-light); + font:20px/26px "Iosevka Bold"; + display:flex; + align-items:center; + justify-content:space-between; +} +#drawer-title{ flex:1 1 auto; overflow:hidden; text-overflow:ellipsis;display:contents; } +#drawer-close{ + width:28px; height:28px; + font:24px/24px monospace; + background:none; border:none; color:var(--color-txt-mid); + cursor:pointer; +} +#drawer-close:hover{ color:var(--color-txt-light); } + +/* body */ +#drawer-body{ + overflow-y:auto; + padding:8px; +} + /******************************** Bottom Control Bar Styling ********************************/ #controlbar { - position: absolute; + position: fixed; bottom: 0; height: 60px; width: 100%; diff --git a/console/main-window.html b/console/main-window.html index 6cceb02..e8e29c8 100644 --- a/console/main-window.html +++ b/console/main-window.html @@ -31,6 +31,20 @@ + + + +