mirror of
https://github.com/smittix/intercept.git
synced 2026-06-08 14:11:54 -07:00
feat: add PagerDirectory JS component
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
const PagerDirectory = (function () {
|
||||
'use strict';
|
||||
|
||||
const STORAGE_KEY = 'pagerView';
|
||||
|
||||
// Map<address, { count, protocol, lastSeen }>
|
||||
const addresses = new Map();
|
||||
let highlighted = null;
|
||||
|
||||
// ---- Helpers ----
|
||||
|
||||
function esc(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, '&').replace(/</g, '<')
|
||||
.replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function formatAge(ts) {
|
||||
const s = Math.floor((Date.now() - ts) / 1000);
|
||||
if (s < 10) return 'just now';
|
||||
if (s < 60) return `${s}s ago`;
|
||||
return `${Math.floor(s / 60)}m ago`;
|
||||
}
|
||||
|
||||
// ---- Directory rendering ----
|
||||
|
||||
function renderDirectory() {
|
||||
const entriesEl = document.getElementById('pagerDirEntries');
|
||||
const countEl = document.getElementById('pagerDirCount');
|
||||
if (!entriesEl) return;
|
||||
|
||||
const sorted = [...addresses.entries()].sort((a, b) => b[1].count - a[1].count);
|
||||
const maxCount = sorted.length > 0 ? sorted[0][1].count : 1;
|
||||
|
||||
if (countEl) countEl.textContent = sorted.length;
|
||||
|
||||
sorted.forEach(([addr, data]) => {
|
||||
let el = entriesEl.querySelector(`[data-pdir-addr="${CSS.escape(addr)}"]`);
|
||||
const isActive = addr === highlighted;
|
||||
const pct = Math.round((data.count / maxCount) * 100);
|
||||
const isPocsag = data.protocol !== 'flex';
|
||||
const protoClass = isPocsag ? 'pdir-proto--p' : 'pdir-proto--f';
|
||||
const barClass = isPocsag ? '' : 'pdir-bar--flex';
|
||||
const html = `
|
||||
<div class="pdir-entry-top">
|
||||
<span class="pdir-proto ${protoClass}">${isPocsag ? 'P' : 'F'}</span>
|
||||
<span class="pdir-addr">${esc(addr)}</span>
|
||||
<span class="pdir-new-dot"></span>
|
||||
<span class="pdir-count">×${data.count}</span>
|
||||
</div>
|
||||
<div class="pdir-bar-wrap"><div class="pdir-bar ${barClass}" style="width:${pct}%"></div></div>
|
||||
<div class="pdir-age">${formatAge(data.lastSeen)}</div>`;
|
||||
|
||||
if (!el) {
|
||||
el = document.createElement('div');
|
||||
el.className = 'pdir-entry';
|
||||
el.dataset.pdirAddr = addr;
|
||||
el.addEventListener('click', () => toggleHighlight(addr));
|
||||
entriesEl.appendChild(el);
|
||||
}
|
||||
el.classList.toggle('pdir-entry--active', isActive);
|
||||
el.innerHTML = html;
|
||||
});
|
||||
|
||||
// Re-order DOM to match sort
|
||||
sorted.forEach(([addr]) => {
|
||||
const el = entriesEl.querySelector(`[data-pdir-addr="${CSS.escape(addr)}"]`);
|
||||
if (el) entriesEl.appendChild(el);
|
||||
});
|
||||
}
|
||||
|
||||
function flashNewDot(addr) {
|
||||
// Find the dot inside this entry after the current render frame
|
||||
setTimeout(() => {
|
||||
const entriesEl = document.getElementById('pagerDirEntries');
|
||||
const entry = entriesEl?.querySelector(`[data-pdir-addr="${CSS.escape(addr)}"]`);
|
||||
const dot = entry?.querySelector('.pdir-new-dot');
|
||||
if (!dot) return;
|
||||
dot.classList.remove('pdir-new-dot--active');
|
||||
void dot.offsetWidth; // force reflow to restart animation
|
||||
dot.classList.add('pdir-new-dot--active');
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// ---- Highlight ----
|
||||
|
||||
function toggleHighlight(addr) {
|
||||
if (highlighted === addr) clearHighlight();
|
||||
else highlight(addr);
|
||||
}
|
||||
|
||||
function highlight(addr) {
|
||||
highlighted = addr;
|
||||
renderDirectory();
|
||||
|
||||
const feedLabel = document.getElementById('pagerFeedLabel');
|
||||
const clearBtn = document.getElementById('pagerClearHighlight');
|
||||
if (feedLabel) feedLabel.textContent = `${addr} highlighted`;
|
||||
if (clearBtn) clearBtn.style.display = 'inline';
|
||||
|
||||
const output = document.getElementById('output');
|
||||
if (!output) return;
|
||||
|
||||
output.querySelectorAll('.signal-card').forEach(card => {
|
||||
card.classList.toggle('pdir-hl', card.dataset.address === addr);
|
||||
});
|
||||
|
||||
const first = output.querySelector(`.signal-card[data-address="${CSS.escape(addr)}"]`);
|
||||
if (first) first.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
|
||||
function clearHighlight() {
|
||||
highlighted = null;
|
||||
renderDirectory();
|
||||
|
||||
const feedLabel = document.getElementById('pagerFeedLabel');
|
||||
const clearBtn = document.getElementById('pagerClearHighlight');
|
||||
if (feedLabel) feedLabel.textContent = 'All messages';
|
||||
if (clearBtn) clearBtn.style.display = 'none';
|
||||
|
||||
document.getElementById('output')
|
||||
?.querySelectorAll('.pdir-hl')
|
||||
.forEach(c => c.classList.remove('pdir-hl'));
|
||||
}
|
||||
|
||||
// ---- Public: message hook ----
|
||||
|
||||
function addMessage(msg) {
|
||||
const addr = msg.address;
|
||||
if (!addr) return;
|
||||
const proto = (msg.protocol || '').includes('FLEX') ? 'flex' : 'pocsag';
|
||||
const entry = addresses.get(addr);
|
||||
if (entry) {
|
||||
entry.count++;
|
||||
entry.lastSeen = Date.now();
|
||||
entry.protocol = proto;
|
||||
} else {
|
||||
addresses.set(addr, { count: 1, protocol: proto, lastSeen: Date.now() });
|
||||
}
|
||||
renderDirectory();
|
||||
flashNewDot(addr);
|
||||
// Re-apply highlight class to the newly inserted card (caller inserts it after this hook)
|
||||
if (highlighted === addr) {
|
||||
setTimeout(() => {
|
||||
const output = document.getElementById('output');
|
||||
output?.querySelectorAll(`.signal-card[data-address="${CSS.escape(addr)}"]`)
|
||||
.forEach(c => c.classList.add('pdir-hl'));
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Show / hide / reset ----
|
||||
|
||||
function applyViewState(mode) {
|
||||
const dirPanel = document.getElementById('pagerDirectoryView');
|
||||
const feedHeader = document.getElementById('pagerFeedHeader');
|
||||
|
||||
if (mode === 'pager') {
|
||||
const saved = localStorage.getItem(STORAGE_KEY) || 'directory';
|
||||
const isDir = saved === 'directory';
|
||||
if (dirPanel) dirPanel.style.display = isDir ? 'flex' : 'none';
|
||||
if (feedHeader) feedHeader.style.display = isDir ? 'flex' : 'none';
|
||||
_updateToggle(isDir);
|
||||
} else {
|
||||
if (dirPanel) dirPanel.style.display = 'none';
|
||||
if (feedHeader) feedHeader.style.display = 'none';
|
||||
clearHighlight();
|
||||
}
|
||||
}
|
||||
|
||||
function show() {
|
||||
localStorage.setItem(STORAGE_KEY, 'directory');
|
||||
applyViewState('pager');
|
||||
}
|
||||
|
||||
function hide() {
|
||||
localStorage.setItem(STORAGE_KEY, 'feed');
|
||||
applyViewState('pager');
|
||||
}
|
||||
|
||||
function _updateToggle(isDir) {
|
||||
document.getElementById('pagerToggleDir')?.classList.toggle('view-toggle-btn--active', isDir);
|
||||
document.getElementById('pagerToggleFeed')?.classList.toggle('view-toggle-btn--active', !isDir);
|
||||
}
|
||||
|
||||
function reset() {
|
||||
addresses.clear();
|
||||
highlighted = null;
|
||||
const entriesEl = document.getElementById('pagerDirEntries');
|
||||
const countEl = document.getElementById('pagerDirCount');
|
||||
if (entriesEl) entriesEl.innerHTML = '';
|
||||
if (countEl) countEl.textContent = '0';
|
||||
clearHighlight();
|
||||
}
|
||||
|
||||
return { addMessage, highlight, clearHighlight, show, hide, reset, applyViewState };
|
||||
})();
|
||||
Reference in New Issue
Block a user