reactoradar 1.5.9 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +103 -31
- package/app.js +900 -153
- package/bin/setup.js +82 -6
- package/index.html +4 -0
- package/main.js +61 -9
- package/package.json +10 -2
- package/preload.js +3 -1
- package/styles.css +117 -13
package/app.js
CHANGED
|
@@ -94,15 +94,46 @@ function highlight(html, term) {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
// ─── Navigation ───────────────────────────────────────────────────────────────
|
|
97
|
+
function _getPanelOrder() {
|
|
98
|
+
const order = getTabOrder();
|
|
99
|
+
// Always include settings at the end
|
|
100
|
+
if (!order.includes('settings')) order.push('settings');
|
|
101
|
+
return order;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function switchPanel(panel) {
|
|
105
|
+
if (!$(`panel-${panel}`)) return;
|
|
106
|
+
document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
|
|
107
|
+
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
|
108
|
+
const btn = document.querySelector(`.nav-btn[data-panel="${panel}"]`);
|
|
109
|
+
if (btn) btn.classList.add('active');
|
|
110
|
+
$(`panel-${panel}`).classList.add('active');
|
|
111
|
+
state.activePanel = panel;
|
|
112
|
+
}
|
|
113
|
+
|
|
97
114
|
document.querySelectorAll('.nav-btn').forEach(btn => {
|
|
98
|
-
btn.addEventListener('click', () =>
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
115
|
+
btn.addEventListener('click', () => switchPanel(btn.dataset.panel));
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Keyboard shortcuts: Cmd+1–9 for panel switching, Cmd+K clear
|
|
119
|
+
document.addEventListener('keydown', (e) => {
|
|
120
|
+
if (!(e.metaKey || e.ctrlKey)) return;
|
|
121
|
+
const num = parseInt(e.key);
|
|
122
|
+
const panelOrder = _getPanelOrder();
|
|
123
|
+
if (num >= 1 && num <= panelOrder.length) {
|
|
124
|
+
e.preventDefault();
|
|
125
|
+
const vis = getTabVisibility();
|
|
126
|
+
const target = panelOrder[num - 1];
|
|
127
|
+
if (vis[target] !== false) switchPanel(target);
|
|
128
|
+
}
|
|
129
|
+
if (e.key === 'k') {
|
|
130
|
+
e.preventDefault();
|
|
131
|
+
clearActiveTab();
|
|
132
|
+
}
|
|
133
|
+
if (e.key === 's') {
|
|
134
|
+
e.preventDefault();
|
|
135
|
+
takeScreenshot();
|
|
136
|
+
}
|
|
106
137
|
});
|
|
107
138
|
|
|
108
139
|
// Global filter removed — each panel has its own search input
|
|
@@ -193,11 +224,25 @@ function clearAll() {
|
|
|
193
224
|
}
|
|
194
225
|
|
|
195
226
|
// ─── CDP Button ───────────────────────────────────────────────────────────────
|
|
196
|
-
$('btnCDP')
|
|
227
|
+
$('btnCDP')?.addEventListener('click', () => {
|
|
197
228
|
// Tell main process to open the CDP DevTools window with the best available target
|
|
198
229
|
window.electronAPI?.openCDPTarget(null); // null = use latest known target
|
|
199
230
|
});
|
|
200
231
|
|
|
232
|
+
// ─── Screenshot Button ────────────────────────────────────────────────────────
|
|
233
|
+
$('btnScreenshot')?.addEventListener('click', takeScreenshot);
|
|
234
|
+
|
|
235
|
+
function takeScreenshot() {
|
|
236
|
+
const btn = $('btnScreenshot');
|
|
237
|
+
if (!btn) return;
|
|
238
|
+
const origText = btn.innerHTML;
|
|
239
|
+
btn.innerHTML = '<span style="opacity:0.6">Saving...</span>';
|
|
240
|
+
// Use Electron's native capturePage — always works, no DOM rendering issues
|
|
241
|
+
window.electronAPI?.captureScreenshot();
|
|
242
|
+
btn.innerHTML = '<span style="color:var(--green)">Saved!</span>';
|
|
243
|
+
setTimeout(() => { btn.innerHTML = origText; }, 2000);
|
|
244
|
+
}
|
|
245
|
+
|
|
201
246
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
202
247
|
// IPC from Main
|
|
203
248
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -273,9 +318,13 @@ if (window.electronAPI) {
|
|
|
273
318
|
document.querySelectorAll('#aboutVersion').forEach(el => el.textContent = 'v' + version);
|
|
274
319
|
});
|
|
275
320
|
|
|
276
|
-
window.electronAPI.on('update-available', ({ current, latest }) => {
|
|
277
|
-
|
|
278
|
-
|
|
321
|
+
window.electronAPI.on('update-available', ({ current, latest, autoUpdate }) => {
|
|
322
|
+
state._updateAvailable = { current, latest, autoUpdate };
|
|
323
|
+
_applyUpdateBanner();
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
window.electronAPI.on('update-downloaded', ({ version }) => {
|
|
327
|
+
state._updateDownloaded = version;
|
|
279
328
|
_applyUpdateBanner();
|
|
280
329
|
});
|
|
281
330
|
|
|
@@ -297,23 +346,47 @@ if (window.electronAPI) {
|
|
|
297
346
|
function _applyUpdateBanner() {
|
|
298
347
|
const info = state._updateAvailable;
|
|
299
348
|
if (!info) return;
|
|
300
|
-
const { current, latest } = info;
|
|
349
|
+
const { current, latest, autoUpdate } = info;
|
|
350
|
+
const downloaded = state._updateDownloaded;
|
|
351
|
+
|
|
301
352
|
const el = $('aboutVersion');
|
|
302
|
-
if (el
|
|
303
|
-
|
|
304
|
-
|
|
353
|
+
if (el) {
|
|
354
|
+
if (downloaded) {
|
|
355
|
+
el.innerHTML = `v${current} <span style="color:var(--green);font-size:10px;margin-left:6px">v${downloaded} ready to install</span>`;
|
|
356
|
+
} else {
|
|
357
|
+
el.innerHTML = `v${current} <span style="color:var(--green);font-size:10px;margin-left:6px">v${latest} available</span>`;
|
|
358
|
+
}
|
|
305
359
|
}
|
|
360
|
+
|
|
361
|
+
// Remove old button if state changed
|
|
362
|
+
const oldBtn = $('updateBtn');
|
|
363
|
+
if (oldBtn && downloaded && !oldBtn.dataset.isRestart) oldBtn.parentElement?.remove();
|
|
364
|
+
|
|
306
365
|
// Add update button in settings if not already there
|
|
307
366
|
if (!$('updateBtn')) {
|
|
308
367
|
const aboutEl = document.querySelector('.settings-about');
|
|
309
368
|
if (aboutEl) {
|
|
310
369
|
const btn = document.createElement('div');
|
|
311
370
|
btn.style.cssText = 'margin-top:10px';
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
371
|
+
if (downloaded) {
|
|
372
|
+
// Update is downloaded — show "Restart & Update"
|
|
373
|
+
btn.innerHTML = '<button id="updateBtn" data-is-restart="1" class="tb-btn primary" style="font-size:11px;padding:6px 16px">Restart & Update to v' + downloaded + '</button>';
|
|
374
|
+
aboutEl.appendChild(btn);
|
|
375
|
+
$('updateBtn')?.addEventListener('click', () => {
|
|
376
|
+
window.electronAPI?.installUpdate();
|
|
377
|
+
});
|
|
378
|
+
} else if (autoUpdate) {
|
|
379
|
+
// Auto-update in progress — show downloading status
|
|
380
|
+
btn.innerHTML = '<button id="updateBtn" class="tb-btn" style="font-size:11px;padding:6px 16px;opacity:0.7" disabled>Downloading v' + latest + '...</button>';
|
|
381
|
+
aboutEl.appendChild(btn);
|
|
382
|
+
} else {
|
|
383
|
+
// npx/manual — show download link
|
|
384
|
+
btn.innerHTML = '<button id="updateBtn" class="tb-btn primary" style="font-size:11px;padding:6px 16px">Download v' + latest + '</button>';
|
|
385
|
+
aboutEl.appendChild(btn);
|
|
386
|
+
$('updateBtn')?.addEventListener('click', () => {
|
|
387
|
+
window.electronAPI?.openExternal('https://github.com/sharanagouda/react-native-debugger/releases');
|
|
388
|
+
});
|
|
389
|
+
}
|
|
317
390
|
}
|
|
318
391
|
}
|
|
319
392
|
}
|
|
@@ -362,6 +435,7 @@ function initConsolePanel() {
|
|
|
362
435
|
<span class="badge" id="cBadge">0</span>
|
|
363
436
|
<input id="consoleSearch" class="net-search-input" style="margin-left:12px" placeholder="Filter logs..." />
|
|
364
437
|
<div class="ml-auto" style="display:flex;align-items:center;gap:6px">
|
|
438
|
+
<button class="panel-clear-btn" id="consoleExport" title="Export logs as JSON">Export</button>
|
|
365
439
|
<button class="panel-clear-btn" id="consoleClear" title="Clear console">Clear</button>
|
|
366
440
|
<div class="console-level-dropdown" id="consoleLevelDropdown">
|
|
367
441
|
<button class="console-level-btn" id="consoleLevelBtn">Levels ▾</button>
|
|
@@ -426,9 +500,19 @@ function initConsolePanel() {
|
|
|
426
500
|
|
|
427
501
|
updateLevelBtnText();
|
|
428
502
|
|
|
503
|
+
$('consoleExport')?.addEventListener('click', () => {
|
|
504
|
+
const data = JSON.stringify(state.console.logs, null, 2);
|
|
505
|
+
const blob = new Blob([data], { type: 'application/json' });
|
|
506
|
+
const url = URL.createObjectURL(blob);
|
|
507
|
+
const a = document.createElement('a');
|
|
508
|
+
a.href = url; a.download = `reactoradar-console-${Date.now()}.json`; a.click();
|
|
509
|
+
URL.revokeObjectURL(url);
|
|
510
|
+
});
|
|
511
|
+
|
|
429
512
|
$('consoleClear').addEventListener('click', () => {
|
|
430
513
|
state.console.logs = [];
|
|
431
514
|
_consolePending = [];
|
|
515
|
+
_lastLogMsg = ''; _lastLogRow = null; _lastLogCount = 1;
|
|
432
516
|
$('cBadge').textContent = '0';
|
|
433
517
|
renderConsole();
|
|
434
518
|
});
|
|
@@ -497,13 +581,85 @@ function updateLevelBtnText() {
|
|
|
497
581
|
|
|
498
582
|
// Console is fed via IPC (network-event handled in IPC section above)
|
|
499
583
|
|
|
584
|
+
// ─── Toast Notifications ─────────────────────────────────────────────────────
|
|
585
|
+
let _toastContainer = null;
|
|
586
|
+
const _activeToasts = {};
|
|
587
|
+
|
|
588
|
+
function getToastsEnabled() {
|
|
589
|
+
try { return localStorage.getItem('rn-debug-toasts') !== 'false'; } catch { return true; }
|
|
590
|
+
}
|
|
591
|
+
function setToastsEnabled(v) {
|
|
592
|
+
try { localStorage.setItem('rn-debug-toasts', v ? 'true' : 'false'); } catch {}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function showToast(message, type, targetPanel) {
|
|
596
|
+
if (!getToastsEnabled()) return;
|
|
597
|
+
if (!_toastContainer) {
|
|
598
|
+
_toastContainer = document.createElement('div');
|
|
599
|
+
_toastContainer.id = 'toastContainer';
|
|
600
|
+
_toastContainer.className = 'toast-container';
|
|
601
|
+
document.body.appendChild(_toastContainer);
|
|
602
|
+
}
|
|
603
|
+
// Don't show toast if user is already on the target panel
|
|
604
|
+
if (targetPanel && state.activePanel === targetPanel) return;
|
|
605
|
+
|
|
606
|
+
// Deduplicate: if same message already showing, increment count
|
|
607
|
+
const key = `${type}:${message}`;
|
|
608
|
+
if (_activeToasts[key] && _activeToasts[key].el.parentNode) {
|
|
609
|
+
const existing = _activeToasts[key];
|
|
610
|
+
existing.count++;
|
|
611
|
+
const msgEl = existing.el.querySelector('.toast-msg');
|
|
612
|
+
if (msgEl) msgEl.textContent = `${message} (${existing.count})`;
|
|
613
|
+
// Reset auto-remove timer
|
|
614
|
+
clearTimeout(existing.timer);
|
|
615
|
+
existing.timer = setTimeout(() => {
|
|
616
|
+
if (existing.el.parentNode) existing.el.remove();
|
|
617
|
+
delete _activeToasts[key];
|
|
618
|
+
}, 5000);
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const toast = document.createElement('div');
|
|
623
|
+
toast.className = `toast toast-${type || 'info'}`;
|
|
624
|
+
toast.innerHTML = `<span class="toast-msg">${esc(message)}</span>`;
|
|
625
|
+
if (targetPanel) {
|
|
626
|
+
const btn = document.createElement('span');
|
|
627
|
+
btn.className = 'toast-action';
|
|
628
|
+
btn.textContent = 'View';
|
|
629
|
+
btn.addEventListener('click', () => { switchPanel(targetPanel); toast.remove(); delete _activeToasts[key]; });
|
|
630
|
+
toast.appendChild(btn);
|
|
631
|
+
}
|
|
632
|
+
const close = document.createElement('span');
|
|
633
|
+
close.className = 'toast-close';
|
|
634
|
+
close.textContent = '✕';
|
|
635
|
+
close.addEventListener('click', () => { toast.remove(); delete _activeToasts[key]; });
|
|
636
|
+
toast.appendChild(close);
|
|
637
|
+
|
|
638
|
+
_toastContainer.appendChild(toast);
|
|
639
|
+
const timer = setTimeout(() => {
|
|
640
|
+
if (toast.parentNode) toast.remove();
|
|
641
|
+
delete _activeToasts[key];
|
|
642
|
+
}, 5000);
|
|
643
|
+
_activeToasts[key] = { el: toast, count: 1, timer };
|
|
644
|
+
// Keep max 3 toasts
|
|
645
|
+
const toasts = _toastContainer.querySelectorAll('.toast');
|
|
646
|
+
if (toasts.length > 3) { toasts[0].remove(); }
|
|
647
|
+
}
|
|
648
|
+
|
|
500
649
|
// ─── Batched console append (fixes re-render performance) ────────────────────
|
|
501
650
|
let _consolePending = [];
|
|
502
651
|
let _consoleRAF = null;
|
|
503
652
|
|
|
653
|
+
let _lastLogMsg = '';
|
|
654
|
+
let _lastLogRow = null;
|
|
655
|
+
let _lastLogCount = 1;
|
|
656
|
+
|
|
657
|
+
const MAX_CONSOLE_LOGS = 5000;
|
|
658
|
+
|
|
504
659
|
function addConsoleLog(event) {
|
|
505
660
|
state.console.logs.push(event);
|
|
506
661
|
_consolePending.push(event);
|
|
662
|
+
|
|
507
663
|
// Batch DOM updates via rAF — only one paint per frame
|
|
508
664
|
if (!_consoleRAF) {
|
|
509
665
|
_consoleRAF = requestAnimationFrame(flushConsoleBatch);
|
|
@@ -532,7 +688,26 @@ function flushConsoleBatch() {
|
|
|
532
688
|
if (!state.console.showRedux) return;
|
|
533
689
|
} else if (levelFilters && !levelFilters[l.level]) return;
|
|
534
690
|
if (searchFilter && !l.message?.toLowerCase().includes(searchFilter)) return;
|
|
535
|
-
|
|
691
|
+
|
|
692
|
+
// Group consecutive identical messages
|
|
693
|
+
const msgKey = `${l.level}:${l.message || ''}`;
|
|
694
|
+
if (msgKey === _lastLogMsg && _lastLogRow && _lastLogRow.parentNode) {
|
|
695
|
+
_lastLogCount++;
|
|
696
|
+
let badge = _lastLogRow.querySelector('.log-group-badge');
|
|
697
|
+
if (!badge) {
|
|
698
|
+
badge = document.createElement('span');
|
|
699
|
+
badge.className = 'log-group-badge';
|
|
700
|
+
_lastLogRow.insertBefore(badge, _lastLogRow.firstChild);
|
|
701
|
+
}
|
|
702
|
+
badge.textContent = _lastLogCount;
|
|
703
|
+
return; // Don't add a new row
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
_lastLogMsg = msgKey;
|
|
707
|
+
_lastLogCount = 1;
|
|
708
|
+
const row = buildLogRow(l);
|
|
709
|
+
_lastLogRow = row;
|
|
710
|
+
frag.appendChild(row);
|
|
536
711
|
added++;
|
|
537
712
|
});
|
|
538
713
|
|
|
@@ -542,9 +717,9 @@ function flushConsoleBatch() {
|
|
|
542
717
|
// Auto-scroll only if user is already near the bottom (within 150px)
|
|
543
718
|
const wasAtBottom = (list.scrollHeight - list.scrollTop - list.clientHeight) < 150;
|
|
544
719
|
list.appendChild(frag);
|
|
545
|
-
// Keep DOM size manageable — remove oldest rows
|
|
720
|
+
// Keep DOM size manageable — remove oldest rows
|
|
546
721
|
const rows = list.querySelectorAll('.log-row');
|
|
547
|
-
const MAX_DOM_ROWS =
|
|
722
|
+
const MAX_DOM_ROWS = 2000;
|
|
548
723
|
if (rows.length > MAX_DOM_ROWS) {
|
|
549
724
|
const toRemove = rows.length - MAX_DOM_ROWS;
|
|
550
725
|
for (let i = 0; i < toRemove; i++) rows[i].remove();
|
|
@@ -911,6 +1086,12 @@ function showContextMenu(e, items) {
|
|
|
911
1086
|
const menu = document.createElement('div');
|
|
912
1087
|
menu.className = 'ctx-menu';
|
|
913
1088
|
items.forEach(({ label, action }) => {
|
|
1089
|
+
if (label === '—' || !action) {
|
|
1090
|
+
const sep = document.createElement('div');
|
|
1091
|
+
sep.className = 'ctx-sep';
|
|
1092
|
+
menu.appendChild(sep);
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
914
1095
|
const item = document.createElement('div');
|
|
915
1096
|
item.className = 'ctx-item';
|
|
916
1097
|
item.textContent = label;
|
|
@@ -943,17 +1124,20 @@ function renderConsole() {
|
|
|
943
1124
|
});
|
|
944
1125
|
|
|
945
1126
|
list.querySelectorAll('.log-row').forEach(e => e.remove());
|
|
946
|
-
if (
|
|
1127
|
+
if (!empty) { /* guard */ }
|
|
1128
|
+
else if (visible.length > 0) {
|
|
947
1129
|
empty.style.display = 'none';
|
|
948
1130
|
} else if (state.console.logs.length > 0) {
|
|
949
|
-
|
|
950
|
-
empty.querySelector('.
|
|
951
|
-
|
|
1131
|
+
const lbl = empty.querySelector('.label');
|
|
1132
|
+
const hint = empty.querySelector('.hint');
|
|
1133
|
+
if (lbl) lbl.textContent = 'No matching logs';
|
|
1134
|
+
if (hint) hint.textContent = 'Adjust level filters or clear search to see logs';
|
|
952
1135
|
empty.style.display = 'flex';
|
|
953
1136
|
} else {
|
|
954
|
-
|
|
955
|
-
empty.querySelector('.
|
|
956
|
-
|
|
1137
|
+
const lbl = empty.querySelector('.label');
|
|
1138
|
+
const hint = empty.querySelector('.hint');
|
|
1139
|
+
if (lbl) lbl.textContent = 'No logs yet';
|
|
1140
|
+
if (hint) hint.textContent = 'Logs will appear here automatically';
|
|
957
1141
|
empty.style.display = 'flex';
|
|
958
1142
|
}
|
|
959
1143
|
|
|
@@ -994,6 +1178,7 @@ function initNetworkPanel() {
|
|
|
994
1178
|
<span class="panel-label">Network</span>
|
|
995
1179
|
<span class="badge" id="nBadge">0</span>
|
|
996
1180
|
<div class="ml-auto" style="display:flex;align-items:center;gap:6px">
|
|
1181
|
+
<button class="panel-clear-btn" id="networkExport" title="Export as HAR">Export HAR</button>
|
|
997
1182
|
<button class="panel-clear-btn" id="networkClear" title="Clear network">Clear</button>
|
|
998
1183
|
<label class="toggle-label" for="netToggle">
|
|
999
1184
|
<span class="toggle-text" id="netToggleText">Capture ON</span>
|
|
@@ -1015,6 +1200,16 @@ function initNetworkPanel() {
|
|
|
1015
1200
|
<button class="net-type-btn" data-type="doc">Doc</button>
|
|
1016
1201
|
<button class="net-type-btn" data-type="ws">WS</button>
|
|
1017
1202
|
</div>
|
|
1203
|
+
<div class="net-status-filters" id="netStatusFilters">
|
|
1204
|
+
<button class="net-status-btn active" data-status="all">All</button>
|
|
1205
|
+
<button class="net-status-btn" data-status="2xx">2xx</button>
|
|
1206
|
+
<button class="net-status-btn" data-status="errors">Errors</button>
|
|
1207
|
+
<button class="net-status-btn net-slow-btn" data-status="slow">Slow (>1s)</button>
|
|
1208
|
+
</div>
|
|
1209
|
+
<div class="net-hidden-wrap" style="position:relative;margin-left:4px">
|
|
1210
|
+
<button class="net-status-btn net-hidden-btn" id="netHiddenBtn" style="display:none" title="Manage hidden URLs">Hidden</button>
|
|
1211
|
+
<div class="net-hidden-dropdown" id="netHiddenDropdown" style="display:none"></div>
|
|
1212
|
+
</div>
|
|
1018
1213
|
<div class="net-throttle" id="netThrottle">
|
|
1019
1214
|
<select id="netThrottleSelect" class="net-throttle-select">
|
|
1020
1215
|
<option value="none">No throttling</option>
|
|
@@ -1042,6 +1237,17 @@ function initNetworkPanel() {
|
|
|
1042
1237
|
</div>
|
|
1043
1238
|
<div class="detail-content" id="netDetailContent"></div>
|
|
1044
1239
|
</div>
|
|
1240
|
+
</div>
|
|
1241
|
+
<div class="net-stats-bar" id="netStatsBar">
|
|
1242
|
+
<span id="netStatsTotal">0 requests</span>
|
|
1243
|
+
<span class="net-stats-sep">|</span>
|
|
1244
|
+
<span id="netStatsAvg">Avg: —</span>
|
|
1245
|
+
<span class="net-stats-sep">|</span>
|
|
1246
|
+
<span id="netStatsSlowest">Slowest: —</span>
|
|
1247
|
+
<span class="net-stats-sep">|</span>
|
|
1248
|
+
<span id="netStatsErrors">Errors: 0</span>
|
|
1249
|
+
<span class="net-stats-sep">|</span>
|
|
1250
|
+
<span id="netStatsSlow">Slow (>1s): 0</span>
|
|
1045
1251
|
</div>`;
|
|
1046
1252
|
|
|
1047
1253
|
$('netToggle').addEventListener('change', (e) => {
|
|
@@ -1066,6 +1272,69 @@ function initNetworkPanel() {
|
|
|
1066
1272
|
renderNetwork();
|
|
1067
1273
|
});
|
|
1068
1274
|
|
|
1275
|
+
// Status filter buttons (All / 2xx / Errors / Slow)
|
|
1276
|
+
$('netStatusFilters').addEventListener('click', (e) => {
|
|
1277
|
+
const btn = e.target.closest('.net-status-btn');
|
|
1278
|
+
if (!btn) return;
|
|
1279
|
+
$('netStatusFilters').querySelectorAll('.net-status-btn').forEach(b => b.classList.remove('active'));
|
|
1280
|
+
btn.classList.add('active');
|
|
1281
|
+
state.network.statusFilter = btn.dataset.status;
|
|
1282
|
+
renderNetwork();
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1285
|
+
// Hidden URLs button
|
|
1286
|
+
$('netHiddenBtn')?.addEventListener('click', () => {
|
|
1287
|
+
const dd = $('netHiddenDropdown');
|
|
1288
|
+
if (!dd) return;
|
|
1289
|
+
const isOpen = dd.style.display !== 'none';
|
|
1290
|
+
if (isOpen) { dd.style.display = 'none'; return; }
|
|
1291
|
+
// Build dropdown with hidden URL list
|
|
1292
|
+
const hidden = getHiddenURLs();
|
|
1293
|
+
dd.innerHTML = '';
|
|
1294
|
+
if (!hidden.length) { dd.style.display = 'none'; return; }
|
|
1295
|
+
const title = document.createElement('div');
|
|
1296
|
+
title.className = 'net-hidden-title';
|
|
1297
|
+
title.innerHTML = `<span>Hidden URLs (${hidden.length})</span><button class="net-hidden-clear" id="netHiddenClearAll">Clear All</button>`;
|
|
1298
|
+
dd.appendChild(title);
|
|
1299
|
+
hidden.forEach(pattern => {
|
|
1300
|
+
const row = document.createElement('div');
|
|
1301
|
+
row.className = 'net-hidden-row';
|
|
1302
|
+
const label = document.createElement('span');
|
|
1303
|
+
label.className = 'net-hidden-url';
|
|
1304
|
+
label.textContent = pattern;
|
|
1305
|
+
label.title = pattern;
|
|
1306
|
+
row.appendChild(label);
|
|
1307
|
+
const btn = document.createElement('button');
|
|
1308
|
+
btn.className = 'net-hidden-unhide';
|
|
1309
|
+
btn.textContent = 'Unhide';
|
|
1310
|
+
btn.addEventListener('click', () => {
|
|
1311
|
+
removeHiddenURL(pattern);
|
|
1312
|
+
row.remove();
|
|
1313
|
+
renderNetwork();
|
|
1314
|
+
if (!getHiddenURLs().length) dd.style.display = 'none';
|
|
1315
|
+
});
|
|
1316
|
+
row.appendChild(btn);
|
|
1317
|
+
dd.appendChild(row);
|
|
1318
|
+
});
|
|
1319
|
+
dd.style.display = 'block';
|
|
1320
|
+
// Clear all handler
|
|
1321
|
+
dd.querySelector('#netHiddenClearAll')?.addEventListener('click', () => {
|
|
1322
|
+
setHiddenURLs([]);
|
|
1323
|
+
_updateHiddenBadge();
|
|
1324
|
+
dd.style.display = 'none';
|
|
1325
|
+
renderNetwork();
|
|
1326
|
+
});
|
|
1327
|
+
});
|
|
1328
|
+
// Close dropdown when clicking outside
|
|
1329
|
+
document.addEventListener('click', (e) => {
|
|
1330
|
+
const dd = $('netHiddenDropdown');
|
|
1331
|
+
if (dd && dd.style.display !== 'none' && !e.target.closest('.net-hidden-wrap')) {
|
|
1332
|
+
dd.style.display = 'none';
|
|
1333
|
+
}
|
|
1334
|
+
});
|
|
1335
|
+
// Initialize hidden badge
|
|
1336
|
+
_updateHiddenBadge();
|
|
1337
|
+
|
|
1069
1338
|
// Throttle select
|
|
1070
1339
|
$('netThrottleSelect').addEventListener('change', (e) => {
|
|
1071
1340
|
state.network.throttle = e.target.value;
|
|
@@ -1073,6 +1342,37 @@ function initNetworkPanel() {
|
|
|
1073
1342
|
window.electronAPI?.setNetworkThrottle(state.network.throttle);
|
|
1074
1343
|
});
|
|
1075
1344
|
|
|
1345
|
+
// Export network as HAR
|
|
1346
|
+
$('networkExport')?.addEventListener('click', () => {
|
|
1347
|
+
const entries = state.network.order.map(id => {
|
|
1348
|
+
const r = state.network.requests[id];
|
|
1349
|
+
if (!r) return null;
|
|
1350
|
+
return {
|
|
1351
|
+
startedDateTime: new Date(r.ts || Date.now()).toISOString(),
|
|
1352
|
+
time: r.duration || 0,
|
|
1353
|
+
request: {
|
|
1354
|
+
method: r.method || 'GET',
|
|
1355
|
+
url: r.url || '',
|
|
1356
|
+
headers: Object.entries(r.requestHeaders || {}).map(([n, v]) => ({ name: n, value: v })),
|
|
1357
|
+
postData: r.requestBody ? { mimeType: 'application/json', text: typeof r.requestBody === 'object' ? JSON.stringify(r.requestBody) : String(r.requestBody) } : undefined,
|
|
1358
|
+
},
|
|
1359
|
+
response: {
|
|
1360
|
+
status: r.status || 0,
|
|
1361
|
+
statusText: r.statusText || '',
|
|
1362
|
+
headers: Object.entries(r.responseHeaders || {}).map(([n, v]) => ({ name: n, value: v })),
|
|
1363
|
+
content: { size: -1, mimeType: 'application/json', text: r.responseBody ? (typeof r.responseBody === 'object' ? JSON.stringify(r.responseBody) : String(r.responseBody)) : '' },
|
|
1364
|
+
},
|
|
1365
|
+
timings: { send: 0, wait: r.duration || 0, receive: 0 },
|
|
1366
|
+
};
|
|
1367
|
+
}).filter(Boolean);
|
|
1368
|
+
const har = { log: { version: '1.2', creator: { name: 'ReactoRadar', version: '1.6.0' }, entries } };
|
|
1369
|
+
const blob = new Blob([JSON.stringify(har, null, 2)], { type: 'application/json' });
|
|
1370
|
+
const url = URL.createObjectURL(blob);
|
|
1371
|
+
const a = document.createElement('a');
|
|
1372
|
+
a.href = url; a.download = `reactoradar-network-${Date.now()}.har`; a.click();
|
|
1373
|
+
URL.revokeObjectURL(url);
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1076
1376
|
// Clear network
|
|
1077
1377
|
$('networkClear').addEventListener('click', () => {
|
|
1078
1378
|
state.network.requests = {};
|
|
@@ -1225,9 +1525,25 @@ function handleNetworkEvent(event) {
|
|
|
1225
1525
|
if (phase === 'request') {
|
|
1226
1526
|
state.network.requests[id] = { ...event, _tab: 'headers' };
|
|
1227
1527
|
if (!state.network.order.includes(id)) state.network.order.push(id);
|
|
1528
|
+
// Cap network history to prevent memory leak
|
|
1529
|
+
const MAX_NET_HISTORY = 1000;
|
|
1530
|
+
if (state.network.order.length > MAX_NET_HISTORY) {
|
|
1531
|
+
const trimIds = state.network.order.splice(0, state.network.order.length - MAX_NET_HISTORY);
|
|
1532
|
+
trimIds.forEach(tid => delete state.network.requests[tid]);
|
|
1533
|
+
}
|
|
1228
1534
|
$('nBadge').textContent = state.network.order.length;
|
|
1229
1535
|
} else {
|
|
1230
1536
|
Object.assign(state.network.requests[id] || (state.network.requests[id] = {}), event);
|
|
1537
|
+
// Toast for errors and slow APIs
|
|
1538
|
+
const r = state.network.requests[id];
|
|
1539
|
+
if (r && (phase === 'response' || phase === 'error')) {
|
|
1540
|
+
const name = r.url?.split('/').pop()?.split('?')[0] || r.url || '?';
|
|
1541
|
+
if (r.phase === 'error' || (r.status && r.status >= 400)) {
|
|
1542
|
+
showToast(`API Error: ${r.status || 'ERR'} ${name}`, 'error', 'network');
|
|
1543
|
+
} else if ((r.duration || 0) >= 3000) {
|
|
1544
|
+
showToast(`Slow API: ${(r.duration/1000).toFixed(1)}s — ${name}`, 'warn', 'network');
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1231
1547
|
}
|
|
1232
1548
|
if (!_netRAF) {
|
|
1233
1549
|
_netRAF = requestAnimationFrame(() => {
|
|
@@ -1283,8 +1599,10 @@ function renderNetwork() {
|
|
|
1283
1599
|
if (!r) return false;
|
|
1284
1600
|
if (statusFilter === '2xx' && !(r.status >= 200 && r.status < 300)) return false;
|
|
1285
1601
|
if (statusFilter === 'errors' && !(r.phase === 'error' || r.status >= 400)) return false;
|
|
1602
|
+
if (statusFilter === 'slow' && !((r.duration || 0) >= 1000)) return false;
|
|
1286
1603
|
if (searchFilter && !r.url?.toLowerCase().includes(searchFilter)) return false;
|
|
1287
1604
|
if (typeFilter !== 'all' && !matchNetType(r, typeFilter)) return false;
|
|
1605
|
+
if (isURLHidden(r.url || '')) return false;
|
|
1288
1606
|
return true;
|
|
1289
1607
|
});
|
|
1290
1608
|
|
|
@@ -1319,6 +1637,32 @@ function renderNetwork() {
|
|
|
1319
1637
|
frag.appendChild(buildNetRow(r, wfMin, wfRange));
|
|
1320
1638
|
});
|
|
1321
1639
|
rows.appendChild(frag);
|
|
1640
|
+
_updateNetStats();
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
function _updateNetStats() {
|
|
1644
|
+
const allReqs = state.network.order.map(id => state.network.requests[id]).filter(Boolean);
|
|
1645
|
+
const completed = allReqs.filter(r => r.duration != null);
|
|
1646
|
+
const total = allReqs.length;
|
|
1647
|
+
const errors = allReqs.filter(r => r.phase === 'error' || (r.status && r.status >= 400)).length;
|
|
1648
|
+
const slow = completed.filter(r => r.duration >= 1000).length;
|
|
1649
|
+
const durations = completed.map(r => r.duration);
|
|
1650
|
+
const avg = durations.length ? Math.round(durations.reduce((a, b) => a + b, 0) / durations.length) : 0;
|
|
1651
|
+
const slowest = durations.length ? Math.max(...durations) : 0;
|
|
1652
|
+
const slowestReq = completed.find(r => r.duration === slowest);
|
|
1653
|
+
const slowestName = slowestReq ? (tryURL(slowestReq.url)?.pathname?.split('/').pop() || slowestReq.url?.split('/').pop() || '?') : '—';
|
|
1654
|
+
|
|
1655
|
+
const el = (id, text) => { const e = $(id); if (e) e.textContent = text; };
|
|
1656
|
+
el('netStatsTotal', `${total} requests`);
|
|
1657
|
+
el('netStatsAvg', `Avg: ${avg ? (avg > 999 ? `${(avg/1000).toFixed(1)}s` : `${avg}ms`) : '—'}`);
|
|
1658
|
+
el('netStatsSlowest', `Slowest: ${slowest ? (slowest > 999 ? `${(slowest/1000).toFixed(1)}s` : `${slowest}ms`) + ` (${slowestName})` : '—'}`);
|
|
1659
|
+
el('netStatsErrors', `Errors: ${errors}`);
|
|
1660
|
+
el('netStatsSlow', `Slow (>1s): ${slow}`);
|
|
1661
|
+
// Highlight if there are slow or errored requests
|
|
1662
|
+
if (slow > 0) $('netStatsSlow')?.classList.add('warn');
|
|
1663
|
+
else $('netStatsSlow')?.classList.remove('warn');
|
|
1664
|
+
if (errors > 0) $('netStatsErrors')?.classList.add('err');
|
|
1665
|
+
else $('netStatsErrors')?.classList.remove('err');
|
|
1322
1666
|
}
|
|
1323
1667
|
|
|
1324
1668
|
function _isHttpError(r) {
|
|
@@ -1327,7 +1671,9 @@ function _isHttpError(r) {
|
|
|
1327
1671
|
|
|
1328
1672
|
function buildNetRow(r, wfMin, wfRange) {
|
|
1329
1673
|
const row = document.createElement('div');
|
|
1330
|
-
|
|
1674
|
+
const rowSlow = !_isHttpError(r) && (r.duration || 0) >= 1000;
|
|
1675
|
+
const rowVerySlow = !_isHttpError(r) && (r.duration || 0) >= 3000;
|
|
1676
|
+
row.className = 'net-row' + (r.id === state.network.selectedId ? ' selected' : '') + (_isHttpError(r) ? ' error' : '') + (rowVerySlow ? ' very-slow' : rowSlow ? ' slow' : '');
|
|
1331
1677
|
row.dataset.id = r.id;
|
|
1332
1678
|
|
|
1333
1679
|
const urlObj = tryURL(r.url);
|
|
@@ -1394,7 +1740,9 @@ function buildNetRow(r, wfMin, wfRange) {
|
|
|
1394
1740
|
|
|
1395
1741
|
// Time
|
|
1396
1742
|
const timeCell = document.createElement('div');
|
|
1397
|
-
|
|
1743
|
+
const dur = r.duration || 0;
|
|
1744
|
+
const slowClass = dur >= 3000 ? ' very-slow' : dur >= 1000 ? ' slow' : '';
|
|
1745
|
+
timeCell.className = 'net-cell net-time' + slowClass;
|
|
1398
1746
|
timeCell.dataset.col = 'time';
|
|
1399
1747
|
timeCell.style.width = NET_COLS[5].width + 'px';
|
|
1400
1748
|
timeCell.textContent = r.duration != null ? (r.duration > 999 ? `${(r.duration/1000).toFixed(1)}s` : `${r.duration}ms`) : '...';
|
|
@@ -1589,6 +1937,12 @@ function showNetContextMenu(e, r) {
|
|
|
1589
1937
|
navigator.clipboard.writeText(text);
|
|
1590
1938
|
}});
|
|
1591
1939
|
}
|
|
1940
|
+
// Hide URL option
|
|
1941
|
+
items.push({ label: '—', action: null }); // separator
|
|
1942
|
+
items.push({ label: 'Hide this URL', action: () => {
|
|
1943
|
+
addHiddenURL(r.url || '');
|
|
1944
|
+
renderNetwork();
|
|
1945
|
+
}});
|
|
1592
1946
|
showContextMenu(e, items);
|
|
1593
1947
|
}
|
|
1594
1948
|
|
|
@@ -1634,7 +1988,12 @@ function initGA4Panel() {
|
|
|
1634
1988
|
<span class="panel-label">GA4 Events</span>
|
|
1635
1989
|
<span class="badge" id="ga4Badge">0</span>
|
|
1636
1990
|
<input id="ga4Search" class="net-search-input" style="margin-left:12px" placeholder="Filter events..." />
|
|
1637
|
-
<div class="ml-auto">
|
|
1991
|
+
<div class="ml-auto" style="display:flex;align-items:center;gap:6px">
|
|
1992
|
+
<label class="toggle-label" for="ga4ColorToggle" style="font-size:10px;gap:4px">
|
|
1993
|
+
<span style="color:var(--text-dim)">Colors</span>
|
|
1994
|
+
<input type="checkbox" id="ga4ColorToggle" class="toggle-input" ${getGA4ColorsEnabled() ? 'checked' : ''} />
|
|
1995
|
+
<span class="toggle-slider"></span>
|
|
1996
|
+
</label>
|
|
1638
1997
|
<button class="panel-clear-btn" id="ga4Clear" title="Clear GA4 events">Clear</button>
|
|
1639
1998
|
</div>
|
|
1640
1999
|
</div>
|
|
@@ -1670,6 +2029,12 @@ function initGA4Panel() {
|
|
|
1670
2029
|
renderGA4Summary(); // update active chip highlight
|
|
1671
2030
|
});
|
|
1672
2031
|
|
|
2032
|
+
$('ga4ColorToggle')?.addEventListener('change', (e) => {
|
|
2033
|
+
setGA4ColorsEnabled(e.target.checked);
|
|
2034
|
+
renderGA4List();
|
|
2035
|
+
renderGA4Summary();
|
|
2036
|
+
});
|
|
2037
|
+
|
|
1673
2038
|
$('ga4Clear').addEventListener('click', () => {
|
|
1674
2039
|
ga4State.events = [];
|
|
1675
2040
|
ga4State.selected = -1;
|
|
@@ -1709,6 +2074,7 @@ function initGA4Panel() {
|
|
|
1709
2074
|
}
|
|
1710
2075
|
|
|
1711
2076
|
function handleGA4Event(event) {
|
|
2077
|
+
if (!isTabEnabled('ga4')) return;
|
|
1712
2078
|
ga4State.events.push({
|
|
1713
2079
|
name: event.name || '?',
|
|
1714
2080
|
params: event.params || {},
|
|
@@ -1729,6 +2095,38 @@ function handleGA4Event(event) {
|
|
|
1729
2095
|
}
|
|
1730
2096
|
}
|
|
1731
2097
|
|
|
2098
|
+
// Assign consistent color to each GA4 event name
|
|
2099
|
+
const _ga4EventColors = {};
|
|
2100
|
+
const _ga4ColorPalette = [
|
|
2101
|
+
'#4facff', // blue
|
|
2102
|
+
'#3dd68c', // green
|
|
2103
|
+
'#ff813f', // orange
|
|
2104
|
+
'#c678dd', // purple
|
|
2105
|
+
'#e06c75', // coral
|
|
2106
|
+
'#56b6c2', // teal
|
|
2107
|
+
'#d19a66', // gold
|
|
2108
|
+
'#98c379', // lime
|
|
2109
|
+
'#e5c07b', // yellow
|
|
2110
|
+
'#ff5e72', // red
|
|
2111
|
+
'#61afef', // light blue
|
|
2112
|
+
'#be5046', // rust
|
|
2113
|
+
];
|
|
2114
|
+
let _ga4ColorIdx = 0;
|
|
2115
|
+
function _ga4EventColor(name) {
|
|
2116
|
+
if (!getGA4ColorsEnabled()) return ''; // empty = inherit default text color
|
|
2117
|
+
if (!_ga4EventColors[name]) {
|
|
2118
|
+
_ga4EventColors[name] = _ga4ColorPalette[_ga4ColorIdx % _ga4ColorPalette.length];
|
|
2119
|
+
_ga4ColorIdx++;
|
|
2120
|
+
}
|
|
2121
|
+
return _ga4EventColors[name];
|
|
2122
|
+
}
|
|
2123
|
+
function getGA4ColorsEnabled() {
|
|
2124
|
+
try { return localStorage.getItem('rn-debug-ga4-colors') === 'true'; } catch { return false; }
|
|
2125
|
+
}
|
|
2126
|
+
function setGA4ColorsEnabled(v) {
|
|
2127
|
+
try { localStorage.setItem('rn-debug-ga4-colors', v ? 'true' : 'false'); } catch {}
|
|
2128
|
+
}
|
|
2129
|
+
|
|
1732
2130
|
function renderGA4List() {
|
|
1733
2131
|
const list = $('ga4List');
|
|
1734
2132
|
const empty = $('ga4Empty');
|
|
@@ -1758,9 +2156,11 @@ function renderGA4List() {
|
|
|
1758
2156
|
|
|
1759
2157
|
const time = new Date(e.ts).toLocaleTimeString('en', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit', fractionalSecondDigits: 3 });
|
|
1760
2158
|
|
|
2159
|
+
const evtColor = _ga4EventColor(e.name);
|
|
2160
|
+
const colorStyle = evtColor ? `color:${evtColor}` : '';
|
|
1761
2161
|
row.innerHTML = `
|
|
1762
2162
|
<span class="ga4-cell ga4-time">${time}</span>
|
|
1763
|
-
<span class="ga4-cell ga4-name">${esc(e.name)}</span>`;
|
|
2163
|
+
<span class="ga4-cell ga4-name" style="${colorStyle}">${esc(e.name)}</span>`;
|
|
1764
2164
|
|
|
1765
2165
|
row.addEventListener('click', () => {
|
|
1766
2166
|
ga4State.selected = e.index;
|
|
@@ -1795,7 +2195,7 @@ function renderGA4Detail(e) {
|
|
|
1795
2195
|
const header = document.createElement('div');
|
|
1796
2196
|
header.className = 'ga4-detail-info';
|
|
1797
2197
|
header.innerHTML = `
|
|
1798
|
-
<div class="ga4-detail-row"><span class="ga4-detail-key">Event Name</span><span class="ga4-detail-val" style="color:
|
|
2198
|
+
<div class="ga4-detail-row"><span class="ga4-detail-key">Event Name</span><span class="ga4-detail-val" style="${_ga4EventColor(e.name) ? 'color:' + _ga4EventColor(e.name) + ';' : ''}font-weight:600;font-size:1.1em">${esc(e.name)}</span></div>
|
|
1799
2199
|
<div class="ga4-detail-row"><span class="ga4-detail-key">Timestamp</span><span class="ga4-detail-val">${time}</span></div>
|
|
1800
2200
|
`;
|
|
1801
2201
|
detail.appendChild(header);
|
|
@@ -1869,8 +2269,15 @@ function renderGA4Summary() {
|
|
|
1869
2269
|
sorted.forEach(([name, count]) => {
|
|
1870
2270
|
const chip = document.createElement('span');
|
|
1871
2271
|
const isActive = ga4State.searchFilter === name.toLowerCase();
|
|
2272
|
+
const chipColor = _ga4EventColor(name);
|
|
1872
2273
|
chip.className = 'ga4-summary-chip' + (isActive ? ' active' : '');
|
|
1873
|
-
|
|
2274
|
+
if (chipColor) {
|
|
2275
|
+
chip.style.borderColor = chipColor;
|
|
2276
|
+
if (isActive) chip.style.background = chipColor + '22';
|
|
2277
|
+
chip.innerHTML = `<b style="color:${chipColor}">${esc(name)}</b><span class="chip-count">${count}</span>`;
|
|
2278
|
+
} else {
|
|
2279
|
+
chip.innerHTML = `<b>${esc(name)}</b><span class="chip-count">${count}</span>`;
|
|
2280
|
+
}
|
|
1874
2281
|
chip.addEventListener('click', () => {
|
|
1875
2282
|
const search = $('ga4Search');
|
|
1876
2283
|
if (isActive) {
|
|
@@ -2051,9 +2458,8 @@ function _createHighlightedTree(key, val, changedPaths, currentPath, isOld) {
|
|
|
2051
2458
|
|
|
2052
2459
|
const children = document.createElement('div');
|
|
2053
2460
|
children.className = 'ov-children';
|
|
2054
|
-
//
|
|
2055
|
-
children.style.display =
|
|
2056
|
-
if (hasChangedDescendant) { arrow.textContent = '\u25BC'; arrow.classList.add('open'); }
|
|
2461
|
+
// Always start collapsed — user expands what they need
|
|
2462
|
+
children.style.display = 'none';
|
|
2057
2463
|
|
|
2058
2464
|
let populated = false;
|
|
2059
2465
|
function populate() {
|
|
@@ -2065,9 +2471,6 @@ function _createHighlightedTree(key, val, changedPaths, currentPath, isOld) {
|
|
|
2065
2471
|
});
|
|
2066
2472
|
}
|
|
2067
2473
|
|
|
2068
|
-
// Populate immediately if expanded, otherwise lazy
|
|
2069
|
-
if (hasChangedDescendant) populate();
|
|
2070
|
-
|
|
2071
2474
|
header.addEventListener('click', (e) => {
|
|
2072
2475
|
e.stopPropagation();
|
|
2073
2476
|
const open = children.style.display !== 'none';
|
|
@@ -2082,6 +2485,8 @@ function _createHighlightedTree(key, val, changedPaths, currentPath, isOld) {
|
|
|
2082
2485
|
|
|
2083
2486
|
function handleReduxEvent(event) {
|
|
2084
2487
|
if (event.type !== 'redux') return;
|
|
2488
|
+
// Skip processing if Redux tab is disabled (saves memory)
|
|
2489
|
+
if (!isTabEnabled('redux')) return;
|
|
2085
2490
|
const { action, nextState } = event;
|
|
2086
2491
|
const idx = state.redux.actions.length;
|
|
2087
2492
|
|
|
@@ -2095,6 +2500,16 @@ function handleReduxEvent(event) {
|
|
|
2095
2500
|
const actionEntry = { type: action?.type || '?', payload: action, ts: event.ts, index: idx, changedKeys };
|
|
2096
2501
|
state.redux.actions.push(actionEntry);
|
|
2097
2502
|
state.redux.states.push(nextState);
|
|
2503
|
+
// Cap Redux history to prevent memory leak (full state stored per action)
|
|
2504
|
+
const MAX_REDUX_HISTORY = 500;
|
|
2505
|
+
if (state.redux.actions.length > MAX_REDUX_HISTORY) {
|
|
2506
|
+
const trim = state.redux.actions.length - MAX_REDUX_HISTORY;
|
|
2507
|
+
state.redux.actions.splice(0, trim);
|
|
2508
|
+
state.redux.states.splice(0, trim);
|
|
2509
|
+
// Re-index remaining actions
|
|
2510
|
+
state.redux.actions.forEach((a, i) => a.index = i);
|
|
2511
|
+
if (state.redux.selected >= 0) state.redux.selected = Math.max(0, state.redux.selected - trim);
|
|
2512
|
+
}
|
|
2098
2513
|
// Don't auto-select — keep all collapsed until user clicks
|
|
2099
2514
|
$('rBadge').textContent = state.redux.actions.length;
|
|
2100
2515
|
renderRedux();
|
|
@@ -2171,7 +2586,7 @@ function renderRedux() {
|
|
|
2171
2586
|
} else {
|
|
2172
2587
|
typeHtml = `<span class="rdx-type">${esc(a.type)}</span>`;
|
|
2173
2588
|
}
|
|
2174
|
-
header.innerHTML = `<span class="rdx-index">#${a.index}</span>${typeHtml}
|
|
2589
|
+
header.innerHTML = `<span class="rdx-index">#${a.index}</span>${typeHtml}<span class="rdx-header-right">${changesBadge}<span class="rdx-time">${ts(a.ts)}</span></span>`;
|
|
2175
2590
|
// Toggle: click to expand, click again to collapse
|
|
2176
2591
|
header.addEventListener('click', () => {
|
|
2177
2592
|
state.redux.selected = isSelected ? -1 : a.index;
|
|
@@ -2195,6 +2610,18 @@ function renderRedux() {
|
|
|
2195
2610
|
const detail = document.createElement('div');
|
|
2196
2611
|
detail.className = 'rdx-entry-detail';
|
|
2197
2612
|
|
|
2613
|
+
// Close button
|
|
2614
|
+
const closeBtn = document.createElement('button');
|
|
2615
|
+
closeBtn.className = 'rdx-close-btn';
|
|
2616
|
+
closeBtn.textContent = '✕';
|
|
2617
|
+
closeBtn.title = 'Close';
|
|
2618
|
+
closeBtn.addEventListener('click', (e) => {
|
|
2619
|
+
e.stopPropagation();
|
|
2620
|
+
state.redux.selected = -1;
|
|
2621
|
+
renderRedux();
|
|
2622
|
+
});
|
|
2623
|
+
detail.appendChild(closeBtn);
|
|
2624
|
+
|
|
2198
2625
|
// Changed keys badges
|
|
2199
2626
|
if (a.changedKeys?.length > 0) {
|
|
2200
2627
|
const keysEl = document.createElement('div');
|
|
@@ -2213,8 +2640,7 @@ function renderRedux() {
|
|
|
2213
2640
|
detail.appendChild(createTreeNode(null, a.payload, false));
|
|
2214
2641
|
}
|
|
2215
2642
|
|
|
2216
|
-
// Store changes —
|
|
2217
|
-
// with changed sub-keys highlighted
|
|
2643
|
+
// Store changes — two-column layout: Previous | Current
|
|
2218
2644
|
const prevS = a.index > 0 ? states[a.index - 1] : null;
|
|
2219
2645
|
const currS = states[a.index];
|
|
2220
2646
|
if (currS && typeof currS === 'object' && a.changedKeys?.length > 0) {
|
|
@@ -2234,30 +2660,63 @@ function renderRedux() {
|
|
|
2234
2660
|
const changedPaths = new Set();
|
|
2235
2661
|
_findLeafChanges(oldVal, newVal, '').forEach(c => changedPaths.add(c.path));
|
|
2236
2662
|
|
|
2237
|
-
// Previous
|
|
2663
|
+
// Two-column grid: Previous | Current
|
|
2664
|
+
const grid = document.createElement('div');
|
|
2665
|
+
grid.className = 'rdx-diff-grid';
|
|
2666
|
+
|
|
2667
|
+
// Previous column
|
|
2668
|
+
const prevCol = document.createElement('div');
|
|
2669
|
+
prevCol.className = 'rdx-diff-col prev';
|
|
2670
|
+
const prevLabel = document.createElement('div');
|
|
2671
|
+
prevLabel.className = 'rdx-state-label prev';
|
|
2672
|
+
prevLabel.textContent = '- Previous';
|
|
2673
|
+
prevCol.appendChild(prevLabel);
|
|
2238
2674
|
if (oldVal !== undefined) {
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
prevTree.appendChild(_createHighlightedTree(null, oldVal, changedPaths, '', true));
|
|
2246
|
-
keyWrap.appendChild(prevTree);
|
|
2675
|
+
prevCol.appendChild(_createHighlightedTree(null, oldVal, changedPaths, '', true));
|
|
2676
|
+
} else {
|
|
2677
|
+
const na = document.createElement('span');
|
|
2678
|
+
na.style.cssText = 'color:var(--text-dim);font-size:10px;font-style:italic';
|
|
2679
|
+
na.textContent = 'undefined';
|
|
2680
|
+
prevCol.appendChild(na);
|
|
2247
2681
|
}
|
|
2248
|
-
|
|
2249
|
-
|
|
2682
|
+
grid.appendChild(prevCol);
|
|
2683
|
+
|
|
2684
|
+
// Current column
|
|
2685
|
+
const currCol = document.createElement('div');
|
|
2686
|
+
currCol.className = 'rdx-diff-col curr';
|
|
2687
|
+
const currLabel = document.createElement('div');
|
|
2688
|
+
currLabel.className = 'rdx-state-label curr';
|
|
2689
|
+
currLabel.textContent = '+ Current';
|
|
2690
|
+
currCol.appendChild(currLabel);
|
|
2250
2691
|
if (newVal !== undefined) {
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
currTree.appendChild(_createHighlightedTree(null, newVal, changedPaths, '', false));
|
|
2258
|
-
keyWrap.appendChild(currTree);
|
|
2692
|
+
currCol.appendChild(_createHighlightedTree(null, newVal, changedPaths, '', false));
|
|
2693
|
+
} else {
|
|
2694
|
+
const na = document.createElement('span');
|
|
2695
|
+
na.style.cssText = 'color:var(--text-dim);font-size:10px;font-style:italic';
|
|
2696
|
+
na.textContent = 'undefined';
|
|
2697
|
+
currCol.appendChild(na);
|
|
2259
2698
|
}
|
|
2260
|
-
|
|
2699
|
+
grid.appendChild(currCol);
|
|
2700
|
+
|
|
2701
|
+
// Right-click to copy on each column
|
|
2702
|
+
prevCol.addEventListener('contextmenu', (e) => {
|
|
2703
|
+
e.preventDefault(); e.stopPropagation();
|
|
2704
|
+
showContextMenu(e, [
|
|
2705
|
+
{ label: 'Copy Previous Value', action: () => navigator.clipboard.writeText(JSON.stringify(oldVal, null, 2)) },
|
|
2706
|
+
{ label: 'Copy Current Value', action: () => navigator.clipboard.writeText(JSON.stringify(newVal, null, 2)) },
|
|
2707
|
+
{ label: `Copy "${key}" key`, action: () => navigator.clipboard.writeText(key) },
|
|
2708
|
+
]);
|
|
2709
|
+
});
|
|
2710
|
+
currCol.addEventListener('contextmenu', (e) => {
|
|
2711
|
+
e.preventDefault(); e.stopPropagation();
|
|
2712
|
+
showContextMenu(e, [
|
|
2713
|
+
{ label: 'Copy Current Value', action: () => navigator.clipboard.writeText(JSON.stringify(newVal, null, 2)) },
|
|
2714
|
+
{ label: 'Copy Previous Value', action: () => navigator.clipboard.writeText(JSON.stringify(oldVal, null, 2)) },
|
|
2715
|
+
{ label: `Copy "${key}" key`, action: () => navigator.clipboard.writeText(key) },
|
|
2716
|
+
]);
|
|
2717
|
+
});
|
|
2718
|
+
|
|
2719
|
+
keyWrap.appendChild(grid);
|
|
2261
2720
|
detail.appendChild(keyWrap);
|
|
2262
2721
|
});
|
|
2263
2722
|
}
|
|
@@ -2323,6 +2782,7 @@ let _storageRAF = null;
|
|
|
2323
2782
|
|
|
2324
2783
|
function handleStorageEvent(event) {
|
|
2325
2784
|
if (event.type !== 'storage') return;
|
|
2785
|
+
if (!isTabEnabled('storage')) return;
|
|
2326
2786
|
const { key, value, action } = event;
|
|
2327
2787
|
if (action === 'set' || action === 'snapshot') {
|
|
2328
2788
|
if (action === 'snapshot' && typeof key === 'object') {
|
|
@@ -2449,6 +2909,226 @@ function setStoredFontSize(s) {
|
|
|
2449
2909
|
try { localStorage.setItem('rn-debug-fontsize', String(s)); } catch {}
|
|
2450
2910
|
}
|
|
2451
2911
|
|
|
2912
|
+
const FONT_FAMILIES = [
|
|
2913
|
+
{ label: 'SF Mono', value: "'SFMono-Regular', 'SF Mono', monospace" },
|
|
2914
|
+
{ label: 'Menlo', value: "Menlo, monospace" },
|
|
2915
|
+
{ label: 'Monaco', value: "Monaco, monospace" },
|
|
2916
|
+
{ label: 'Courier New', value: "'Courier New', Courier, monospace" },
|
|
2917
|
+
{ label: 'System Mono', value: "monospace" },
|
|
2918
|
+
];
|
|
2919
|
+
function getStoredFontFamily() {
|
|
2920
|
+
try {
|
|
2921
|
+
const saved = localStorage.getItem('rn-debug-fontfamily');
|
|
2922
|
+
// Reset if saved value was a removed font
|
|
2923
|
+
if (saved && !FONT_FAMILIES.some(f => f.value === saved)) return FONT_FAMILIES[0].value;
|
|
2924
|
+
return saved || FONT_FAMILIES[0].value;
|
|
2925
|
+
} catch { return FONT_FAMILIES[0].value; }
|
|
2926
|
+
}
|
|
2927
|
+
function setStoredFontFamily(f) {
|
|
2928
|
+
try { localStorage.setItem('rn-debug-fontfamily', f); } catch {}
|
|
2929
|
+
}
|
|
2930
|
+
function applyFontFamily(family) {
|
|
2931
|
+
document.body.style.fontFamily = family;
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
// ─── Hidden URLs (Network tab) ───────────────────────────────────────────────
|
|
2935
|
+
function getHiddenURLs() {
|
|
2936
|
+
try { return JSON.parse(localStorage.getItem('rn-debug-hidden-urls') || '[]'); } catch { return []; }
|
|
2937
|
+
}
|
|
2938
|
+
function setHiddenURLs(list) {
|
|
2939
|
+
try { localStorage.setItem('rn-debug-hidden-urls', JSON.stringify(list)); } catch {}
|
|
2940
|
+
}
|
|
2941
|
+
function addHiddenURL(url) {
|
|
2942
|
+
// Extract the base URL (without query params) as the pattern
|
|
2943
|
+
const pattern = url.split('?')[0];
|
|
2944
|
+
const list = getHiddenURLs();
|
|
2945
|
+
if (!list.includes(pattern)) {
|
|
2946
|
+
list.push(pattern);
|
|
2947
|
+
setHiddenURLs(list);
|
|
2948
|
+
}
|
|
2949
|
+
_updateHiddenBadge();
|
|
2950
|
+
}
|
|
2951
|
+
function removeHiddenURL(pattern) {
|
|
2952
|
+
const list = getHiddenURLs().filter(u => u !== pattern);
|
|
2953
|
+
setHiddenURLs(list);
|
|
2954
|
+
_updateHiddenBadge();
|
|
2955
|
+
}
|
|
2956
|
+
function isURLHidden(url) {
|
|
2957
|
+
const hidden = getHiddenURLs();
|
|
2958
|
+
if (!hidden.length) return false;
|
|
2959
|
+
const base = url.split('?')[0];
|
|
2960
|
+
return hidden.some(pattern => base === pattern || base.startsWith(pattern));
|
|
2961
|
+
}
|
|
2962
|
+
function _updateHiddenBadge() {
|
|
2963
|
+
const btn = $('netHiddenBtn');
|
|
2964
|
+
if (!btn) return;
|
|
2965
|
+
const count = getHiddenURLs().length;
|
|
2966
|
+
btn.textContent = count > 0 ? `Hidden (${count})` : 'Hidden';
|
|
2967
|
+
btn.style.display = count > 0 ? '' : 'none';
|
|
2968
|
+
}
|
|
2969
|
+
|
|
2970
|
+
// ─── Tab Visibility ──────────────────────────────────────────────────────────
|
|
2971
|
+
const TAB_CONFIG = [
|
|
2972
|
+
{ id: 'console', label: 'Console', icon: '🖥', essential: true },
|
|
2973
|
+
{ id: 'network', label: 'Network', icon: '📡', essential: true },
|
|
2974
|
+
{ id: 'redux', label: 'Redux', icon: '🔲', essential: false },
|
|
2975
|
+
{ id: 'ga4', label: 'GA4 Events', icon: '📊', essential: false },
|
|
2976
|
+
{ id: 'storage', label: 'Storage', icon: '💾', essential: false },
|
|
2977
|
+
{ id: 'memory', label: 'Memory', icon: '🧠', essential: false },
|
|
2978
|
+
{ id: 'performance', label: 'Performance', icon: '⚡', essential: false },
|
|
2979
|
+
{ id: 'react', label: 'React Tree', icon: '⚛️', essential: false },
|
|
2980
|
+
];
|
|
2981
|
+
function getTabVisibility() {
|
|
2982
|
+
try {
|
|
2983
|
+
const saved = JSON.parse(localStorage.getItem('rn-debug-tab-visibility') || '{}');
|
|
2984
|
+
const result = {};
|
|
2985
|
+
TAB_CONFIG.forEach(t => { result[t.id] = saved[t.id] !== undefined ? saved[t.id] : true; });
|
|
2986
|
+
return result;
|
|
2987
|
+
} catch {
|
|
2988
|
+
const result = {};
|
|
2989
|
+
TAB_CONFIG.forEach(t => { result[t.id] = true; });
|
|
2990
|
+
return result;
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
function setTabVisibility(vis) {
|
|
2994
|
+
try { localStorage.setItem('rn-debug-tab-visibility', JSON.stringify(vis)); } catch {}
|
|
2995
|
+
}
|
|
2996
|
+
function getTabOrder() {
|
|
2997
|
+
try {
|
|
2998
|
+
const saved = JSON.parse(localStorage.getItem('rn-debug-tab-order') || '[]');
|
|
2999
|
+
if (saved.length === TAB_CONFIG.length) return saved;
|
|
3000
|
+
} catch {}
|
|
3001
|
+
return TAB_CONFIG.map(t => t.id);
|
|
3002
|
+
}
|
|
3003
|
+
function setTabOrder(order) {
|
|
3004
|
+
try { localStorage.setItem('rn-debug-tab-order', JSON.stringify(order)); } catch {}
|
|
3005
|
+
}
|
|
3006
|
+
function applyTabVisibility() {
|
|
3007
|
+
const vis = getTabVisibility();
|
|
3008
|
+
const order = getTabOrder();
|
|
3009
|
+
const nav = $('sidebar');
|
|
3010
|
+
if (!nav) return;
|
|
3011
|
+
// Reorder nav buttons according to saved order + hide disabled ones
|
|
3012
|
+
// Settings button always stays last
|
|
3013
|
+
const settingsBtn = nav.querySelector('.nav-btn[data-panel="settings"]');
|
|
3014
|
+
const spacer = nav.querySelector('.nav-spacer');
|
|
3015
|
+
const anchor = spacer || settingsBtn; // insert before spacer or settings
|
|
3016
|
+
order.forEach(tabId => {
|
|
3017
|
+
const btn = nav.querySelector(`.nav-btn[data-panel="${tabId}"]`);
|
|
3018
|
+
if (btn) {
|
|
3019
|
+
btn.style.display = vis[tabId] ? '' : 'none';
|
|
3020
|
+
nav.insertBefore(btn, anchor);
|
|
3021
|
+
}
|
|
3022
|
+
});
|
|
3023
|
+
// If active panel is now hidden, switch to first visible
|
|
3024
|
+
if (!vis[state.activePanel]) {
|
|
3025
|
+
const first = order.find(id => vis[id]);
|
|
3026
|
+
if (first) switchPanel(first);
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
function isTabEnabled(tabId) {
|
|
3030
|
+
return getTabVisibility()[tabId] !== false;
|
|
3031
|
+
}
|
|
3032
|
+
|
|
3033
|
+
function _buildTabVisGrid() {
|
|
3034
|
+
const container = $('tabVisibilityGrid');
|
|
3035
|
+
if (!container) return;
|
|
3036
|
+
container.innerHTML = '';
|
|
3037
|
+
const vis = getTabVisibility();
|
|
3038
|
+
const order = getTabOrder();
|
|
3039
|
+
let dragSrc = null;
|
|
3040
|
+
|
|
3041
|
+
order.forEach(tabId => {
|
|
3042
|
+
const t = TAB_CONFIG.find(c => c.id === tabId);
|
|
3043
|
+
if (!t) return;
|
|
3044
|
+
|
|
3045
|
+
const item = document.createElement('div');
|
|
3046
|
+
item.className = `tab-vis-item ${vis[t.id] ? 'active' : 'inactive'}`;
|
|
3047
|
+
item.dataset.tab = t.id;
|
|
3048
|
+
item.draggable = true;
|
|
3049
|
+
|
|
3050
|
+
// Drag handle
|
|
3051
|
+
const drag = document.createElement('span');
|
|
3052
|
+
drag.className = 'tab-vis-drag';
|
|
3053
|
+
drag.textContent = '⠿';
|
|
3054
|
+
item.appendChild(drag);
|
|
3055
|
+
|
|
3056
|
+
// Checkbox
|
|
3057
|
+
const check = document.createElement('input');
|
|
3058
|
+
check.type = 'checkbox';
|
|
3059
|
+
check.className = 'tab-vis-check';
|
|
3060
|
+
check.checked = vis[t.id];
|
|
3061
|
+
if (t.essential) check.disabled = true;
|
|
3062
|
+
check.addEventListener('change', () => {
|
|
3063
|
+
const v = getTabVisibility();
|
|
3064
|
+
v[t.id] = check.checked;
|
|
3065
|
+
setTabVisibility(v);
|
|
3066
|
+
applyTabVisibility();
|
|
3067
|
+
item.classList.toggle('active', check.checked);
|
|
3068
|
+
item.classList.toggle('inactive', !check.checked);
|
|
3069
|
+
});
|
|
3070
|
+
item.appendChild(check);
|
|
3071
|
+
|
|
3072
|
+
// Icon + label
|
|
3073
|
+
const icon = document.createElement('span');
|
|
3074
|
+
icon.className = 'tab-vis-icon';
|
|
3075
|
+
icon.textContent = t.icon;
|
|
3076
|
+
item.appendChild(icon);
|
|
3077
|
+
|
|
3078
|
+
const label = document.createElement('span');
|
|
3079
|
+
label.className = 'tab-vis-label';
|
|
3080
|
+
label.textContent = t.label;
|
|
3081
|
+
item.appendChild(label);
|
|
3082
|
+
|
|
3083
|
+
if (t.essential) {
|
|
3084
|
+
const req = document.createElement('span');
|
|
3085
|
+
req.className = 'tab-vis-required';
|
|
3086
|
+
req.textContent = 'Required';
|
|
3087
|
+
item.appendChild(req);
|
|
3088
|
+
}
|
|
3089
|
+
|
|
3090
|
+
// Drag events
|
|
3091
|
+
item.addEventListener('dragstart', (e) => {
|
|
3092
|
+
dragSrc = item;
|
|
3093
|
+
item.classList.add('dragging');
|
|
3094
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
3095
|
+
});
|
|
3096
|
+
item.addEventListener('dragend', () => {
|
|
3097
|
+
item.classList.remove('dragging');
|
|
3098
|
+
container.querySelectorAll('.tab-vis-item').forEach(el => el.classList.remove('drag-over'));
|
|
3099
|
+
dragSrc = null;
|
|
3100
|
+
});
|
|
3101
|
+
item.addEventListener('dragover', (e) => {
|
|
3102
|
+
e.preventDefault();
|
|
3103
|
+
e.dataTransfer.dropEffect = 'move';
|
|
3104
|
+
if (dragSrc && dragSrc !== item) item.classList.add('drag-over');
|
|
3105
|
+
});
|
|
3106
|
+
item.addEventListener('dragleave', () => {
|
|
3107
|
+
item.classList.remove('drag-over');
|
|
3108
|
+
});
|
|
3109
|
+
item.addEventListener('drop', (e) => {
|
|
3110
|
+
e.preventDefault();
|
|
3111
|
+
item.classList.remove('drag-over');
|
|
3112
|
+
if (!dragSrc || dragSrc === item) return;
|
|
3113
|
+
// Reorder: move dragSrc before or after this item
|
|
3114
|
+
const items = [...container.querySelectorAll('.tab-vis-item')];
|
|
3115
|
+
const fromIdx = items.indexOf(dragSrc);
|
|
3116
|
+
const toIdx = items.indexOf(item);
|
|
3117
|
+
if (fromIdx < toIdx) {
|
|
3118
|
+
container.insertBefore(dragSrc, item.nextSibling);
|
|
3119
|
+
} else {
|
|
3120
|
+
container.insertBefore(dragSrc, item);
|
|
3121
|
+
}
|
|
3122
|
+
// Save new order
|
|
3123
|
+
const newOrder = [...container.querySelectorAll('.tab-vis-item')].map(el => el.dataset.tab);
|
|
3124
|
+
setTabOrder(newOrder);
|
|
3125
|
+
applyTabVisibility();
|
|
3126
|
+
});
|
|
3127
|
+
|
|
3128
|
+
container.appendChild(item);
|
|
3129
|
+
});
|
|
3130
|
+
}
|
|
3131
|
+
|
|
2452
3132
|
function getStoredAppName() {
|
|
2453
3133
|
try { return localStorage.getItem('rn-debug-appname') || 'ReactoRadar'; } catch { return 'ReactoRadar'; }
|
|
2454
3134
|
}
|
|
@@ -2513,108 +3193,119 @@ function initSettingsPanel() {
|
|
|
2513
3193
|
const panel = $('panel-settings');
|
|
2514
3194
|
const current = getStoredTheme();
|
|
2515
3195
|
const currentSize = getStoredFontSize();
|
|
2516
|
-
|
|
3196
|
+
panel.innerHTML = `
|
|
2517
3197
|
<div class="panel-toolbar">
|
|
2518
3198
|
<span class="panel-label">Settings</span>
|
|
2519
3199
|
</div>
|
|
2520
3200
|
<div class="scroll-area">
|
|
2521
|
-
<div class="settings-
|
|
2522
|
-
<div class="settings-
|
|
2523
|
-
<div class="settings-section
|
|
2524
|
-
|
|
2525
|
-
<div>
|
|
2526
|
-
<div
|
|
2527
|
-
|
|
3201
|
+
<div class="settings-two-col">
|
|
3202
|
+
<div class="settings-col-left">
|
|
3203
|
+
<div class="settings-section">
|
|
3204
|
+
<div class="settings-section-title">Appearance</div>
|
|
3205
|
+
<div class="settings-row" style="flex-direction:column;align-items:flex-start;gap:8px">
|
|
3206
|
+
<div>
|
|
3207
|
+
<div class="settings-label">Theme</div>
|
|
3208
|
+
<div class="settings-hint">Choose a color theme</div>
|
|
3209
|
+
</div>
|
|
3210
|
+
<div class="theme-grid" id="themeSwitcher"></div>
|
|
2528
3211
|
</div>
|
|
2529
|
-
<div class="
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
<div class="
|
|
3212
|
+
<div class="settings-row">
|
|
3213
|
+
<div>
|
|
3214
|
+
<div class="settings-label">Font Size</div>
|
|
3215
|
+
<div class="settings-hint">Adjust text size</div>
|
|
3216
|
+
</div>
|
|
3217
|
+
<div class="font-size-control">
|
|
3218
|
+
<button class="font-size-btn" id="fontSizeDown">A-</button>
|
|
3219
|
+
<span class="font-size-display" id="fontSizeDisplay">${currentSize}px</span>
|
|
3220
|
+
<button class="font-size-btn" id="fontSizeUp">A+</button>
|
|
3221
|
+
</div>
|
|
2535
3222
|
</div>
|
|
2536
|
-
<div class="
|
|
2537
|
-
<
|
|
2538
|
-
|
|
2539
|
-
|
|
3223
|
+
<div class="settings-row">
|
|
3224
|
+
<div>
|
|
3225
|
+
<div class="settings-label">Font Family</div>
|
|
3226
|
+
</div>
|
|
3227
|
+
<select id="fontFamilySelect" class="net-throttle-select" style="width:150px">
|
|
3228
|
+
${FONT_FAMILIES.map(f => `<option value="${esc(f.value)}" ${f.value === getStoredFontFamily() ? 'selected' : ''}>${esc(f.label)}</option>`).join('')}
|
|
3229
|
+
</select>
|
|
2540
3230
|
</div>
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
<div
|
|
3231
|
+
<div class="settings-row">
|
|
3232
|
+
<div>
|
|
3233
|
+
<div class="settings-label">App Name</div>
|
|
3234
|
+
</div>
|
|
3235
|
+
<div style="display:flex;align-items:center;gap:6px">
|
|
3236
|
+
<input id="appNameInput" class="net-search-input" style="width:120px;text-align:center" value="${getStoredAppName()}" />
|
|
3237
|
+
<button class="font-size-btn" id="appNameReset" title="Reset">Reset</button>
|
|
3238
|
+
</div>
|
|
2546
3239
|
</div>
|
|
2547
|
-
<div
|
|
2548
|
-
<
|
|
2549
|
-
|
|
3240
|
+
<div class="settings-row">
|
|
3241
|
+
<div>
|
|
3242
|
+
<div class="settings-label">Toast Notifications</div>
|
|
3243
|
+
<div class="settings-hint">Show alerts for API errors and slow requests</div>
|
|
3244
|
+
</div>
|
|
3245
|
+
<label class="toggle-label" for="toastToggle">
|
|
3246
|
+
<input type="checkbox" id="toastToggle" class="toggle-input" ${getToastsEnabled() ? 'checked' : ''} />
|
|
3247
|
+
<span class="toggle-slider"></span>
|
|
3248
|
+
</label>
|
|
2550
3249
|
</div>
|
|
2551
3250
|
</div>
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
3251
|
+
<div class="settings-section">
|
|
3252
|
+
<div class="settings-section-title">Connection</div>
|
|
3253
|
+
<div class="settings-row">
|
|
3254
|
+
<div>
|
|
3255
|
+
<div class="settings-label">Bridge Ports</div>
|
|
3256
|
+
<div class="settings-hint">Redux :9090 · Storage :9091 · Network :9092</div>
|
|
3257
|
+
</div>
|
|
3258
|
+
</div>
|
|
3259
|
+
<div class="settings-row">
|
|
3260
|
+
<div>
|
|
3261
|
+
<div class="settings-label">Metro Port</div>
|
|
3262
|
+
</div>
|
|
3263
|
+
<input id="metroPortInput" type="number" class="net-search-input" style="width:70px;text-align:center" value="${getStoredMetroPort()}" />
|
|
2559
3264
|
</div>
|
|
2560
3265
|
</div>
|
|
2561
|
-
<div class="settings-
|
|
2562
|
-
<div
|
|
2563
|
-
|
|
2564
|
-
<div class="
|
|
3266
|
+
<div class="settings-section">
|
|
3267
|
+
<div class="settings-section-title">About</div>
|
|
3268
|
+
<div class="settings-about">
|
|
3269
|
+
<div class="about-name" id="aboutAppName">${getStoredAppName()}</div>
|
|
3270
|
+
<div class="about-version" id="aboutVersion">v${state._appVersion || '...'}</div>
|
|
3271
|
+
<div class="about-desc">Standalone macOS debugger for React Native.<br/>Supports Hermes, New Arch, and RN 0.74+.</div>
|
|
3272
|
+
<div class="about-links" style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap">
|
|
3273
|
+
<span class="about-link" id="linkGithub">GitHub</span>
|
|
3274
|
+
<span class="about-link" id="linkDocs">Docs</span>
|
|
3275
|
+
<span class="about-link" id="linkLinkedIn">LinkedIn</span>
|
|
3276
|
+
</div>
|
|
3277
|
+
<div style="margin-top:12px;text-align:center">
|
|
3278
|
+
<button class="support-btn" id="linkSupport" title="Support ReactoRadar development">☕ Support this project</button>
|
|
3279
|
+
</div>
|
|
2565
3280
|
</div>
|
|
2566
|
-
<input id="metroPortInput" type="number" class="net-search-input" style="width:70px;text-align:center" value="${getStoredMetroPort()}" />
|
|
2567
3281
|
</div>
|
|
2568
3282
|
</div>
|
|
2569
|
-
<div class="settings-
|
|
2570
|
-
<div class="settings-section
|
|
2571
|
-
|
|
2572
|
-
<div class="settings-
|
|
2573
|
-
<div class="
|
|
2574
|
-
</div>
|
|
2575
|
-
<div class="settings-row">
|
|
2576
|
-
<div class="settings-label">Clear All</div>
|
|
2577
|
-
<div class="settings-hint" style="font-size:11px;color:var(--text-mid)">⌘K</div>
|
|
2578
|
-
</div>
|
|
2579
|
-
<div class="settings-row">
|
|
2580
|
-
<div class="settings-label">Open JS Debugger</div>
|
|
2581
|
-
<div class="settings-hint" style="font-size:11px;color:var(--text-mid)">⌘D</div>
|
|
3283
|
+
<div class="settings-col-right">
|
|
3284
|
+
<div class="settings-section">
|
|
3285
|
+
<div class="settings-section-title">Panels</div>
|
|
3286
|
+
<div class="settings-hint" style="margin-bottom:8px">Show/hide tabs and drag to reorder. Disabled tabs save memory.</div>
|
|
3287
|
+
<div class="tab-visibility-grid" id="tabVisibilityGrid"></div>
|
|
2582
3288
|
</div>
|
|
2583
|
-
<div class="settings-
|
|
2584
|
-
<div class="settings-
|
|
2585
|
-
<div class="settings-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
<div class="settings-hint" style="font-size:11px;color:var(--text-mid)">⌘+ / ⌘-</div>
|
|
2594
|
-
</div>
|
|
2595
|
-
</div>
|
|
2596
|
-
<div class="settings-section">
|
|
2597
|
-
<div class="settings-section-title">How to Use</div>
|
|
2598
|
-
<div class="settings-row" style="flex-direction:column;align-items:flex-start;gap:8px">
|
|
2599
|
-
<div class="settings-hint" style="line-height:1.8">
|
|
2600
|
-
<b style="color:var(--text)">1. Setup</b> — Run <code style="color:var(--accent);background:var(--bg3);padding:1px 5px;border-radius:3px">npx reactoradar setup</code> from your RN project<br/>
|
|
2601
|
-
<b style="color:var(--text)">2. Start</b> — Run <code style="color:var(--accent);background:var(--bg3);padding:1px 5px;border-radius:3px">npx reactoradar</code> or open ReactoRadar.app<br/>
|
|
2602
|
-
<b style="color:var(--text)">3. Run your app</b> — <code style="color:var(--accent);background:var(--bg3);padding:1px 5px;border-radius:3px">npx react-native start --reset-cache</code><br/>
|
|
2603
|
-
<b style="color:var(--text)">4. Debug</b> — Console, Network, Redux data flows automatically<br/>
|
|
2604
|
-
<b style="color:var(--text)">5. Remove</b> — Run <code style="color:var(--accent);background:var(--bg3);padding:1px 5px;border-radius:3px">npx reactoradar remove</code> to clean uninstall
|
|
3289
|
+
<div class="settings-section">
|
|
3290
|
+
<div class="settings-section-title">Keyboard Shortcuts</div>
|
|
3291
|
+
<div class="settings-shortcut-grid">
|
|
3292
|
+
<span class="sc-key">⌘K</span><span class="sc-label">Clear All</span>
|
|
3293
|
+
<span class="sc-key">⌘D</span><span class="sc-label">JS Debugger</span>
|
|
3294
|
+
<span class="sc-key">⌘R</span><span class="sc-label">React DevTools</span>
|
|
3295
|
+
<span class="sc-key">⌘⇧T</span><span class="sc-label">Toggle Theme</span>
|
|
3296
|
+
<span class="sc-key">⌘F</span><span class="sc-label">Find</span>
|
|
3297
|
+
<span class="sc-key">⌘1–9</span><span class="sc-label">Switch Panels</span>
|
|
3298
|
+
<span class="sc-key">⌘+/−</span><span class="sc-label">Zoom</span>
|
|
2605
3299
|
</div>
|
|
2606
3300
|
</div>
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
<span class="about-link" id="linkGithub">GitHub</span>
|
|
2616
|
-
<span class="about-link" id="linkDocs">Documentation</span>
|
|
2617
|
-
<span class="about-link" id="linkLinkedIn">Developer LinkedIn</span>
|
|
3301
|
+
<div class="settings-section">
|
|
3302
|
+
<div class="settings-section-title">Quick Start</div>
|
|
3303
|
+
<div class="settings-hint" style="line-height:1.8;font-size:11px">
|
|
3304
|
+
<b style="color:var(--text)">1.</b> <code style="color:var(--accent);background:var(--bg3);padding:1px 5px;border-radius:3px">npx reactoradar setup</code><br/>
|
|
3305
|
+
<b style="color:var(--text)">2.</b> <code style="color:var(--accent);background:var(--bg3);padding:1px 5px;border-radius:3px">npx reactoradar</code> or open app<br/>
|
|
3306
|
+
<b style="color:var(--text)">3.</b> <code style="color:var(--accent);background:var(--bg3);padding:1px 5px;border-radius:3px">npx react-native start</code><br/>
|
|
3307
|
+
<b style="color:var(--text)">4.</b> Console, Network, Redux auto-connect<br/>
|
|
3308
|
+
<b style="color:var(--text)">5.</b> <code style="color:var(--accent);background:var(--bg3);padding:1px 5px;border-radius:3px">npx reactoradar remove</code> to uninstall
|
|
2618
3309
|
</div>
|
|
2619
3310
|
</div>
|
|
2620
3311
|
</div>
|
|
@@ -2633,6 +3324,9 @@ function initSettingsPanel() {
|
|
|
2633
3324
|
{ id: 'github-dark', name: 'GitHub Dark', colors: ['#0d1117','#58a6ff','#3fb950','#f85149'] },
|
|
2634
3325
|
{ id: 'one-dark', name: 'One Dark', colors: ['#282c34','#61afef','#98c379','#e06c75'] },
|
|
2635
3326
|
];
|
|
3327
|
+
// Tab visibility + drag reorder
|
|
3328
|
+
_buildTabVisGrid();
|
|
3329
|
+
|
|
2636
3330
|
const grid = $('themeSwitcher');
|
|
2637
3331
|
themes.forEach(t => {
|
|
2638
3332
|
const btn = document.createElement('button');
|
|
@@ -2667,6 +3361,9 @@ function initSettingsPanel() {
|
|
|
2667
3361
|
$('linkLinkedIn')?.addEventListener('click', () => {
|
|
2668
3362
|
window.electronAPI?.openExternal('https://www.linkedin.com/in/sharanagoudamk/');
|
|
2669
3363
|
});
|
|
3364
|
+
$('linkSupport')?.addEventListener('click', () => {
|
|
3365
|
+
window.electronAPI?.openExternal('https://razorpay.me/@reactoradar');
|
|
3366
|
+
});
|
|
2670
3367
|
|
|
2671
3368
|
// App name
|
|
2672
3369
|
$('appNameInput').addEventListener('change', (e) => {
|
|
@@ -2703,14 +3400,64 @@ function initSettingsPanel() {
|
|
|
2703
3400
|
applyFontSize(size);
|
|
2704
3401
|
});
|
|
2705
3402
|
|
|
3403
|
+
// Font family
|
|
3404
|
+
$('fontFamilySelect')?.addEventListener('change', (e) => {
|
|
3405
|
+
const family = e.target.value;
|
|
3406
|
+
setStoredFontFamily(family);
|
|
3407
|
+
applyFontFamily(family);
|
|
3408
|
+
});
|
|
3409
|
+
|
|
3410
|
+
// Toast toggle
|
|
3411
|
+
$('toastToggle')?.addEventListener('change', (e) => {
|
|
3412
|
+
setToastsEnabled(e.target.checked);
|
|
3413
|
+
});
|
|
3414
|
+
|
|
2706
3415
|
// Apply update banner if update info arrived before settings panel was created
|
|
2707
3416
|
_applyUpdateBanner();
|
|
2708
3417
|
}
|
|
2709
3418
|
|
|
2710
|
-
//
|
|
3419
|
+
// ─── Memory Monitor ──────────────────────────────────────────────────────────
|
|
3420
|
+
// Check memory usage periodically and warn user before it causes blank screen
|
|
3421
|
+
let _memoryWarningShown = false;
|
|
3422
|
+
setInterval(() => {
|
|
3423
|
+
if (!window.performance || !performance.memory) return;
|
|
3424
|
+
const used = performance.memory.usedJSHeapSize;
|
|
3425
|
+
const limit = performance.memory.jsHeapSizeLimit;
|
|
3426
|
+
const pct = used / limit;
|
|
3427
|
+
// Warn at 70% usage
|
|
3428
|
+
if (pct > 0.7 && !_memoryWarningShown) {
|
|
3429
|
+
_memoryWarningShown = true;
|
|
3430
|
+
const banner = document.createElement('div');
|
|
3431
|
+
banner.id = 'memoryWarning';
|
|
3432
|
+
banner.className = 'memory-warning';
|
|
3433
|
+
const usedMB = Math.round(used / 1024 / 1024);
|
|
3434
|
+
banner.innerHTML = `<span>High memory usage (${usedMB}MB) — ReactoRadar may become unresponsive.</span>`
|
|
3435
|
+
+ `<button class="memory-warn-btn" id="memWarnClear">Clear All Data</button>`
|
|
3436
|
+
+ `<button class="memory-warn-btn" id="memWarnDismiss">Dismiss</button>`;
|
|
3437
|
+
document.body.prepend(banner);
|
|
3438
|
+
$('memWarnClear')?.addEventListener('click', () => {
|
|
3439
|
+
// Clear all panel data
|
|
3440
|
+
state.console.logs = []; _consolePending = [];
|
|
3441
|
+
_lastLogMsg = ''; _lastLogRow = null; _lastLogCount = 1;
|
|
3442
|
+
$('cBadge').textContent = '0'; renderConsole();
|
|
3443
|
+
state.network.requests = {}; state.network.order = []; state.network.selectedId = null;
|
|
3444
|
+
$('nBadge').textContent = '0'; renderNetwork();
|
|
3445
|
+
state.redux.actions = []; state.redux.states = []; state.redux.selected = -1;
|
|
3446
|
+
$('rBadge').textContent = '0'; renderRedux();
|
|
3447
|
+
banner.remove(); _memoryWarningShown = false;
|
|
3448
|
+
});
|
|
3449
|
+
$('memWarnDismiss')?.addEventListener('click', () => { banner.remove(); });
|
|
3450
|
+
}
|
|
3451
|
+
// Reset flag when memory drops
|
|
3452
|
+
if (pct < 0.5) _memoryWarningShown = false;
|
|
3453
|
+
}, 30000); // Check every 30 seconds
|
|
3454
|
+
|
|
3455
|
+
// Apply saved theme + font size + font family + app name on load
|
|
2711
3456
|
applyTheme(getStoredTheme());
|
|
2712
3457
|
applyFontSize(getStoredFontSize());
|
|
3458
|
+
applyFontFamily(getStoredFontFamily());
|
|
2713
3459
|
applyAppName(getStoredAppName());
|
|
3460
|
+
applyTabVisibility();
|
|
2714
3461
|
|
|
2715
3462
|
// Send stored metro port to backend
|
|
2716
3463
|
window.electronAPI?.setMetroPort(getStoredMetroPort());
|
|
@@ -2925,8 +3672,7 @@ function buildSourceTreeNode(name, value, depth) {
|
|
|
2925
3672
|
});
|
|
2926
3673
|
}
|
|
2927
3674
|
|
|
2928
|
-
|
|
2929
|
-
|
|
3675
|
+
// Folders start collapsed — populate lazily on first expand
|
|
2930
3676
|
header.addEventListener('click', () => {
|
|
2931
3677
|
const isOpen = children.style.display !== 'none';
|
|
2932
3678
|
if (!isOpen) {
|
|
@@ -3111,6 +3857,7 @@ function drawPerfGraph(canvasId, data, maxVal, color) {
|
|
|
3111
3857
|
|
|
3112
3858
|
// Handle performance events from SDK (always updates meters, graphs only when recording)
|
|
3113
3859
|
function handlePerfEvent(event) {
|
|
3860
|
+
if (!isTabEnabled('performance') && !isTabEnabled('memory')) return;
|
|
3114
3861
|
if (event.fps != null) {
|
|
3115
3862
|
perfState.fps.push(event.fps);
|
|
3116
3863
|
if (perfState.fps.length > 100) perfState.fps.shift();
|