From adc061ae26c4ee5b27ae1bf1e6e988fccd952a99 Mon Sep 17 00:00:00 2001 From: mysticmomba Date: Thu, 24 Sep 2026 09:43:55 -0400 Subject: [PATCH] feat: Added Virtual Scrolling. This allows us to scan a whole /16 subnet without overloading the UI or using a lot of RAM. --- CHANGELOG.md | 1 + src/main.js | 115 ++++++++++++++++++++++++++++++++++++++----------- src/styles.css | 12 ++++++ 3 files changed, 104 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd6c3bc..b6b17f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,4 +3,5 @@ ### 🚀 Features - Added changelog +- Added Themes ## [0.1.0] - 2026-09-23 diff --git a/src/main.js b/src/main.js index bce5afe..979b8a2 100644 --- a/src/main.js +++ b/src/main.js @@ -19,7 +19,7 @@ var theme = (() => { } try { localStorage.setItem(STORAGE_KEY, name); - } catch (e) {} + } catch (e) { } } function init() { @@ -28,7 +28,7 @@ var theme = (() => { var picker = document.getElementById('theme-picker'); picker.value = saved; - picker.addEventListener('change', function() { + picker.addEventListener('change', function () { set(picker.value); }); } @@ -39,12 +39,18 @@ var theme = (() => { return { init }; })(); const table = (() => { + var dirty = false; + var flushTimer = null; + var ipIndex = {}; let data = []; let filtered = []; let sortKey = 'ip'; let sortDir = 'asc'; let filterText = ''; let checkedIps = new Set(); + var ROW_HEIGHT = 32; + var BUFFER_ROWS = 20; + var scrollContainer = null; function ipToNum(ip) { const parts = String(ip).split('.'); @@ -193,20 +199,61 @@ const table = (() => { } function render() { - const tbody = document.getElementById('results-body'); - const count = document.getElementById('host-count'); + var tbody = document.getElementById('results-body'); + var count = document.getElementById('host-count'); if (!tbody) return; + expandedIp = null; - const rows = filtered.map((host) => { - const ip = host.ip; - const status = host.status || 'Unknown'; - const cls = 'status-' + status.toLowerCase(); - const rtt = host.ping ? host.ping.rtt_ms.toFixed(1) : '-'; - const hostname = host.hostname || '-'; + if (!scrollContainer) { + scrollContainer = document.getElementById('results-table-container'); + scrollContainer.addEventListener('scroll', onScroll); + } - const isChecked = checkedIps.has(ip) ? 'checked' : ''; - return '' + renderVisible(); + + if (count) { + var up = data.filter(function (h) { return h.status === 'Up'; }).length; + count.textContent = up + ' up / ' + data.length + ' total'; + } + + if (columns && columns.apply) columns.apply(); + } + + function renderVisible() { + var tbody = document.getElementById('results-body'); + if (!tbody || !scrollContainer) return; + + var totalRows = filtered.length; + var viewHeight = scrollContainer.clientHeight; + var scrollTop = scrollContainer.scrollTop; + var headerHeight = 36; + + var startIdx = Math.max(0, Math.floor((scrollTop - headerHeight) / ROW_HEIGHT) - BUFFER_ROWS); + var visibleCount = Math.ceil(viewHeight / ROW_HEIGHT) + (BUFFER_ROWS * 2); + var endIdx = Math.min(totalRows, startIdx + visibleCount); + + var topSpace = startIdx * ROW_HEIGHT; + var bottomSpace = (totalRows - endIdx) * ROW_HEIGHT; + + var colCount = document.querySelectorAll('#results-table th').length; + + var html = ''; + + if (topSpace > 0) { + html += ''; + } + + for (var i = startIdx; i < endIdx; i++) { + var host = filtered[i]; + var ip = host.ip; + var status = host.status || 'Unknown'; + var cls = 'status-' + status.toLowerCase(); + var rtt = host.ping ? host.ping.rtt_ms.toFixed(1) : '-'; + var hostname = host.hostname || '-'; + var isChecked = checkedIps.has(ip) ? 'checked' : ''; + + html += '' + '' + '' + ip + '' + '' + status + '' @@ -214,24 +261,43 @@ const table = (() => { + '' + hostname + '' + '' + formatPorts(host) + '' + ''; - }); - - tbody.innerHTML = rows.join(''); - - if (count) { - const up = data.filter((h) => h.status === 'Up').length; - count.textContent = up + ' up / ' + data.length + ' total'; } - if (columns && columns.apply) columns.apply(); + + if (bottomSpace > 0) { + html += ''; + } + + tbody.innerHTML = html; + } + + var scrollTimeout = null; + function onScroll() { + if (scrollTimeout) return; + scrollTimeout = requestAnimationFrame(function () { + scrollTimeout = null; + renderVisible(); + if (columns && columns.apply) columns.apply(); + }); } function upsert(host) { - const idx = data.findIndex((h) => h.ip === host.ip); + var idx = data.findIndex(function (h) { return h.ip === host.ip; }); if (idx >= 0) { data[idx] = host; } else { + ipIndex[host.ip] = data.length; data.push(host); } + dirty = true; + if (!flushTimer) { + flushTimer = setTimeout(flush, 200); + } + } + + function flush() { + flushTimer = null; + if (!dirty) return; + dirty = false; refresh(); } @@ -239,6 +305,7 @@ const table = (() => { data = []; filtered = []; checkedIps.clear(); + ipIndex = {}; document.getElementById('check-all').checked = false; refresh(); } @@ -1061,7 +1128,7 @@ var updater = (() => { return { init }; })(); var changelog = (() => { - var modal = function() { return document.getElementById('changelog-modal'); }; + var modal = function () { return document.getElementById('changelog-modal'); }; var loaded = false; async function open() { @@ -1085,11 +1152,11 @@ var changelog = (() => { document.getElementById('btn-changelog').addEventListener('click', open); document.getElementById('changelog-close').addEventListener('click', close); - modal().addEventListener('click', function(e) { + modal().addEventListener('click', function (e) { if (e.target === modal()) close(); }); - document.addEventListener('keydown', function(e) { + document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && modal().classList.contains('visible')) close(); }); } diff --git a/src/styles.css b/src/styles.css index 85b6be5..5cc067f 100644 --- a/src/styles.css +++ b/src/styles.css @@ -409,6 +409,7 @@ body { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + height: 32px; } #results-table tbody tr { @@ -428,6 +429,15 @@ body { box-shadow: inset 3px 0 0 var(--cyan); } +.virtual-spacer { + border: none !important; +} + +.virtual-spacer td { + padding: 0 !important; + border: none !important; +} + /* ===== Status Colors ===== */ .status-up { @@ -1322,6 +1332,7 @@ body { .changelog-content a:hover { text-decoration: underline; } + .theme-picker { padding: 1px 6px; font-size: 11px; @@ -1345,6 +1356,7 @@ body { background: var(--bg-secondary); color: var(--text-primary); } + /* ===== Theme: Midnight ===== */ :root[data-theme="midnight"] {