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 @@
+
+
+
+
@@ -186,7 +204,7 @@
Placeholder Name
Zone Text
-
+
@@ -204,6 +222,24 @@
Placeholder Name
Channel Text
+
+
+
+
+
+
+
+
+
+
@@ -346,6 +382,18 @@
Time Format
+
+
+
+ Call Log Display
+ |
+
+
+ |
+
@@ -357,7 +405,7 @@ RX Audio AGC
|
-
+
@@ -435,7 +483,7 @@ Extension Config
|
- Etension Address
+ Extension Address
|
@@ -444,7 +492,7 @@ Etension Address
|
- Etension Port
+ Extension Port
|
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",
|