From 1e76b916933792720b1255819067284484b384be Mon Sep 17 00:00:00 2001 From: Alex Bloom Date: Wed, 16 Apr 2025 10:13:53 -0400 Subject: [PATCH 1/8] Minor spelling mistake on the extension screen. --- console/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/console/index.html b/console/index.html index 3f88817..0c8dca5 100644 --- a/console/index.html +++ b/console/index.html @@ -483,7 +483,7 @@

Extension Config

-

Etension Address

+

Extension Address

@@ -492,7 +492,7 @@

Etension Address

-

Etension Port

+

Extension Port

From 72954fa06edeeba8eaf024194a819d4223636dfe Mon Sep 17 00:00:00 2001 From: Alex Bloom Date: Thu, 24 Apr 2025 10:34:48 -0400 Subject: [PATCH 2/8] Changes to interface for call log --- console/client.js | 75 +++++++++++++++ console/css/custom.css | 128 +++++++++++++++++++++++++- console/index.html | 18 ++++ daemon/Properties/launchSettings.json | 2 +- daemon/Radio.cs | 7 +- 5 files changed, 223 insertions(+), 7 deletions(-) diff --git a/console/client.js b/console/client.js index 706cc85..72ab87a 100644 --- a/console/client.js +++ b/console/client.js @@ -10,6 +10,7 @@ const defaultConfig = { Radios: [], Autoconnect: false, ClockFormat: "UTC", + CallLogOverlay: true, Audio: { ButtonSounds: true, UnselectedVol: -9.0, @@ -460,6 +461,76 @@ window.electronAPI.saveMidiConfig((event, data) => { Radio UI Functions ***********************************************************************************/ +/** + * Toggle the caller log on the radio card + * @param {HTMLElement} toggleAnchor the anchor element that was clicked + * @returns {void} + * */ +function toggleCallerLog(toggleAnchor){ + const wrap = toggleAnchor.previousElementSibling; // the .caller-log-wrap + wrap.classList.toggle('expanded'); + + + const card = toggleAnchor.closest('.radio-card'); + card.classList.toggle('log-open', wrap.classList.contains('expanded')); + + // change tooltip text + toggleAnchor.title = wrap.classList.contains('expanded') + ? 'Hide caller log' : 'Show caller log'; +} + +/** + * 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); + + // keep only three rows + while (log.children.length > 3){ + log.removeChild(log.lastChild); + } + // clear the live line + live.textContent = ""; + st.liveId = ""; + } +} /** * Select a radio * @param {string} id the id of the radio to select @@ -633,6 +704,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() { diff --git a/console/css/custom.css b/console/css/custom.css index 66cae51..ac47fe6 100644 --- a/console/css/custom.css +++ b/console/css/custom.css @@ -408,18 +408,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; /* expanded size */ + 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; } @@ -740,7 +747,8 @@ ion-icon { .radio-card .content { position: absolute; width: 100%; - height: 90px; + min-height:90px; /* original size */ + height:auto; /* expand with caller log */ top: 32px; left: 2px; box-sizing: border-box; @@ -847,6 +855,120 @@ ion-icon { color: var(--color-card-midpurple); } +/* 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; + max-height:54px; /* exactly three rows */ + overflow:hidden; /* hides rows we remove last */ +} + +.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); /* faint backdrop so it stands out */ + border:1px solid rgba(255,255,255,.08); + transition:max-height .25s ease; + max-height:0; /* collapsed */ + 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; /* floats on the same line as live row */ + 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 */ +#caller-drawer{ + position:fixed; + top:48px; /* under your navbar */ + right:0; + width:320px; /* tweak to taste */ + height:calc(100% - 48px); + background:var(--color-bg-mid); + box-shadow:-6px 0 14px rgba(0,0,0,.6); + transform:translateX(100%); /* hidden off-screen */ + transition:transform .25s ease; + z-index:50; + display:flex; + flex-direction:column; +} +#caller-drawer.open{ transform:translateX(0); } + +/* 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; +} +#drawer-title{ flex:1 1 auto; overflow:hidden; text-overflow:ellipsis; } +#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; /* scroll if log gets long */ + padding:8px; +} + /******************************** Bottom Control Bar Styling ********************************/ diff --git a/console/index.html b/console/index.html index 0c8dca5..cfc844a 100644 --- a/console/index.html +++ b/console/index.html @@ -186,6 +186,24 @@

    Placeholder Name

    Zone Text
    Channel Text
    + +
    + + + + + + + +
    diff --git a/daemon/Properties/launchSettings.json b/daemon/Properties/launchSettings.json index 940bc2b..0c392a0 100644 --- a/daemon/Properties/launchSettings.json +++ b/daemon/Properties/launchSettings.json @@ -3,7 +3,7 @@ "verbose noreset": { "commandName": "Project", "commandLineArgs": "-d -v -nr -c .\\config\\config.toml", - "workingDirectory": "H:\\Documents\\GitHub\\RadioConsole2\\daemon" + "workingDirectory": "E:\\RadioConsole2\\daemon" }, "verbose": { "commandName": "Project", diff --git a/daemon/Radio.cs b/daemon/Radio.cs index dcff3e2..a4d632f 100644 --- a/daemon/Radio.cs +++ b/daemon/Radio.cs @@ -239,10 +239,11 @@ public void Start(bool noreset) { // Update the radio status to connecting Status.State = RadioState.Connecting; - RadioStatusCallback(); + // Start runtimes depending on control type if (Type == RadioType.SB9600) { + RadioStatusCallback(); IntSB9600.radioStatus = Status; IntSB9600.Start(noreset); } @@ -266,7 +267,7 @@ private void RadioStatusCallback() { Log.Verbose("Got radio status callback from interface"); // Perform lookups on zone/channel names (radio-control-type agnostic) - if (ZoneLookups.Count > 0) + if (ZoneLookups != null && ZoneLookups.Count > 0) { foreach (TextLookup lookup in ZoneLookups) { @@ -293,7 +294,7 @@ private void RadioStatusCallback() } } } - if (ChanLookups.Count > 0) + if (ChanLookups !=null && ChanLookups.Count > 0) { foreach (TextLookup lookup in ChanLookups) { From 3dde6e654be5b3777ee549c5a3379b1b3f79bbd4 Mon Sep 17 00:00:00 2001 From: Alex Bloom Date: Fri, 25 Apr 2025 13:15:11 -0400 Subject: [PATCH 3/8] Added Setting to switch display modes for Call logs. --- console/client.js | 138 +++++++++++++++++++++++++++++++++++++---- console/css/custom.css | 59 +++++++++++++----- console/index.html | 34 +++++++++- 3 files changed, 202 insertions(+), 29 deletions(-) diff --git a/console/client.js b/console/client.js index 72ab87a..72d1f60 100644 --- a/console/client.js +++ b/console/client.js @@ -10,7 +10,7 @@ const defaultConfig = { Radios: [], Autoconnect: false, ClockFormat: "UTC", - CallLogOverlay: true, + CallLogFormat: "Card", Audio: { ButtonSounds: true, UnselectedVol: -9.0, @@ -233,6 +233,7 @@ function pageLoad() { // Setup clock timer setInterval(updateClock, 100); + } /** @@ -460,6 +461,28 @@ 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`).hide(); + }); + radios.forEach((radio, idx) => { + $(`#radio${idx} .caller-log-toggle`).hide(); + }); + } else { + $("#navbar-call-log").hide(); + radios.forEach((radio, idx) => { + $(`#radio${idx} .caller-log-toggle`).show(); + }); + } +} /** * Toggle the caller log on the radio card @@ -467,18 +490,83 @@ window.electronAPI.saveMidiConfig((event, data) => { * @returns {void} * */ function toggleCallerLog(toggleAnchor){ - const wrap = toggleAnchor.previousElementSibling; // the .caller-log-wrap + 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')); - - const card = toggleAnchor.closest('.radio-card'); - card.classList.toggle('log-open', wrap.classList.contains('expanded')); - - // change tooltip text + // 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. @@ -487,7 +575,6 @@ function toggleCallerLog(toggleAnchor){ * @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 = { @@ -522,13 +609,22 @@ function updateCallerId(cardEl, idStr){ PTT ID`; log.prepend(li); - // keep only three rows - while (log.children.length > 3){ + + + 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); } } /** @@ -558,6 +654,8 @@ function selectRadio(id) { selectedRadioIdx = null; // Update stream volumes updateRadioAudio(); + // Update the call log drawer + selectDrawerCard($(`#${id}`)[0]); } else { // Deselect all radio cards deselectRadios(); @@ -569,6 +667,7 @@ function selectRadio(id) { // Update controls updateRadioControls(); updateRadioAudio(); + selectDrawerCard($(`#${id}`)[0]); } // Update the extension exUpdateSelected(); @@ -1371,6 +1470,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 @@ -1421,16 +1522,26 @@ async function readConfig() { if (config.Autoconnect) { connectAllButton(); } + + //setup call log + if (config.CallLogFormat == "Drawer") { + radios.forEach((radio, idx) => { + $(`#radio${idx} .caller-log-toggle`).hide(); + }); + } } 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); @@ -1450,6 +1561,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)); @@ -1457,6 +1570,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 ac47fe6..7ce8b72 100644 --- a/console/css/custom.css +++ b/console/css/custom.css @@ -264,6 +264,7 @@ ion-icon { #navbar-connect, #navbar-ext, +#navbar-call-log, #navbar-edit { float: right; padding-left: 0; @@ -419,7 +420,7 @@ ion-icon { user-select: none; } .radio-card.log-open { - min-height: 188px; /* expanded size */ + min-height: 188px; clip-path: polygon(0 0, 100% 0, 100% 81%, 93% 100%, 0 100%); } @@ -747,8 +748,8 @@ ion-icon { .radio-card .content { position: absolute; width: 100%; - min-height:90px; /* original size */ - height:auto; /* expand with caller log */ + min-height:90px; + height:auto; top: 32px; left: 2px; box-sizing: border-box; @@ -876,8 +877,7 @@ ion-icon { padding:0; font-size:16px; line-height:18px; - max-height:54px; /* exactly three rows */ - overflow:hidden; /* hides rows we remove last */ + overflow:hidden; } .caller-log .cidtime{opacity:.75} /* dimmed time column */ @@ -888,10 +888,10 @@ ion-icon { position:relative; margin-top:2px; overflow-y:hidden; - background:rgba(0,0,0,.25); /* faint backdrop so it stands out */ + background:rgba(0,0,0,.25); border:1px solid rgba(255,255,255,.08); transition:max-height .25s ease; - max-height:0; /* collapsed */ + max-height:0; width: 90%; } @@ -915,7 +915,7 @@ ion-icon { /* chevron toggle */ .caller-log-toggle{ position:absolute; - right:6px; top:2px; /* floats on the same line as live row */ + right:6px; top:2px; font-size:16px; color:var(--color-txt-mid); cursor:pointer; @@ -927,23 +927,49 @@ ion-icon { transform:rotate(180deg); /* points up when open */ } -/* drawer container */ -#caller-drawer{ +/* drawer container left */ +/*#caller-drawer{ position:fixed; - top:48px; /* under your navbar */ + top:48px; right:0; - width:320px; /* tweak to taste */ + width:320px; height:calc(100% - 48px); background:var(--color-bg-mid); box-shadow:-6px 0 14px rgba(0,0,0,.6); - transform:translateX(100%); /* hidden off-screen */ + transform:translateX(100%); transition:transform .25s ease; z-index:50; display:flex; flex-direction:column; } -#caller-drawer.open{ transform:translateX(0); } +#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; @@ -953,8 +979,9 @@ ion-icon { 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; } +#drawer-title{ flex:1 1 auto; overflow:hidden; text-overflow:ellipsis;display:contents; } #drawer-close{ width:28px; height:28px; font:24px/24px monospace; @@ -965,7 +992,7 @@ ion-icon { /* body */ #drawer-body{ - overflow-y:auto; /* scroll if log gets long */ + overflow-y:auto; padding:8px; } diff --git a/console/index.html b/console/index.html index cfc844a..db16659 100644 --- a/console/index.html +++ b/console/index.html @@ -6,7 +6,7 @@ - + @@ -31,6 +31,20 @@ + + + +