reactoradar 1.6.5 → 1.6.7
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/AGENTS.md +341 -0
- package/app.js +22 -4448
- package/index.html +12 -0
- package/init.js +187 -0
- package/main.js +1 -0
- package/package.json +4 -2
- package/panels/console.js +788 -0
- package/panels/ga4.js +328 -0
- package/panels/native.js +256 -0
- package/panels/network.js +968 -0
- package/{src/renderer/panels → panels}/performance.js +51 -14
- package/panels/react.js +21 -0
- package/panels/redux.js +438 -0
- package/panels/settings.js +832 -0
- package/panels/sources.js +282 -0
- package/{src/renderer/panels → panels}/storage.js +77 -26
- package/styles.css +40 -7
- package/src/main/main.js +0 -396
- package/src/main/preload.js +0 -28
- package/src/renderer/app.js +0 -221
- package/src/renderer/components/object-tree.js +0 -245
- package/src/renderer/index.html +0 -111
- package/src/renderer/panels/console.js +0 -248
- package/src/renderer/panels/memory.js +0 -60
- package/src/renderer/panels/network.js +0 -559
- package/src/renderer/panels/react.js +0 -31
- package/src/renderer/panels/redux.js +0 -159
- package/src/renderer/panels/settings.js +0 -93
- package/src/renderer/panels/sources.js +0 -189
- package/src/renderer/state.js +0 -132
- package/src/renderer/styles/components.css +0 -145
- package/src/renderer/styles/console.css +0 -73
- package/src/renderer/styles/main.css +0 -229
- package/src/renderer/styles/network.css +0 -242
- package/src/renderer/styles/performance.css +0 -45
- package/src/renderer/styles/redux.css +0 -77
- package/src/renderer/styles/settings.css +0 -63
- package/src/renderer/styles/sources.css +0 -48
- package/src/renderer/styles/storage.css +0 -28
- package/src/renderer/styles/theme-light.css +0 -57
package/app.js
CHANGED
|
@@ -136,8 +136,6 @@ document.addEventListener('keydown', (e) => {
|
|
|
136
136
|
}
|
|
137
137
|
});
|
|
138
138
|
|
|
139
|
-
// Global filter removed — each panel has its own search input
|
|
140
|
-
|
|
141
139
|
// ─── Clear (each panel has its own clear button now) ─────────────────────────
|
|
142
140
|
|
|
143
141
|
function clearActiveTab() {
|
|
@@ -238,23 +236,23 @@ function clearAll() {
|
|
|
238
236
|
if (ga4Detail) ga4Detail.innerHTML = '';
|
|
239
237
|
// Native logs
|
|
240
238
|
_nativeState.logs = [];
|
|
241
|
-
const
|
|
242
|
-
if (
|
|
239
|
+
const nativeList2 = $('nativeLogList');
|
|
240
|
+
if (nativeList2) nativeList2.innerHTML = '';
|
|
243
241
|
// Performance
|
|
244
242
|
perfState.fps = [];
|
|
245
243
|
perfState.jsThread = [];
|
|
246
244
|
perfState.uiThread = [];
|
|
247
245
|
perfState.data = [];
|
|
248
|
-
const
|
|
249
|
-
const
|
|
250
|
-
const
|
|
246
|
+
const perfFPS2 = $('perfFPS'); if (perfFPS2) perfFPS2.textContent = '—';
|
|
247
|
+
const perfJS2 = $('perfJS'); if (perfJS2) perfJS2.textContent = '—';
|
|
248
|
+
const perfUI2 = $('perfUI'); if (perfUI2) perfUI2.textContent = '—';
|
|
251
249
|
clearPerfCanvas('perfFPSCanvas');
|
|
252
250
|
clearPerfCanvas('perfJSCanvas');
|
|
253
251
|
clearPerfCanvas('perfUICanvas');
|
|
254
252
|
// Memory
|
|
255
|
-
const
|
|
256
|
-
const
|
|
257
|
-
const
|
|
253
|
+
const memHU2 = $('memHeapUsed'); if (memHU2) memHU2.textContent = '—';
|
|
254
|
+
const memHT2 = $('memHeapTotal'); if (memHT2) memHT2.textContent = '—';
|
|
255
|
+
const memN2 = $('memNative'); if (memN2) memN2.textContent = '—';
|
|
258
256
|
// Badges
|
|
259
257
|
$('cBadge').textContent = '0';
|
|
260
258
|
$('nBadge').textContent = '0';
|
|
@@ -283,8 +281,13 @@ function freeMemory() {
|
|
|
283
281
|
if (state.console.logs.length > 200) {
|
|
284
282
|
state.console.logs = state.console.logs.slice(-200);
|
|
285
283
|
}
|
|
286
|
-
//
|
|
287
|
-
state.redux.
|
|
284
|
+
// Trim Redux history (keep actions and states in sync — they must have same length)
|
|
285
|
+
if (state.redux.actions.length > 50) {
|
|
286
|
+
state.redux.actions = state.redux.actions.slice(-50);
|
|
287
|
+
state.redux.states = state.redux.states.slice(-50);
|
|
288
|
+
state.redux.actions.forEach((a, i) => a.index = i);
|
|
289
|
+
state.redux.selected = -1;
|
|
290
|
+
}
|
|
288
291
|
// Drop storage values (keep keys for reference)
|
|
289
292
|
for (const k in state.storage.entries) {
|
|
290
293
|
state.storage.entries[k] = null;
|
|
@@ -306,254 +309,7 @@ function freeMemory() {
|
|
|
306
309
|
_consolePending = [];
|
|
307
310
|
}
|
|
308
311
|
|
|
309
|
-
// ─── CDP Button ───────────────────────────────────────────────────────────────
|
|
310
|
-
$('btnCDP')?.addEventListener('click', () => {
|
|
311
|
-
// Tell main process to open the CDP DevTools window with the best available target
|
|
312
|
-
window.electronAPI?.openCDPTarget(null); // null = use latest known target
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
// ─── Screenshot Button ────────────────────────────────────────────────────────
|
|
316
|
-
$('btnScreenshot')?.addEventListener('click', takeScreenshot);
|
|
317
|
-
|
|
318
|
-
function takeScreenshot() {
|
|
319
|
-
const btn = $('btnScreenshot');
|
|
320
|
-
if (!btn) return;
|
|
321
|
-
const origText = btn.innerHTML;
|
|
322
|
-
btn.innerHTML = '<span style="opacity:0.6">Saving...</span>';
|
|
323
|
-
// Use Electron's native capturePage — always works, no DOM rendering issues
|
|
324
|
-
window.electronAPI?.captureScreenshot();
|
|
325
|
-
btn.innerHTML = '<span style="color:var(--green)">Saved!</span>';
|
|
326
|
-
setTimeout(() => { btn.innerHTML = origText; }, 2000);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
330
|
-
// IPC from Main
|
|
331
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
332
|
-
if (window.electronAPI) {
|
|
333
|
-
window.electronAPI.on('ports', ports => { state.ports = ports; });
|
|
334
|
-
|
|
335
|
-
window.electronAPI.on('cdp-targets', targets => {
|
|
336
|
-
state.cdpTargets = targets;
|
|
337
|
-
const btn = $('btnCDP');
|
|
338
|
-
if (btn) {
|
|
339
|
-
const hasCDP = targets?.length > 0;
|
|
340
|
-
const port = state.ports?.METRO || getStoredMetroPort();
|
|
341
|
-
btn.textContent = hasCDP
|
|
342
|
-
? `JS Debugger (:${port}) [${targets.length}] ↗`
|
|
343
|
-
: `JS Debugger (:${port}) ↗`;
|
|
344
|
-
btn.style.opacity = hasCDP ? '1' : '0.5';
|
|
345
|
-
if (hasCDP) {
|
|
346
|
-
btn.onclick = () => window.electronAPI.openCDPTarget(targets[0].webSocketDebuggerUrl);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
window.electronAPI.on('redux-event', handleReduxEvent);
|
|
352
|
-
window.electronAPI.on('network-event', handleNetworkEvent);
|
|
353
|
-
window.electronAPI.on('storage-event', handleStorageEvent);
|
|
354
|
-
|
|
355
|
-
window.electronAPI.on('ga4-event', handleGA4Event);
|
|
356
|
-
|
|
357
|
-
window.electronAPI.on('perf-event', event => {
|
|
358
|
-
handlePerfEvent(event);
|
|
359
|
-
handleMemoryEvent(event);
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
window.electronAPI.on('clear-all-ui', clearAll);
|
|
363
|
-
|
|
364
|
-
// When all device bridges disconnect, release heavy memory but keep logs visible.
|
|
365
|
-
// Debounced to avoid data loss during hot reloads or flaky connections.
|
|
366
|
-
let _disconnectTimer = null;
|
|
367
|
-
window.electronAPI.on('device-all-disconnected', () => {
|
|
368
|
-
clearTimeout(_disconnectTimer);
|
|
369
|
-
_disconnectTimer = setTimeout(() => {
|
|
370
|
-
console.log('[App] All devices disconnected — freeing memory');
|
|
371
|
-
freeMemory();
|
|
372
|
-
}, 3000);
|
|
373
|
-
});
|
|
374
|
-
// Cancel pending free if a device reconnects
|
|
375
|
-
const _cancelDisconnectTimer = () => { clearTimeout(_disconnectTimer); _disconnectTimer = null; };
|
|
376
|
-
window.electronAPI.on('redux-connected', on => { if (on) _cancelDisconnectTimer(); updateDeviceBanner('redux', on); });
|
|
377
|
-
window.electronAPI.on('network-connected', on => { if (on) _cancelDisconnectTimer(); updateDeviceBanner('network', on); });
|
|
378
|
-
window.electronAPI.on('storage-connected', on => { if (on) _cancelDisconnectTimer(); updateDeviceBanner('storage', on); });
|
|
379
|
-
window.electronAPI.on('react-dt-status', on => { updateDeviceBanner('reactDT', on); });
|
|
380
|
-
|
|
381
|
-
// Cmd+F — focus the search input for the active panel
|
|
382
|
-
function _handleFind() {
|
|
383
|
-
// If network detail is open, focus the detail search
|
|
384
|
-
if (state.activePanel === 'network' && state.network.selectedId) {
|
|
385
|
-
const wrap = $('detailSearchWrap');
|
|
386
|
-
const input = $('detailSearchInput');
|
|
387
|
-
if (wrap && input) {
|
|
388
|
-
wrap.style.display = 'flex';
|
|
389
|
-
input.focus();
|
|
390
|
-
input.select();
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
const searchMap = {
|
|
395
|
-
console: 'consoleSearch',
|
|
396
|
-
network: 'netSearchInput',
|
|
397
|
-
ga4: 'ga4Search',
|
|
398
|
-
redux: 'reduxSearch',
|
|
399
|
-
storage: 'storageSearch',
|
|
400
|
-
};
|
|
401
|
-
const inputId = searchMap[state.activePanel];
|
|
402
|
-
if (inputId) {
|
|
403
|
-
const el = $(inputId);
|
|
404
|
-
if (el) { el.focus(); el.select(); }
|
|
405
|
-
}
|
|
406
|
-
// Also show/focus Console bottom find bar
|
|
407
|
-
if (state.activePanel === 'console') {
|
|
408
|
-
const bar = $('consoleFindBar');
|
|
409
|
-
if (bar) { bar.style.display = 'flex'; $('consoleFindInput')?.focus(); }
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
window.electronAPI.on('focus-search', _handleFind);
|
|
413
|
-
// Direct keyboard fallback — Electron menu accelerators can miss in some contexts
|
|
414
|
-
document.addEventListener('keydown', (e) => {
|
|
415
|
-
if ((e.metaKey || e.ctrlKey) && e.key === 'f') {
|
|
416
|
-
e.preventDefault();
|
|
417
|
-
_handleFind();
|
|
418
|
-
}
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
window.electronAPI.on('app-version', (version, isPackaged) => {
|
|
422
|
-
state._appVersion = version;
|
|
423
|
-
state._isPackaged = !!isPackaged;
|
|
424
|
-
// Update anywhere the version is displayed
|
|
425
|
-
document.querySelectorAll('#aboutVersion').forEach(el => el.textContent = 'v' + version);
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
window.electronAPI.on('update-available', ({ current, latest, autoUpdate }) => {
|
|
429
|
-
state._updateAvailable = { current, latest, autoUpdate };
|
|
430
|
-
_applyUpdateBanner();
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
window.electronAPI.on('update-downloaded', ({ version }) => {
|
|
434
|
-
state._updateDownloaded = version;
|
|
435
|
-
_applyUpdateBanner();
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
window.electronAPI.on('trigger-open-cdp', () => {
|
|
439
|
-
window.electronAPI?.openCDPTarget(null);
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
// Theme toggle from menu shortcut (Cmd+Shift+T)
|
|
443
|
-
window.electronAPI.on('theme-changed', theme => {
|
|
444
|
-
document.documentElement.setAttribute('data-theme', theme);
|
|
445
|
-
setStoredTheme(theme);
|
|
446
|
-
document.querySelectorAll('#themeSwitcher .theme-card')
|
|
447
|
-
.forEach(b => b.classList.toggle('active', b.dataset.theme === theme));
|
|
448
|
-
});
|
|
449
|
-
}
|
|
450
|
-
|
|
451
312
|
// ─── Device Connection Status (inline in titlebar) ───────────────────────────
|
|
452
|
-
// Reusable — called from IPC handler AND from initSettingsPanel
|
|
453
|
-
function _applyUpdateBanner() {
|
|
454
|
-
const info = state._updateAvailable;
|
|
455
|
-
if (!info) return;
|
|
456
|
-
const { current, latest, autoUpdate } = info;
|
|
457
|
-
const downloaded = state._updateDownloaded;
|
|
458
|
-
const targetVersion = downloaded || latest;
|
|
459
|
-
|
|
460
|
-
const el = $('aboutVersion');
|
|
461
|
-
if (el) {
|
|
462
|
-
if (downloaded) {
|
|
463
|
-
el.innerHTML = `v${current} <span style="color:var(--green);font-size:10px;margin-left:6px">v${downloaded} ready to install</span>`;
|
|
464
|
-
} else {
|
|
465
|
-
el.innerHTML = `v${current} <span style="color:var(--green);font-size:10px;margin-left:6px">v${latest} available</span>`;
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
// Remove old buttons if state changed
|
|
470
|
-
const oldBtn = $('updateBtn');
|
|
471
|
-
if (oldBtn && downloaded && !oldBtn.dataset.isRestart) oldBtn.parentElement?.remove();
|
|
472
|
-
const oldChangelog = $('changelogBtn');
|
|
473
|
-
if (oldChangelog && downloaded && !oldChangelog.dataset.updated) oldChangelog.remove();
|
|
474
|
-
|
|
475
|
-
const aboutEl = document.querySelector('.settings-about');
|
|
476
|
-
if (!aboutEl) return;
|
|
477
|
-
|
|
478
|
-
// Add "What's new?" link
|
|
479
|
-
if (!$('changelogBtn')) {
|
|
480
|
-
const link = document.createElement('div');
|
|
481
|
-
link.style.cssText = 'margin-top:6px;text-align:center';
|
|
482
|
-
link.innerHTML = `<span id="changelogBtn" class="about-link" style="font-size:10px;cursor:pointer" data-updated="${downloaded ? '1' : ''}">What's new in v${targetVersion}?</span>`;
|
|
483
|
-
aboutEl.appendChild(link);
|
|
484
|
-
$('changelogBtn')?.addEventListener('click', () => _showChangelog(targetVersion));
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
// Add update button
|
|
488
|
-
if (!$('updateBtn')) {
|
|
489
|
-
const btn = document.createElement('div');
|
|
490
|
-
btn.style.cssText = 'margin-top:8px;text-align:center';
|
|
491
|
-
if (downloaded) {
|
|
492
|
-
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>';
|
|
493
|
-
aboutEl.appendChild(btn);
|
|
494
|
-
$('updateBtn')?.addEventListener('click', () => window.electronAPI?.installUpdate());
|
|
495
|
-
} else if (autoUpdate) {
|
|
496
|
-
btn.innerHTML = '<button id="updateBtn" class="tb-btn" style="font-size:11px;padding:6px 16px;opacity:0.7" disabled>Downloading v' + latest + '...</button>';
|
|
497
|
-
aboutEl.appendChild(btn);
|
|
498
|
-
} else {
|
|
499
|
-
btn.innerHTML = '<button id="updateBtn" class="tb-btn primary" style="font-size:11px;padding:6px 16px">Download v' + latest + '</button>';
|
|
500
|
-
aboutEl.appendChild(btn);
|
|
501
|
-
$('updateBtn')?.addEventListener('click', () => window.electronAPI?.openExternal('https://github.com/sharanagouda/reactoradar/releases'));
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
async function _showChangelog(version) {
|
|
507
|
-
if (!version || typeof version !== 'string') return;
|
|
508
|
-
|
|
509
|
-
// Remove existing modal
|
|
510
|
-
$('changelogModal')?.remove();
|
|
511
|
-
|
|
512
|
-
const safeVersion = esc(version);
|
|
513
|
-
const modal = document.createElement('div');
|
|
514
|
-
modal.id = 'changelogModal';
|
|
515
|
-
modal.className = 'changelog-modal-overlay';
|
|
516
|
-
modal.innerHTML = `
|
|
517
|
-
<div class="changelog-modal">
|
|
518
|
-
<div class="changelog-header">
|
|
519
|
-
<span class="changelog-title">What's New in v${safeVersion}</span>
|
|
520
|
-
<button class="changelog-close" id="changelogClose">×</button>
|
|
521
|
-
</div>
|
|
522
|
-
<div class="changelog-body" id="changelogBody">
|
|
523
|
-
<div style="color:var(--text-dim);padding:20px;text-align:center">Loading release notes...</div>
|
|
524
|
-
</div>
|
|
525
|
-
</div>`;
|
|
526
|
-
document.body.appendChild(modal);
|
|
527
|
-
|
|
528
|
-
// Close handlers
|
|
529
|
-
$('changelogClose')?.addEventListener('click', () => modal.remove());
|
|
530
|
-
modal.addEventListener('click', (e) => { if (e.target === modal) modal.remove(); });
|
|
531
|
-
|
|
532
|
-
// Fetch changelog
|
|
533
|
-
try {
|
|
534
|
-
const notes = await window.electronAPI?.fetchChangelog(version);
|
|
535
|
-
const body = $('changelogBody');
|
|
536
|
-
if (!body) return;
|
|
537
|
-
if (!notes || typeof notes !== 'string') {
|
|
538
|
-
body.innerHTML = '<div style="color:var(--text-dim);padding:20px;text-align:center">No release notes available.</div>';
|
|
539
|
-
return;
|
|
540
|
-
}
|
|
541
|
-
if (body && notes) {
|
|
542
|
-
// Simple markdown-like rendering
|
|
543
|
-
body.innerHTML = notes
|
|
544
|
-
.replace(/^### (.+)$/gm, '<h3 style="color:var(--accent);font-size:12px;font-weight:700;margin:12px 0 6px">$1</h3>')
|
|
545
|
-
.replace(/^## (.+)$/gm, '<h2 style="color:var(--text);font-size:14px;font-weight:700;margin:16px 0 8px">$1</h2>')
|
|
546
|
-
.replace(/^- \*\*(.+?)\*\*(.*)$/gm, '<div style="margin:3px 0;font-size:11px;line-height:1.6"><b style="color:var(--text)">$1</b><span style="color:var(--text-dim)">$2</span></div>')
|
|
547
|
-
.replace(/^- (.+)$/gm, '<div style="margin:3px 0;font-size:11px;line-height:1.6;color:var(--text-mid)">• $1</div>')
|
|
548
|
-
.replace(/`([^`]+)`/g, '<code style="background:var(--bg3);padding:1px 4px;border-radius:3px;color:var(--accent);font-size:10px">$1</code>')
|
|
549
|
-
.replace(/\n\n/g, '<br/>');
|
|
550
|
-
}
|
|
551
|
-
} catch {
|
|
552
|
-
const body = $('changelogBody');
|
|
553
|
-
if (body) body.innerHTML = '<div style="color:var(--red);padding:20px;text-align:center">Could not fetch release notes</div>';
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
|
|
557
313
|
function updateDeviceBanner(service, connected) {
|
|
558
314
|
state.connections[service] = connected;
|
|
559
315
|
const el = $('deviceStatus');
|
|
@@ -571,4194 +327,12 @@ function updateDeviceBanner(service, connected) {
|
|
|
571
327
|
}
|
|
572
328
|
}
|
|
573
329
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
577
|
-
// Load saved log level filters from localStorage
|
|
578
|
-
function getStoredLogLevels() {
|
|
579
|
-
try {
|
|
580
|
-
const saved = localStorage.getItem('rn-debug-log-levels');
|
|
581
|
-
if (saved) return JSON.parse(saved);
|
|
582
|
-
} catch {}
|
|
583
|
-
return { log: true, info: true, warn: true, error: true, debug: true, redux: false };
|
|
584
|
-
}
|
|
585
|
-
function setStoredLogLevels(levels) {
|
|
586
|
-
try { localStorage.setItem('rn-debug-log-levels', JSON.stringify(levels)); } catch {}
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
function initConsolePanel() {
|
|
590
|
-
const panel = $('panel-console');
|
|
591
|
-
const levels = getStoredLogLevels();
|
|
592
|
-
state.console.levelFilters = levels;
|
|
593
|
-
state.console.showRedux = !!levels.redux;
|
|
594
|
-
|
|
595
|
-
panel.innerHTML = `
|
|
596
|
-
<div class="panel-toolbar">
|
|
597
|
-
<span class="panel-label">Console</span>
|
|
598
|
-
<span class="badge" id="cBadge">0</span>
|
|
599
|
-
<input id="consoleSearch" class="net-search-input" style="margin-left:12px" placeholder="Filter logs..." />
|
|
600
|
-
<div class="ml-auto" style="display:flex;align-items:center;gap:6px">
|
|
601
|
-
<button class="panel-clear-btn" id="consoleExport" title="Export logs as JSON">Export</button>
|
|
602
|
-
<button class="panel-clear-btn" id="consoleClear" title="Clear console">Clear</button>
|
|
603
|
-
<div class="console-level-dropdown" id="consoleLevelDropdown">
|
|
604
|
-
<button class="console-level-btn" id="consoleLevelBtn">Levels ▾</button>
|
|
605
|
-
<div class="console-level-menu" id="consoleLevelMenu">
|
|
606
|
-
<label class="console-level-option"><input type="checkbox" data-level="log" ${levels.log ? 'checked' : ''} /><span class="lvl-dot" style="background:var(--text-mid)"></span>Log</label>
|
|
607
|
-
<label class="console-level-option"><input type="checkbox" data-level="info" ${levels.info ? 'checked' : ''} /><span class="lvl-dot" style="background:var(--accent)"></span>Info</label>
|
|
608
|
-
<label class="console-level-option"><input type="checkbox" data-level="warn" ${levels.warn ? 'checked' : ''} /><span class="lvl-dot" style="background:var(--yellow)"></span>Warn</label>
|
|
609
|
-
<label class="console-level-option"><input type="checkbox" data-level="error" ${levels.error ? 'checked' : ''} /><span class="lvl-dot" style="background:var(--red)"></span>Error</label>
|
|
610
|
-
<label class="console-level-option"><input type="checkbox" data-level="debug" ${levels.debug ? 'checked' : ''} /><span class="lvl-dot" style="background:var(--accent2)"></span>Debug</label>
|
|
611
|
-
<div style="border-top:1px solid var(--border);margin:4px 0"></div>
|
|
612
|
-
<label class="console-level-option"><input type="checkbox" data-level="redux" ${levels.redux ? 'checked' : ''} /><span class="lvl-dot" style="background:var(--green)"></span>Redux Actions</label>
|
|
613
|
-
</div>
|
|
614
|
-
</div>
|
|
615
|
-
</div>
|
|
616
|
-
</div>
|
|
617
|
-
<div class="scroll-area" id="consoleList">
|
|
618
|
-
<div class="empty-state" id="consoleEmpty">
|
|
619
|
-
<div class="icon">⬛</div>
|
|
620
|
-
<div class="label">No logs yet</div>
|
|
621
|
-
<div class="hint">Logs will appear here automatically</div>
|
|
622
|
-
</div>
|
|
623
|
-
</div>
|
|
624
|
-
<div class="console-find-bar" id="consoleFindBar" style="display:none">
|
|
625
|
-
<input id="consoleFindInput" class="console-find-input" placeholder="Find in logs... (Cmd+F)" />
|
|
626
|
-
<span id="consoleFindCount" class="console-find-count"></span>
|
|
627
|
-
<button class="console-find-btn" id="consoleFindPrev" title="Previous">▲</button>
|
|
628
|
-
<button class="console-find-btn" id="consoleFindNext" title="Next">▼</button>
|
|
629
|
-
<button class="console-find-btn" id="consoleFindClose" title="Close (Esc)">✕</button>
|
|
630
|
-
</div>`;
|
|
631
|
-
|
|
632
|
-
// Search filter
|
|
633
|
-
$('consoleSearch').addEventListener('input', (e) => {
|
|
634
|
-
state.console.searchFilter = e.target.value.toLowerCase().trim();
|
|
635
|
-
renderConsole();
|
|
636
|
-
});
|
|
637
|
-
|
|
638
|
-
// Level dropdown toggle
|
|
639
|
-
$('consoleLevelBtn').addEventListener('click', (e) => {
|
|
640
|
-
e.stopPropagation();
|
|
641
|
-
$('consoleLevelMenu').classList.toggle('open');
|
|
642
|
-
});
|
|
643
|
-
|
|
644
|
-
// Close dropdown when clicking outside
|
|
645
|
-
document.addEventListener('click', (e) => {
|
|
646
|
-
if (!e.target.closest('#consoleLevelDropdown')) {
|
|
647
|
-
$('consoleLevelMenu')?.classList.remove('open');
|
|
648
|
-
}
|
|
649
|
-
});
|
|
650
|
-
|
|
651
|
-
// Level checkbox changes
|
|
652
|
-
$('consoleLevelMenu').addEventListener('change', (e) => {
|
|
653
|
-
const checkbox = e.target;
|
|
654
|
-
const level = checkbox.dataset.level;
|
|
655
|
-
if (level) {
|
|
656
|
-
state.console.levelFilters[level] = checkbox.checked;
|
|
657
|
-
if (level === 'redux') state.console.showRedux = checkbox.checked;
|
|
658
|
-
setStoredLogLevels(state.console.levelFilters);
|
|
659
|
-
updateLevelBtnText();
|
|
660
|
-
renderConsole();
|
|
661
|
-
}
|
|
662
|
-
});
|
|
663
|
-
|
|
664
|
-
updateLevelBtnText();
|
|
665
|
-
|
|
666
|
-
$('consoleExport')?.addEventListener('click', () => {
|
|
667
|
-
const data = JSON.stringify(state.console.logs, null, 2);
|
|
668
|
-
const blob = new Blob([data], { type: 'application/json' });
|
|
669
|
-
const url = URL.createObjectURL(blob);
|
|
670
|
-
const a = document.createElement('a');
|
|
671
|
-
a.href = url; a.download = `reactoradar-console-${Date.now()}.json`; a.click();
|
|
672
|
-
URL.revokeObjectURL(url);
|
|
673
|
-
});
|
|
674
|
-
|
|
675
|
-
$('consoleClear').addEventListener('click', () => {
|
|
676
|
-
state.console.logs = [];
|
|
677
|
-
_consolePending = [];
|
|
678
|
-
_lastLogMsg = ''; _lastLogRow = null; _lastLogCount = 1;
|
|
679
|
-
$('cBadge').textContent = '0';
|
|
680
|
-
renderConsole();
|
|
681
|
-
});
|
|
682
|
-
|
|
683
|
-
// Find bar (Cmd+F)
|
|
684
|
-
let _findMatches = [];
|
|
685
|
-
let _findIdx = -1;
|
|
686
|
-
|
|
687
|
-
function doFind(term) {
|
|
688
|
-
// Clear previous highlights
|
|
689
|
-
document.querySelectorAll('.console-find-highlight').forEach(el => {
|
|
690
|
-
el.replaceWith(el.textContent);
|
|
691
|
-
});
|
|
692
|
-
_findMatches = [];
|
|
693
|
-
_findIdx = -1;
|
|
694
|
-
if (!term) { $('consoleFindCount').textContent = ''; return; }
|
|
695
|
-
|
|
696
|
-
const rows = document.querySelectorAll('#consoleList .log-row');
|
|
697
|
-
rows.forEach(row => {
|
|
698
|
-
const text = row.textContent.toLowerCase();
|
|
699
|
-
if (text.includes(term.toLowerCase())) _findMatches.push(row);
|
|
700
|
-
});
|
|
701
|
-
$('consoleFindCount').textContent = _findMatches.length ? `${_findMatches.length} found` : 'No matches';
|
|
702
|
-
if (_findMatches.length) { _findIdx = 0; _findMatches[0].scrollIntoView({ block: 'nearest' }); _findMatches[0].style.outline = '1px solid var(--accent)'; }
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
function findNav(dir) {
|
|
706
|
-
if (!_findMatches.length) return;
|
|
707
|
-
if (_findMatches[_findIdx]) _findMatches[_findIdx].style.outline = '';
|
|
708
|
-
_findIdx = (_findIdx + dir + _findMatches.length) % _findMatches.length;
|
|
709
|
-
_findMatches[_findIdx].scrollIntoView({ block: 'nearest' });
|
|
710
|
-
_findMatches[_findIdx].style.outline = '1px solid var(--accent)';
|
|
711
|
-
$('consoleFindCount').textContent = `${_findIdx + 1}/${_findMatches.length}`;
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
$('consoleFindInput').addEventListener('input', (e) => doFind(e.target.value));
|
|
715
|
-
$('consoleFindPrev').addEventListener('click', () => findNav(-1));
|
|
716
|
-
$('consoleFindNext').addEventListener('click', () => findNav(1));
|
|
717
|
-
$('consoleFindClose').addEventListener('click', () => {
|
|
718
|
-
$('consoleFindBar').style.display = 'none';
|
|
719
|
-
if (_findMatches[_findIdx]) _findMatches[_findIdx].style.outline = '';
|
|
720
|
-
_findMatches = []; _findIdx = -1;
|
|
721
|
-
$('consoleFindInput').value = '';
|
|
722
|
-
$('consoleFindCount').textContent = '';
|
|
723
|
-
});
|
|
724
|
-
$('consoleFindInput').addEventListener('keydown', (e) => {
|
|
725
|
-
if (e.key === 'Escape') $('consoleFindClose').click();
|
|
726
|
-
if (e.key === 'Enter') findNav(e.shiftKey ? -1 : 1);
|
|
727
|
-
});
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
function updateLevelBtnText() {
|
|
731
|
-
const levels = state.console.levelFilters;
|
|
732
|
-
const logLevels = { log: levels.log, info: levels.info, warn: levels.warn, error: levels.error, debug: levels.debug };
|
|
733
|
-
const allOn = Object.values(logLevels).every(v => v);
|
|
734
|
-
const allOff = Object.values(logLevels).every(v => !v);
|
|
735
|
-
const btn = $('consoleLevelBtn');
|
|
330
|
+
function takeScreenshot() {
|
|
331
|
+
const btn = $('btnScreenshot');
|
|
736
332
|
if (!btn) return;
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
btn.textContent = text + ' ▾';
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
// Console is fed via IPC (network-event handled in IPC section above)
|
|
746
|
-
|
|
747
|
-
// ─── Toast Notifications ─────────────────────────────────────────────────────
|
|
748
|
-
let _toastContainer = null;
|
|
749
|
-
const _activeToasts = {};
|
|
750
|
-
|
|
751
|
-
function getToastsEnabled() {
|
|
752
|
-
try { return localStorage.getItem('rn-debug-toasts') !== 'false'; } catch { return true; }
|
|
753
|
-
}
|
|
754
|
-
function setToastsEnabled(v) {
|
|
755
|
-
try { localStorage.setItem('rn-debug-toasts', v ? 'true' : 'false'); } catch {}
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
function showToast(message, type, targetPanel) {
|
|
759
|
-
if (!getToastsEnabled()) return;
|
|
760
|
-
if (!_toastContainer) {
|
|
761
|
-
_toastContainer = document.createElement('div');
|
|
762
|
-
_toastContainer.id = 'toastContainer';
|
|
763
|
-
_toastContainer.className = 'toast-container';
|
|
764
|
-
document.body.appendChild(_toastContainer);
|
|
765
|
-
}
|
|
766
|
-
// Don't show toast if user is already on the target panel
|
|
767
|
-
if (targetPanel && state.activePanel === targetPanel) return;
|
|
768
|
-
|
|
769
|
-
// Deduplicate: if same message already showing, increment count
|
|
770
|
-
const key = `${type}:${message}`;
|
|
771
|
-
if (_activeToasts[key] && _activeToasts[key].el.parentNode) {
|
|
772
|
-
const existing = _activeToasts[key];
|
|
773
|
-
existing.count++;
|
|
774
|
-
const msgEl = existing.el.querySelector('.toast-msg');
|
|
775
|
-
if (msgEl) msgEl.textContent = `${message} (${existing.count})`;
|
|
776
|
-
// Reset auto-remove timer
|
|
777
|
-
clearTimeout(existing.timer);
|
|
778
|
-
existing.timer = setTimeout(() => {
|
|
779
|
-
if (existing.el.parentNode) existing.el.remove();
|
|
780
|
-
delete _activeToasts[key];
|
|
781
|
-
}, 5000);
|
|
782
|
-
return;
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
const toast = document.createElement('div');
|
|
786
|
-
toast.className = `toast toast-${type || 'info'}`;
|
|
787
|
-
toast.innerHTML = `<span class="toast-msg">${esc(message)}</span>`;
|
|
788
|
-
if (targetPanel) {
|
|
789
|
-
const btn = document.createElement('span');
|
|
790
|
-
btn.className = 'toast-action';
|
|
791
|
-
btn.textContent = 'View';
|
|
792
|
-
btn.addEventListener('click', () => { switchPanel(targetPanel); toast.remove(); delete _activeToasts[key]; });
|
|
793
|
-
toast.appendChild(btn);
|
|
794
|
-
}
|
|
795
|
-
const close = document.createElement('span');
|
|
796
|
-
close.className = 'toast-close';
|
|
797
|
-
close.textContent = '✕';
|
|
798
|
-
close.addEventListener('click', () => { toast.remove(); delete _activeToasts[key]; });
|
|
799
|
-
toast.appendChild(close);
|
|
800
|
-
|
|
801
|
-
_toastContainer.appendChild(toast);
|
|
802
|
-
const timer = setTimeout(() => {
|
|
803
|
-
if (toast.parentNode) toast.remove();
|
|
804
|
-
delete _activeToasts[key];
|
|
805
|
-
}, 5000);
|
|
806
|
-
_activeToasts[key] = { el: toast, count: 1, timer };
|
|
807
|
-
// Keep max 3 toasts
|
|
808
|
-
const toasts = _toastContainer.querySelectorAll('.toast');
|
|
809
|
-
if (toasts.length > 3) { toasts[0].remove(); }
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
// ─── Batched console append (fixes re-render performance) ────────────────────
|
|
813
|
-
let _consolePending = [];
|
|
814
|
-
let _consoleRAF = null;
|
|
815
|
-
|
|
816
|
-
let _lastLogMsg = '';
|
|
817
|
-
let _lastLogRow = null;
|
|
818
|
-
let _lastLogCount = 1;
|
|
819
|
-
|
|
820
|
-
const MAX_CONSOLE_LOGS = 5000;
|
|
821
|
-
|
|
822
|
-
function addConsoleLog(event) {
|
|
823
|
-
state.console.logs.push(event);
|
|
824
|
-
// Cap in-memory logs to prevent memory leak
|
|
825
|
-
if (state.console.logs.length > MAX_CONSOLE_LOGS) {
|
|
826
|
-
state.console.logs = state.console.logs.slice(-MAX_CONSOLE_LOGS);
|
|
827
|
-
}
|
|
828
|
-
_consolePending.push(event);
|
|
829
|
-
|
|
830
|
-
// Batch DOM updates via rAF — only one paint per frame
|
|
831
|
-
if (!_consoleRAF) {
|
|
832
|
-
_consoleRAF = requestAnimationFrame(flushConsoleBatch);
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
function flushConsoleBatch() {
|
|
837
|
-
_consoleRAF = null;
|
|
838
|
-
const batch = _consolePending;
|
|
839
|
-
_consolePending = [];
|
|
840
|
-
if (!batch.length) return;
|
|
841
|
-
|
|
842
|
-
$('cBadge').textContent = state.console.logs.length;
|
|
843
|
-
|
|
844
|
-
const list = $('consoleList');
|
|
845
|
-
const empty = $('consoleEmpty');
|
|
846
|
-
if (!list) return;
|
|
847
|
-
|
|
848
|
-
const { levelFilters, searchFilter } = state.console;
|
|
849
|
-
const frag = document.createDocumentFragment();
|
|
850
|
-
let added = 0;
|
|
851
|
-
|
|
852
|
-
batch.forEach(l => {
|
|
853
|
-
// Redux logs use showRedux flag; regular logs use levelFilters
|
|
854
|
-
if (l.level === 'redux') {
|
|
855
|
-
if (!state.console.showRedux) return;
|
|
856
|
-
} else if (levelFilters && !levelFilters[l.level]) return;
|
|
857
|
-
if (searchFilter && !l.message?.toLowerCase().includes(searchFilter)) return;
|
|
858
|
-
|
|
859
|
-
// Group consecutive identical messages
|
|
860
|
-
const msgKey = `${l.level}:${l.message || ''}`;
|
|
861
|
-
if (msgKey === _lastLogMsg && _lastLogRow && _lastLogRow.parentNode) {
|
|
862
|
-
_lastLogCount++;
|
|
863
|
-
let badge = _lastLogRow.querySelector('.log-group-badge');
|
|
864
|
-
if (!badge) {
|
|
865
|
-
badge = document.createElement('span');
|
|
866
|
-
badge.className = 'log-group-badge';
|
|
867
|
-
_lastLogRow.insertBefore(badge, _lastLogRow.firstChild);
|
|
868
|
-
}
|
|
869
|
-
badge.textContent = _lastLogCount;
|
|
870
|
-
return; // Don't add a new row
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
_lastLogMsg = msgKey;
|
|
874
|
-
_lastLogCount = 1;
|
|
875
|
-
const row = buildLogRow(l);
|
|
876
|
-
_lastLogRow = row;
|
|
877
|
-
frag.appendChild(row);
|
|
878
|
-
added++;
|
|
879
|
-
});
|
|
880
|
-
|
|
881
|
-
if (added > 0) {
|
|
882
|
-
// Hide empty state as soon as we have visible rows
|
|
883
|
-
if (empty) empty.style.display = 'none';
|
|
884
|
-
// Auto-scroll only if user is already near the bottom (within 150px)
|
|
885
|
-
const wasAtBottom = (list.scrollHeight - list.scrollTop - list.clientHeight) < 150;
|
|
886
|
-
list.appendChild(frag);
|
|
887
|
-
// Keep DOM size manageable — remove oldest rows
|
|
888
|
-
const rows = list.querySelectorAll('.log-row');
|
|
889
|
-
const MAX_DOM_ROWS = 2000;
|
|
890
|
-
if (rows.length > MAX_DOM_ROWS) {
|
|
891
|
-
const toRemove = rows.length - MAX_DOM_ROWS;
|
|
892
|
-
for (let i = 0; i < toRemove; i++) rows[i].remove();
|
|
893
|
-
}
|
|
894
|
-
if (wasAtBottom) list.scrollTop = list.scrollHeight;
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
window.electronAPI?.on('console-event', addConsoleLog);
|
|
899
|
-
|
|
900
|
-
// ─── Object Tree Renderer (Chrome DevTools-like) ─────────────────────────────
|
|
901
|
-
// Builds interactive, collapsible DOM nodes for objects/arrays.
|
|
902
|
-
|
|
903
|
-
// Collect all entries for an object: own data properties + prototype getter values.
|
|
904
|
-
// Getter-derived keys use the clean name (e.g. "deliveryId") and skip backing
|
|
905
|
-
// fields (e.g. "_deliveryId") so the log output mirrors the model's public API.
|
|
906
|
-
function collectEntries(val) {
|
|
907
|
-
if (Array.isArray(val)) return val.map((v, i) => [i, v]);
|
|
908
|
-
|
|
909
|
-
const result = {};
|
|
910
|
-
const getterKeys = new Set();
|
|
911
|
-
|
|
912
|
-
// 1. Walk prototype chain and invoke getters
|
|
913
|
-
let proto = Object.getPrototypeOf(val);
|
|
914
|
-
while (proto && proto !== Object.prototype) {
|
|
915
|
-
const descs = Object.getOwnPropertyDescriptors(proto);
|
|
916
|
-
for (const [k, desc] of Object.entries(descs)) {
|
|
917
|
-
if (k === 'constructor') continue;
|
|
918
|
-
if (desc.get && !(k in result)) {
|
|
919
|
-
try { result[k] = desc.get.call(val); } catch { /* skip broken getters */ }
|
|
920
|
-
getterKeys.add(k);
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
proto = Object.getPrototypeOf(proto);
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
// 2. Add own data properties, but skip backing fields whose getter is present.
|
|
927
|
-
// Convention: getter "foo" backs "_foo"; if "foo" was collected, skip "_foo".
|
|
928
|
-
const ownKeys = Object.keys(val);
|
|
929
|
-
for (const k of ownKeys) {
|
|
930
|
-
const clean = k.startsWith('_') ? k.slice(1) : null;
|
|
931
|
-
if (clean && getterKeys.has(clean)) continue; // skip _backing field
|
|
932
|
-
if (!(k in result)) result[k] = val[k];
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
return Object.entries(result);
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
function objPreview(val, maxLen) {
|
|
939
|
-
maxLen = maxLen || 80;
|
|
940
|
-
if (val === null) return 'null';
|
|
941
|
-
if (val === undefined) return 'undefined';
|
|
942
|
-
if (Array.isArray(val)) {
|
|
943
|
-
if (val.length === 0) return '[]';
|
|
944
|
-
const items = [];
|
|
945
|
-
let len = 2; // [ ]
|
|
946
|
-
for (let i = 0; i < val.length && len < maxLen; i++) {
|
|
947
|
-
const s = primitivePreview(val[i]);
|
|
948
|
-
len += s.length + 2;
|
|
949
|
-
items.push(s);
|
|
950
|
-
}
|
|
951
|
-
const suffix = items.length < val.length ? ', ...' : '';
|
|
952
|
-
return `(${val.length}) [${items.join(', ')}${suffix}]`;
|
|
953
|
-
}
|
|
954
|
-
if (typeof val === 'object') {
|
|
955
|
-
const entries = collectEntries(val);
|
|
956
|
-
if (entries.length === 0) return '{}';
|
|
957
|
-
const items = [];
|
|
958
|
-
let len = 2;
|
|
959
|
-
for (let i = 0; i < entries.length && len < maxLen; i++) {
|
|
960
|
-
const s = `${entries[i][0]}: ${primitivePreview(entries[i][1])}`;
|
|
961
|
-
len += s.length + 2;
|
|
962
|
-
items.push(s);
|
|
963
|
-
}
|
|
964
|
-
const suffix = items.length < entries.length ? ', ...' : '';
|
|
965
|
-
return `{${items.join(', ')}${suffix}}`;
|
|
966
|
-
}
|
|
967
|
-
return primitivePreview(val);
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
function primitivePreview(val) {
|
|
971
|
-
if (val === null) return 'null';
|
|
972
|
-
if (val === undefined) return 'undefined';
|
|
973
|
-
if (typeof val === 'string') return val.length > 50 ? `"${val.slice(0,50)}..."` : `"${val}"`;
|
|
974
|
-
if (typeof val === 'number' || typeof val === 'boolean') return String(val);
|
|
975
|
-
if (Array.isArray(val)) return `Array(${val.length})`;
|
|
976
|
-
if (typeof val === 'object') return `{...}`;
|
|
977
|
-
return String(val);
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
function createTreeNode(key, val, startCollapsed) {
|
|
981
|
-
const isArray = Array.isArray(val);
|
|
982
|
-
const isObj = val !== null && typeof val === 'object';
|
|
983
|
-
|
|
984
|
-
if (!isObj) {
|
|
985
|
-
// Primitive leaf
|
|
986
|
-
const row = document.createElement('div');
|
|
987
|
-
row.className = 'ov-leaf';
|
|
988
|
-
if (key !== null) {
|
|
989
|
-
const k = document.createElement('span');
|
|
990
|
-
k.className = 'ov-key';
|
|
991
|
-
k.textContent = isNaN(key) ? `${key}: ` : `${key}: `;
|
|
992
|
-
row.appendChild(k);
|
|
993
|
-
}
|
|
994
|
-
row.appendChild(createPrimitiveSpan(val));
|
|
995
|
-
return row;
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
// Collapsible object/array
|
|
999
|
-
const container = document.createElement('div');
|
|
1000
|
-
container.className = 'ov-node';
|
|
1001
|
-
|
|
1002
|
-
const header = document.createElement('div');
|
|
1003
|
-
header.className = 'ov-header';
|
|
1004
|
-
|
|
1005
|
-
const arrow = document.createElement('span');
|
|
1006
|
-
arrow.className = 'ov-arrow';
|
|
1007
|
-
arrow.textContent = '\u25B6'; // ▶
|
|
1008
|
-
header.appendChild(arrow);
|
|
1009
|
-
|
|
1010
|
-
if (key !== null) {
|
|
1011
|
-
const k = document.createElement('span');
|
|
1012
|
-
k.className = 'ov-key';
|
|
1013
|
-
k.textContent = `${key}: `;
|
|
1014
|
-
header.appendChild(k);
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
const preview = document.createElement('span');
|
|
1018
|
-
preview.className = 'ov-preview';
|
|
1019
|
-
preview.textContent = objPreview(val);
|
|
1020
|
-
header.appendChild(preview);
|
|
1021
|
-
|
|
1022
|
-
container.appendChild(header);
|
|
1023
|
-
|
|
1024
|
-
const children = document.createElement('div');
|
|
1025
|
-
children.className = 'ov-children';
|
|
1026
|
-
children.style.display = 'none';
|
|
1027
|
-
|
|
1028
|
-
let populated = false;
|
|
1029
|
-
|
|
1030
|
-
function populateChildren() {
|
|
1031
|
-
if (populated) return;
|
|
1032
|
-
populated = true;
|
|
1033
|
-
const entries = collectEntries(val);
|
|
1034
|
-
entries.forEach(([k, v]) => {
|
|
1035
|
-
children.appendChild(createTreeNode(k, v, true));
|
|
1036
|
-
});
|
|
1037
|
-
// For arrays show length, for objects show prototype hint
|
|
1038
|
-
if (isArray) {
|
|
1039
|
-
const lenNode = document.createElement('div');
|
|
1040
|
-
lenNode.className = 'ov-leaf ov-meta';
|
|
1041
|
-
lenNode.textContent = `length: ${val.length}`;
|
|
1042
|
-
children.appendChild(lenNode);
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
let expanded = !startCollapsed;
|
|
1047
|
-
if (expanded) {
|
|
1048
|
-
populateChildren();
|
|
1049
|
-
children.style.display = 'block';
|
|
1050
|
-
arrow.textContent = '\u25BC'; // ▼
|
|
1051
|
-
arrow.classList.add('expanded');
|
|
1052
|
-
preview.style.display = 'none';
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
header.addEventListener('click', (e) => {
|
|
1056
|
-
e.stopPropagation();
|
|
1057
|
-
expanded = !expanded;
|
|
1058
|
-
if (expanded) {
|
|
1059
|
-
populateChildren();
|
|
1060
|
-
children.style.display = 'block';
|
|
1061
|
-
arrow.textContent = '\u25BC';
|
|
1062
|
-
arrow.classList.add('expanded');
|
|
1063
|
-
preview.style.display = 'none';
|
|
1064
|
-
} else {
|
|
1065
|
-
children.style.display = 'none';
|
|
1066
|
-
arrow.textContent = '\u25B6';
|
|
1067
|
-
arrow.classList.remove('expanded');
|
|
1068
|
-
preview.style.display = '';
|
|
1069
|
-
}
|
|
1070
|
-
});
|
|
1071
|
-
|
|
1072
|
-
container.appendChild(children);
|
|
1073
|
-
return container;
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
function _safeStr(val) {
|
|
1077
|
-
if (val === null) return 'null';
|
|
1078
|
-
if (val === undefined) return 'undefined';
|
|
1079
|
-
if (typeof val === 'string') return val;
|
|
1080
|
-
if (typeof val === 'number' || typeof val === 'boolean') return String(val);
|
|
1081
|
-
try { return JSON.stringify(val, null, 2); } catch { return String(val); }
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
function createPrimitiveSpan(val) {
|
|
1085
|
-
const s = document.createElement('span');
|
|
1086
|
-
if (val === null) { s.className = 'ov-null'; s.textContent = 'null'; }
|
|
1087
|
-
else if (val === undefined) { s.className = 'ov-undef'; s.textContent = 'undefined'; }
|
|
1088
|
-
else if (typeof val === 'string') { s.className = 'ov-str'; s.textContent = `"${val}"`; }
|
|
1089
|
-
else if (typeof val === 'number') { s.className = 'ov-num'; s.textContent = String(val); }
|
|
1090
|
-
else if (typeof val === 'boolean') { s.className = 'ov-bool'; s.textContent = String(val); }
|
|
1091
|
-
else { s.textContent = _safeStr(val); }
|
|
1092
|
-
return s;
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
// Parse a structured arg from the SDK (or fall back to raw message string)
|
|
1096
|
-
function renderConsoleArg(arg) {
|
|
1097
|
-
if (!arg || typeof arg !== 'object' || !arg.t) {
|
|
1098
|
-
// Backward compat: raw string
|
|
1099
|
-
const s = document.createElement('span');
|
|
1100
|
-
s.className = 'ov-str';
|
|
1101
|
-
s.textContent = _safeStr(arg);
|
|
1102
|
-
return s;
|
|
1103
|
-
}
|
|
1104
|
-
const { t, v } = arg;
|
|
1105
|
-
if (t === 'string') {
|
|
1106
|
-
const s = document.createElement('span');
|
|
1107
|
-
s.className = 'log-text';
|
|
1108
|
-
s.textContent = v;
|
|
1109
|
-
return s;
|
|
1110
|
-
}
|
|
1111
|
-
if (t === 'number') { return createPrimitiveSpan(v); }
|
|
1112
|
-
if (t === 'boolean') { return createPrimitiveSpan(v); }
|
|
1113
|
-
if (t === 'null') { return createPrimitiveSpan(null); }
|
|
1114
|
-
if (t === 'undefined') { return createPrimitiveSpan(undefined); }
|
|
1115
|
-
if (t === 'object' || t === 'array') {
|
|
1116
|
-
return createTreeNode(null, v, false);
|
|
1117
|
-
}
|
|
1118
|
-
const s = document.createElement('span');
|
|
1119
|
-
s.textContent = _safeStr(v);
|
|
1120
|
-
return s;
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
// Build the body of a console log row. If structured args exist, render each;
|
|
1124
|
-
// otherwise fall back to the flat message string and try to detect JSON in it.
|
|
1125
|
-
function buildLogBody(logEntry) {
|
|
1126
|
-
const container = document.createElement('div');
|
|
1127
|
-
container.className = 'log-body';
|
|
1128
|
-
|
|
1129
|
-
if (logEntry.args && Array.isArray(logEntry.args) && logEntry.args.length > 0) {
|
|
1130
|
-
// Structured args from updated SDK
|
|
1131
|
-
logEntry.args.forEach((arg, i) => {
|
|
1132
|
-
if (i > 0) container.appendChild(document.createTextNode(' '));
|
|
1133
|
-
container.appendChild(renderConsoleArg(arg));
|
|
1134
|
-
});
|
|
1135
|
-
} else if (logEntry.message != null) {
|
|
1136
|
-
// Legacy / flat message — try to parse JSON objects out of it
|
|
1137
|
-
const msg = String(logEntry.message);
|
|
1138
|
-
// Try parsing the whole message as JSON
|
|
1139
|
-
try {
|
|
1140
|
-
const parsed = JSON.parse(msg);
|
|
1141
|
-
if (typeof parsed === 'object' && parsed !== null) {
|
|
1142
|
-
container.appendChild(createTreeNode(null, parsed, false));
|
|
1143
|
-
return container;
|
|
1144
|
-
}
|
|
1145
|
-
} catch {}
|
|
1146
|
-
|
|
1147
|
-
// Otherwise render as text, but look for embedded JSON blocks
|
|
1148
|
-
// If it looks like it contains JSON, try to pretty-render inline
|
|
1149
|
-
const jsonRe = /(\{[\s\S]*\}|\[[\s\S]*\])/;
|
|
1150
|
-
const match = msg.match(jsonRe);
|
|
1151
|
-
if (match && match[0].length > 2) {
|
|
1152
|
-
try {
|
|
1153
|
-
const parsed = JSON.parse(match[0]);
|
|
1154
|
-
// There's text before/after
|
|
1155
|
-
const before = msg.slice(0, match.index);
|
|
1156
|
-
const after = msg.slice(match.index + match[0].length);
|
|
1157
|
-
if (before) container.appendChild(document.createTextNode(before));
|
|
1158
|
-
container.appendChild(createTreeNode(null, parsed, false));
|
|
1159
|
-
if (after) container.appendChild(document.createTextNode(after));
|
|
1160
|
-
return container;
|
|
1161
|
-
} catch {}
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
// Plain text
|
|
1165
|
-
const span = document.createElement('span');
|
|
1166
|
-
span.className = 'log-text';
|
|
1167
|
-
span.textContent = msg;
|
|
1168
|
-
container.appendChild(span);
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
return container;
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
function buildLogRow(l) {
|
|
1175
|
-
const div = document.createElement('div');
|
|
1176
|
-
div.className = `log-row entry ${l.level}`;
|
|
1177
|
-
|
|
1178
|
-
const timeSpan = document.createElement('span');
|
|
1179
|
-
timeSpan.className = 'log-time';
|
|
1180
|
-
timeSpan.textContent = ts(l.ts);
|
|
1181
|
-
div.appendChild(timeSpan);
|
|
1182
|
-
|
|
1183
|
-
const lvlSpan = document.createElement('span');
|
|
1184
|
-
lvlSpan.className = `lvl-badge lvl-${l.level}`;
|
|
1185
|
-
lvlSpan.textContent = l.level;
|
|
1186
|
-
div.appendChild(lvlSpan);
|
|
1187
|
-
|
|
1188
|
-
// Arrow (inline, not inside body-wrap)
|
|
1189
|
-
const arrow = document.createElement('span');
|
|
1190
|
-
arrow.className = 'log-arrow';
|
|
1191
|
-
arrow.textContent = '\u25B6';
|
|
1192
|
-
div.appendChild(arrow);
|
|
1193
|
-
|
|
1194
|
-
// Body wrapper
|
|
1195
|
-
const bodyWrap = document.createElement('div');
|
|
1196
|
-
bodyWrap.className = 'log-body-wrap';
|
|
1197
|
-
|
|
1198
|
-
// Single-line preview: message text + caller
|
|
1199
|
-
const preview = document.createElement('div');
|
|
1200
|
-
preview.className = 'log-preview';
|
|
1201
|
-
const msgText = (l.message || '').replace(/\n/g, ' ').slice(0, 200);
|
|
1202
|
-
const previewText = document.createElement('span');
|
|
1203
|
-
previewText.textContent = msgText + ((l.message || '').length > 200 ? '...' : '');
|
|
1204
|
-
preview.appendChild(previewText);
|
|
1205
|
-
bodyWrap.appendChild(preview);
|
|
1206
|
-
|
|
1207
|
-
// Full content (hidden by default)
|
|
1208
|
-
const full = document.createElement('div');
|
|
1209
|
-
full.className = 'log-full';
|
|
1210
|
-
full.style.display = 'none';
|
|
1211
|
-
full.appendChild(buildLogBody(l));
|
|
1212
|
-
bodyWrap.appendChild(full);
|
|
1213
|
-
|
|
1214
|
-
let expanded = false;
|
|
1215
|
-
// Only toggle on click, NOT on text selection drag
|
|
1216
|
-
let _mouseDownPos = null;
|
|
1217
|
-
bodyWrap.addEventListener('mousedown', (e) => {
|
|
1218
|
-
_mouseDownPos = { x: e.clientX, y: e.clientY };
|
|
1219
|
-
});
|
|
1220
|
-
bodyWrap.addEventListener('click', (e) => {
|
|
1221
|
-
// Don't toggle if user is selecting text (dragged mouse)
|
|
1222
|
-
if (_mouseDownPos) {
|
|
1223
|
-
const dx = Math.abs(e.clientX - _mouseDownPos.x);
|
|
1224
|
-
const dy = Math.abs(e.clientY - _mouseDownPos.y);
|
|
1225
|
-
if (dx > 3 || dy > 3) return; // user dragged to select
|
|
1226
|
-
}
|
|
1227
|
-
// Don't toggle if there's an active text selection
|
|
1228
|
-
const sel = window.getSelection();
|
|
1229
|
-
if (sel && sel.toString().length > 0) return;
|
|
1230
|
-
// Don't toggle if clicking inside object tree expander
|
|
1231
|
-
if (e.target.closest('.ov-header')) return;
|
|
1232
|
-
expanded = !expanded;
|
|
1233
|
-
if (expanded) {
|
|
1234
|
-
preview.style.display = 'none';
|
|
1235
|
-
full.style.display = 'block';
|
|
1236
|
-
arrow.textContent = '\u25BC';
|
|
1237
|
-
arrow.classList.add('expanded');
|
|
1238
|
-
} else {
|
|
1239
|
-
preview.style.display = '';
|
|
1240
|
-
full.style.display = 'none';
|
|
1241
|
-
arrow.textContent = '\u25B6';
|
|
1242
|
-
arrow.classList.remove('expanded');
|
|
1243
|
-
}
|
|
1244
|
-
});
|
|
1245
|
-
|
|
1246
|
-
// Right-click → copy options
|
|
1247
|
-
div.addEventListener('contextmenu', (e) => {
|
|
1248
|
-
e.preventDefault();
|
|
1249
|
-
const items = [];
|
|
1250
|
-
|
|
1251
|
-
// Copy selected text
|
|
1252
|
-
const sel = window.getSelection();
|
|
1253
|
-
if (sel && sel.toString().length > 0) {
|
|
1254
|
-
items.push({ label: 'Copy Selection', action: () => navigator.clipboard.writeText(sel.toString()) });
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
// Copy full log message
|
|
1258
|
-
items.push({ label: 'Copy Message', action: () => {
|
|
1259
|
-
navigator.clipboard.writeText(l.message || '');
|
|
1260
|
-
}});
|
|
1261
|
-
|
|
1262
|
-
// Copy as JSON (if structured args exist)
|
|
1263
|
-
if (l.args && l.args.length > 0) {
|
|
1264
|
-
items.push({ label: 'Copy as JSON', action: () => {
|
|
1265
|
-
const json = l.args.map(a => {
|
|
1266
|
-
if (a.t === 'object' || a.t === 'array') return JSON.stringify(a.v, null, 2);
|
|
1267
|
-
return String(a.v);
|
|
1268
|
-
}).join(' ');
|
|
1269
|
-
navigator.clipboard.writeText(json);
|
|
1270
|
-
}});
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
// Copy caller location
|
|
1274
|
-
if (l.caller) {
|
|
1275
|
-
items.push({ label: 'Copy Caller', action: () => navigator.clipboard.writeText(l.caller) });
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
showContextMenu(e, items);
|
|
1279
|
-
});
|
|
1280
|
-
|
|
1281
|
-
div.appendChild(bodyWrap);
|
|
1282
|
-
return div;
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
// ─── Shared context menu helper ──────────────────────────────────────────────
|
|
1286
|
-
function showContextMenu(e, items) {
|
|
1287
|
-
document.querySelectorAll('.ctx-menu').forEach(el => el.remove());
|
|
1288
|
-
const menu = document.createElement('div');
|
|
1289
|
-
menu.className = 'ctx-menu';
|
|
1290
|
-
items.forEach(({ label, action }) => {
|
|
1291
|
-
if (label === '—' || !action) {
|
|
1292
|
-
const sep = document.createElement('div');
|
|
1293
|
-
sep.className = 'ctx-sep';
|
|
1294
|
-
menu.appendChild(sep);
|
|
1295
|
-
return;
|
|
1296
|
-
}
|
|
1297
|
-
const item = document.createElement('div');
|
|
1298
|
-
item.className = 'ctx-item';
|
|
1299
|
-
item.textContent = label;
|
|
1300
|
-
item.addEventListener('click', () => { action(); menu.remove(); });
|
|
1301
|
-
menu.appendChild(item);
|
|
1302
|
-
});
|
|
1303
|
-
menu.style.left = Math.min(e.clientX, window.innerWidth - 200) + 'px';
|
|
1304
|
-
menu.style.top = Math.min(e.clientY, window.innerHeight - items.length * 32 - 10) + 'px';
|
|
1305
|
-
document.body.appendChild(menu);
|
|
1306
|
-
setTimeout(() => {
|
|
1307
|
-
const close = (ev) => { if (!menu.contains(ev.target)) { menu.remove(); document.removeEventListener('click', close); } };
|
|
1308
|
-
document.addEventListener('click', close);
|
|
1309
|
-
}, 0);
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
// Full re-render — only used on filter/level change, NOT on every incoming log
|
|
1313
|
-
function renderConsole() {
|
|
1314
|
-
const list = $('consoleList');
|
|
1315
|
-
const empty = $('consoleEmpty');
|
|
1316
|
-
if (!list) return;
|
|
1317
|
-
|
|
1318
|
-
const { levelFilters, searchFilter } = state.console;
|
|
1319
|
-
const visible = state.console.logs.filter(l => {
|
|
1320
|
-
// Redux logs use showRedux flag; regular logs use levelFilters
|
|
1321
|
-
if (l.level === 'redux') {
|
|
1322
|
-
if (!state.console.showRedux) return false;
|
|
1323
|
-
} else if (levelFilters && !levelFilters[l.level]) return false;
|
|
1324
|
-
if (searchFilter && !l.message?.toLowerCase().includes(searchFilter)) return false;
|
|
1325
|
-
return true;
|
|
1326
|
-
});
|
|
1327
|
-
|
|
1328
|
-
list.querySelectorAll('.log-row').forEach(e => e.remove());
|
|
1329
|
-
if (!empty) { /* guard */ }
|
|
1330
|
-
else if (visible.length > 0) {
|
|
1331
|
-
empty.style.display = 'none';
|
|
1332
|
-
} else if (state.console.logs.length > 0) {
|
|
1333
|
-
const lbl = empty.querySelector('.label');
|
|
1334
|
-
const hint = empty.querySelector('.hint');
|
|
1335
|
-
if (lbl) lbl.textContent = 'No matching logs';
|
|
1336
|
-
if (hint) hint.textContent = 'Adjust level filters or clear search to see logs';
|
|
1337
|
-
empty.style.display = 'flex';
|
|
1338
|
-
} else {
|
|
1339
|
-
const lbl = empty.querySelector('.label');
|
|
1340
|
-
const hint = empty.querySelector('.hint');
|
|
1341
|
-
if (lbl) lbl.textContent = 'No logs yet';
|
|
1342
|
-
if (hint) hint.textContent = 'Logs will appear here automatically';
|
|
1343
|
-
empty.style.display = 'flex';
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
// Render only the last N visible rows for performance
|
|
1347
|
-
const MAX_RENDER = 5000;
|
|
1348
|
-
const toRender = visible.length > MAX_RENDER ? visible.slice(-MAX_RENDER) : visible;
|
|
1349
|
-
if (visible.length > MAX_RENDER) {
|
|
1350
|
-
const info = document.createElement('div');
|
|
1351
|
-
info.className = 'log-row';
|
|
1352
|
-
info.style.cssText = 'color:var(--text-dim);font-size:10px;padding:6px 14px;text-align:center;font-style:italic';
|
|
1353
|
-
info.textContent = `${visible.length - MAX_RENDER} older logs hidden for performance`;
|
|
1354
|
-
list.appendChild(info);
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
const frag = document.createDocumentFragment();
|
|
1358
|
-
toRender.forEach(l => frag.appendChild(buildLogRow(l)));
|
|
1359
|
-
list.appendChild(frag);
|
|
1360
|
-
list.scrollTop = list.scrollHeight;
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1364
|
-
// NETWORK PANEL (Chrome DevTools-style)
|
|
1365
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1366
|
-
const NET_COLS = [
|
|
1367
|
-
{ key: 'name', label: 'Name', width: 380, min: 150 },
|
|
1368
|
-
{ key: 'status', label: 'Status', width: 60, min: 40 },
|
|
1369
|
-
{ key: 'type', label: 'Type', width: 70, min: 40 },
|
|
1370
|
-
{ key: 'initiator', label: 'Initiator', width: 80, min: 50 },
|
|
1371
|
-
{ key: 'size', label: 'Size', width: 65, min: 40 },
|
|
1372
|
-
{ key: 'time', label: 'Time', width: 65, min: 40 },
|
|
1373
|
-
{ key: 'waterfall', label: 'Waterfall', width: 100, min: 60 },
|
|
1374
|
-
];
|
|
1375
|
-
|
|
1376
|
-
function initNetworkPanel() {
|
|
1377
|
-
const panel = $('panel-network');
|
|
1378
|
-
panel.innerHTML = `
|
|
1379
|
-
<div class="panel-toolbar">
|
|
1380
|
-
<span class="panel-label">Network</span>
|
|
1381
|
-
<span class="badge" id="nBadge">0</span>
|
|
1382
|
-
<div class="ml-auto" style="display:flex;align-items:center;gap:6px">
|
|
1383
|
-
<button class="panel-clear-btn" id="networkExport" title="Export as HAR">Export HAR</button>
|
|
1384
|
-
<button class="panel-clear-btn" id="networkClear" title="Clear network">Clear</button>
|
|
1385
|
-
<label class="toggle-label" for="netToggle">
|
|
1386
|
-
<span class="toggle-text" id="netToggleText">Capture ON</span>
|
|
1387
|
-
<input type="checkbox" id="netToggle" class="toggle-input" checked />
|
|
1388
|
-
<span class="toggle-slider"></span>
|
|
1389
|
-
</label>
|
|
1390
|
-
</div>
|
|
1391
|
-
</div>
|
|
1392
|
-
<div class="net-filter-bar" id="netFilterBar">
|
|
1393
|
-
<input id="netSearchInput" class="net-search-input" placeholder="Filter URLs..." />
|
|
1394
|
-
<div class="net-type-filters" id="netTypeFilters">
|
|
1395
|
-
<button class="net-type-btn" data-type="all">All</button>
|
|
1396
|
-
<button class="net-type-btn active" data-type="fetch">Fetch/XHR</button>
|
|
1397
|
-
<button class="net-type-btn" data-type="js">JS</button>
|
|
1398
|
-
<button class="net-type-btn" data-type="css">CSS</button>
|
|
1399
|
-
<button class="net-type-btn" data-type="img">Img</button>
|
|
1400
|
-
<button class="net-type-btn" data-type="media">Media</button>
|
|
1401
|
-
<button class="net-type-btn" data-type="font">Font</button>
|
|
1402
|
-
<button class="net-type-btn" data-type="doc">Doc</button>
|
|
1403
|
-
<button class="net-type-btn" data-type="ws">WS</button>
|
|
1404
|
-
</div>
|
|
1405
|
-
<div class="net-status-filters" id="netStatusFilters">
|
|
1406
|
-
<button class="net-status-btn active" data-status="all">All</button>
|
|
1407
|
-
<button class="net-status-btn" data-status="2xx">2xx</button>
|
|
1408
|
-
<button class="net-status-btn" data-status="errors">Errors</button>
|
|
1409
|
-
<button class="net-status-btn net-slow-btn" data-status="slow">Slow (>1s)</button>
|
|
1410
|
-
</div>
|
|
1411
|
-
<div class="net-hidden-wrap" style="position:relative;margin-left:4px">
|
|
1412
|
-
<button class="net-status-btn net-hidden-btn" id="netHiddenBtn" style="display:none" title="Manage hidden URLs">Hidden</button>
|
|
1413
|
-
<div class="net-hidden-dropdown" id="netHiddenDropdown" style="display:none"></div>
|
|
1414
|
-
</div>
|
|
1415
|
-
<div class="net-throttle" id="netThrottle">
|
|
1416
|
-
<select id="netThrottleSelect" class="net-throttle-select">
|
|
1417
|
-
<option value="none">No throttling</option>
|
|
1418
|
-
<option value="fast3g">Fast 3G</option>
|
|
1419
|
-
<option value="slow3g">Slow 3G</option>
|
|
1420
|
-
<option value="offline">Offline</option>
|
|
1421
|
-
</select>
|
|
1422
|
-
</div>
|
|
1423
|
-
</div>
|
|
1424
|
-
<div class="net-layout">
|
|
1425
|
-
<div class="net-table-wrap" id="netTableWrap">
|
|
1426
|
-
<div class="net-header" id="netHeader"></div>
|
|
1427
|
-
<div class="net-rows" id="netRows">
|
|
1428
|
-
<div class="empty-state" id="networkEmpty">
|
|
1429
|
-
<div class="icon">📡</div>
|
|
1430
|
-
<div class="label">No requests yet</div>
|
|
1431
|
-
<div class="hint">API calls will appear here automatically</div>
|
|
1432
|
-
</div>
|
|
1433
|
-
</div>
|
|
1434
|
-
</div>
|
|
1435
|
-
<div class="net-detail-pane" id="netDetailPane">
|
|
1436
|
-
<div class="net-detail-bar">
|
|
1437
|
-
<div class="detail-tabs" id="netDetailTabs"></div>
|
|
1438
|
-
<div class="detail-search-wrap" id="detailSearchWrap" style="display:none">
|
|
1439
|
-
<input id="detailSearchInput" class="detail-search-input" placeholder="Search key or value..." />
|
|
1440
|
-
<span id="detailSearchCount" class="detail-search-count"></span>
|
|
1441
|
-
<button class="detail-search-nav" id="detailSearchPrev" title="Previous">▲</button>
|
|
1442
|
-
<button class="detail-search-nav" id="detailSearchNext" title="Next">▼</button>
|
|
1443
|
-
<button class="detail-search-close" id="detailSearchClose" title="Close search">×</button>
|
|
1444
|
-
</div>
|
|
1445
|
-
<button class="detail-close" id="netDetailClose" title="Close">×</button>
|
|
1446
|
-
</div>
|
|
1447
|
-
<div class="detail-content" id="netDetailContent"></div>
|
|
1448
|
-
</div>
|
|
1449
|
-
</div>
|
|
1450
|
-
<div class="net-stats-bar" id="netStatsBar">
|
|
1451
|
-
<span id="netStatsTotal">0 requests</span>
|
|
1452
|
-
<span class="net-stats-sep">|</span>
|
|
1453
|
-
<span id="netStatsAvg">Avg: —</span>
|
|
1454
|
-
<span class="net-stats-sep">|</span>
|
|
1455
|
-
<span id="netStatsSlowest">Slowest: —</span>
|
|
1456
|
-
<span class="net-stats-sep">|</span>
|
|
1457
|
-
<span id="netStatsErrors">Errors: 0</span>
|
|
1458
|
-
<span class="net-stats-sep">|</span>
|
|
1459
|
-
<span id="netStatsSlow">Slow (>1s): 0</span>
|
|
1460
|
-
</div>`;
|
|
1461
|
-
|
|
1462
|
-
$('netToggle').addEventListener('change', (e) => {
|
|
1463
|
-
state.network.enabled = e.target.checked;
|
|
1464
|
-
$('netToggleText').textContent = e.target.checked ? 'Capture ON' : 'Capture OFF';
|
|
1465
|
-
window.electronAPI?.setNetworkCapture(e.target.checked);
|
|
1466
|
-
});
|
|
1467
|
-
|
|
1468
|
-
// Network search input
|
|
1469
|
-
$('netSearchInput').addEventListener('input', (e) => {
|
|
1470
|
-
state.network.searchFilter = e.target.value.toLowerCase().trim();
|
|
1471
|
-
renderNetwork();
|
|
1472
|
-
});
|
|
1473
|
-
|
|
1474
|
-
// Type filter buttons
|
|
1475
|
-
$('netTypeFilters').addEventListener('click', (e) => {
|
|
1476
|
-
const btn = e.target.closest('.net-type-btn');
|
|
1477
|
-
if (!btn) return;
|
|
1478
|
-
$('netTypeFilters').querySelectorAll('.net-type-btn').forEach(b => b.classList.remove('active'));
|
|
1479
|
-
btn.classList.add('active');
|
|
1480
|
-
state.network.typeFilter = btn.dataset.type;
|
|
1481
|
-
renderNetwork();
|
|
1482
|
-
});
|
|
1483
|
-
|
|
1484
|
-
// Status filter buttons (All / 2xx / Errors / Slow)
|
|
1485
|
-
$('netStatusFilters').addEventListener('click', (e) => {
|
|
1486
|
-
const btn = e.target.closest('.net-status-btn');
|
|
1487
|
-
if (!btn) return;
|
|
1488
|
-
$('netStatusFilters').querySelectorAll('.net-status-btn').forEach(b => b.classList.remove('active'));
|
|
1489
|
-
btn.classList.add('active');
|
|
1490
|
-
state.network.statusFilter = btn.dataset.status;
|
|
1491
|
-
renderNetwork();
|
|
1492
|
-
});
|
|
1493
|
-
|
|
1494
|
-
// Hidden URLs button
|
|
1495
|
-
$('netHiddenBtn')?.addEventListener('click', () => {
|
|
1496
|
-
const dd = $('netHiddenDropdown');
|
|
1497
|
-
if (!dd) return;
|
|
1498
|
-
const isOpen = dd.style.display !== 'none';
|
|
1499
|
-
if (isOpen) { dd.style.display = 'none'; return; }
|
|
1500
|
-
// Build dropdown with hidden URL list
|
|
1501
|
-
const hidden = getHiddenURLs();
|
|
1502
|
-
dd.innerHTML = '';
|
|
1503
|
-
if (!hidden.length) { dd.style.display = 'none'; return; }
|
|
1504
|
-
const title = document.createElement('div');
|
|
1505
|
-
title.className = 'net-hidden-title';
|
|
1506
|
-
title.innerHTML = `<span>Hidden URLs (${hidden.length})</span><button class="net-hidden-clear" id="netHiddenClearAll">Clear All</button>`;
|
|
1507
|
-
dd.appendChild(title);
|
|
1508
|
-
hidden.forEach(pattern => {
|
|
1509
|
-
const row = document.createElement('div');
|
|
1510
|
-
row.className = 'net-hidden-row';
|
|
1511
|
-
const label = document.createElement('span');
|
|
1512
|
-
label.className = 'net-hidden-url';
|
|
1513
|
-
label.textContent = pattern;
|
|
1514
|
-
label.title = pattern;
|
|
1515
|
-
row.appendChild(label);
|
|
1516
|
-
const btn = document.createElement('button');
|
|
1517
|
-
btn.className = 'net-hidden-unhide';
|
|
1518
|
-
btn.textContent = 'Unhide';
|
|
1519
|
-
btn.addEventListener('click', () => {
|
|
1520
|
-
removeHiddenURL(pattern);
|
|
1521
|
-
row.remove();
|
|
1522
|
-
renderNetwork();
|
|
1523
|
-
if (!getHiddenURLs().length) dd.style.display = 'none';
|
|
1524
|
-
});
|
|
1525
|
-
row.appendChild(btn);
|
|
1526
|
-
dd.appendChild(row);
|
|
1527
|
-
});
|
|
1528
|
-
dd.style.display = 'block';
|
|
1529
|
-
// Clear all handler
|
|
1530
|
-
dd.querySelector('#netHiddenClearAll')?.addEventListener('click', () => {
|
|
1531
|
-
setHiddenURLs([]);
|
|
1532
|
-
_updateHiddenBadge();
|
|
1533
|
-
dd.style.display = 'none';
|
|
1534
|
-
renderNetwork();
|
|
1535
|
-
});
|
|
1536
|
-
});
|
|
1537
|
-
// Close dropdown when clicking outside
|
|
1538
|
-
document.addEventListener('click', (e) => {
|
|
1539
|
-
const dd = $('netHiddenDropdown');
|
|
1540
|
-
if (dd && dd.style.display !== 'none' && !e.target.closest('.net-hidden-wrap')) {
|
|
1541
|
-
dd.style.display = 'none';
|
|
1542
|
-
}
|
|
1543
|
-
});
|
|
1544
|
-
// Initialize hidden badge
|
|
1545
|
-
_updateHiddenBadge();
|
|
1546
|
-
|
|
1547
|
-
// Throttle select
|
|
1548
|
-
$('netThrottleSelect').addEventListener('change', (e) => {
|
|
1549
|
-
state.network.throttle = e.target.value;
|
|
1550
|
-
// Send throttle config to the RN app
|
|
1551
|
-
window.electronAPI?.setNetworkThrottle(state.network.throttle);
|
|
1552
|
-
});
|
|
1553
|
-
|
|
1554
|
-
// Export network as HAR
|
|
1555
|
-
$('networkExport')?.addEventListener('click', () => {
|
|
1556
|
-
const entries = state.network.order.map(id => {
|
|
1557
|
-
const r = state.network.requests[id];
|
|
1558
|
-
if (!r) return null;
|
|
1559
|
-
return {
|
|
1560
|
-
startedDateTime: new Date(r.ts || Date.now()).toISOString(),
|
|
1561
|
-
time: r.duration || 0,
|
|
1562
|
-
request: {
|
|
1563
|
-
method: r.method || 'GET',
|
|
1564
|
-
url: r.url || '',
|
|
1565
|
-
headers: Object.entries(r.requestHeaders || {}).map(([n, v]) => ({ name: n, value: v })),
|
|
1566
|
-
postData: r.requestBody ? { mimeType: 'application/json', text: typeof r.requestBody === 'object' ? JSON.stringify(r.requestBody) : String(r.requestBody) } : undefined,
|
|
1567
|
-
},
|
|
1568
|
-
response: {
|
|
1569
|
-
status: r.status || 0,
|
|
1570
|
-
statusText: r.statusText || '',
|
|
1571
|
-
headers: Object.entries(r.responseHeaders || {}).map(([n, v]) => ({ name: n, value: v })),
|
|
1572
|
-
content: { size: -1, mimeType: 'application/json', text: r.responseBody ? (typeof r.responseBody === 'object' ? JSON.stringify(r.responseBody) : String(r.responseBody)) : '' },
|
|
1573
|
-
},
|
|
1574
|
-
timings: { send: 0, wait: r.duration || 0, receive: 0 },
|
|
1575
|
-
};
|
|
1576
|
-
}).filter(Boolean);
|
|
1577
|
-
const har = { log: { version: '1.2', creator: { name: 'ReactoRadar', version: '1.6.0' }, entries } };
|
|
1578
|
-
const blob = new Blob([JSON.stringify(har, null, 2)], { type: 'application/json' });
|
|
1579
|
-
const url = URL.createObjectURL(blob);
|
|
1580
|
-
const a = document.createElement('a');
|
|
1581
|
-
a.href = url; a.download = `reactoradar-network-${Date.now()}.har`; a.click();
|
|
1582
|
-
URL.revokeObjectURL(url);
|
|
1583
|
-
});
|
|
1584
|
-
|
|
1585
|
-
// Clear network
|
|
1586
|
-
$('networkClear').addEventListener('click', () => {
|
|
1587
|
-
state.network.requests = {};
|
|
1588
|
-
state.network.order = [];
|
|
1589
|
-
state.network.selectedId = null;
|
|
1590
|
-
closeNetDetail();
|
|
1591
|
-
$('nBadge').textContent = '0';
|
|
1592
|
-
renderNetwork();
|
|
1593
|
-
});
|
|
1594
|
-
|
|
1595
|
-
// Close detail button
|
|
1596
|
-
$('netDetailClose').addEventListener('click', closeNetDetail);
|
|
1597
|
-
|
|
1598
|
-
// Detail panel search
|
|
1599
|
-
let _detailSearchMatches = [];
|
|
1600
|
-
let _detailSearchIdx = -1;
|
|
1601
|
-
|
|
1602
|
-
function _detailSearch() {
|
|
1603
|
-
const term = $('detailSearchInput')?.value?.trim().toLowerCase();
|
|
1604
|
-
const body = $('netDetailContent');
|
|
1605
|
-
if (!body || !term) { _detailClearSearch(); return; }
|
|
1606
|
-
|
|
1607
|
-
// Remove old highlights
|
|
1608
|
-
body.querySelectorAll('.detail-search-hl').forEach(el => {
|
|
1609
|
-
const parent = el.parentNode;
|
|
1610
|
-
parent.replaceChild(document.createTextNode(el.textContent), el);
|
|
1611
|
-
parent.normalize();
|
|
1612
|
-
});
|
|
1613
|
-
|
|
1614
|
-
_detailSearchMatches = [];
|
|
1615
|
-
_detailSearchIdx = -1;
|
|
1616
|
-
|
|
1617
|
-
// Walk all text nodes and highlight matches
|
|
1618
|
-
const walker = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, null);
|
|
1619
|
-
const textNodes = [];
|
|
1620
|
-
while (walker.nextNode()) textNodes.push(walker.currentNode);
|
|
1621
|
-
|
|
1622
|
-
textNodes.forEach(node => {
|
|
1623
|
-
const text = node.textContent;
|
|
1624
|
-
const lower = text.toLowerCase();
|
|
1625
|
-
if (!lower.includes(term)) return;
|
|
1626
|
-
|
|
1627
|
-
const frag = document.createDocumentFragment();
|
|
1628
|
-
let lastIdx = 0;
|
|
1629
|
-
let idx;
|
|
1630
|
-
while ((idx = lower.indexOf(term, lastIdx)) !== -1) {
|
|
1631
|
-
if (idx > lastIdx) frag.appendChild(document.createTextNode(text.slice(lastIdx, idx)));
|
|
1632
|
-
const hl = document.createElement('span');
|
|
1633
|
-
hl.className = 'detail-search-hl';
|
|
1634
|
-
hl.textContent = text.slice(idx, idx + term.length);
|
|
1635
|
-
_detailSearchMatches.push(hl);
|
|
1636
|
-
frag.appendChild(hl);
|
|
1637
|
-
lastIdx = idx + term.length;
|
|
1638
|
-
}
|
|
1639
|
-
if (lastIdx < text.length) frag.appendChild(document.createTextNode(text.slice(lastIdx)));
|
|
1640
|
-
node.parentNode.replaceChild(frag, node);
|
|
1641
|
-
});
|
|
1642
|
-
|
|
1643
|
-
// Update count
|
|
1644
|
-
const countEl = $('detailSearchCount');
|
|
1645
|
-
if (countEl) countEl.textContent = _detailSearchMatches.length ? `${_detailSearchMatches.length} found` : 'No match';
|
|
1646
|
-
|
|
1647
|
-
// Navigate to first match
|
|
1648
|
-
if (_detailSearchMatches.length) _detailNavTo(0);
|
|
1649
|
-
}
|
|
1650
|
-
|
|
1651
|
-
function _detailNavTo(idx) {
|
|
1652
|
-
// Remove active highlight from previous
|
|
1653
|
-
if (_detailSearchIdx >= 0 && _detailSearchMatches[_detailSearchIdx]) {
|
|
1654
|
-
_detailSearchMatches[_detailSearchIdx].classList.remove('active');
|
|
1655
|
-
}
|
|
1656
|
-
_detailSearchIdx = idx;
|
|
1657
|
-
const el = _detailSearchMatches[idx];
|
|
1658
|
-
if (!el) return;
|
|
1659
|
-
el.classList.add('active');
|
|
1660
|
-
el.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
1661
|
-
// Update count
|
|
1662
|
-
const countEl = $('detailSearchCount');
|
|
1663
|
-
if (countEl) countEl.textContent = `${idx + 1}/${_detailSearchMatches.length}`;
|
|
1664
|
-
}
|
|
1665
|
-
|
|
1666
|
-
function _detailClearSearch() {
|
|
1667
|
-
const body = $('netDetailContent');
|
|
1668
|
-
if (body) {
|
|
1669
|
-
body.querySelectorAll('.detail-search-hl').forEach(el => {
|
|
1670
|
-
const parent = el.parentNode;
|
|
1671
|
-
parent.replaceChild(document.createTextNode(el.textContent), el);
|
|
1672
|
-
parent.normalize();
|
|
1673
|
-
});
|
|
1674
|
-
}
|
|
1675
|
-
_detailSearchMatches = [];
|
|
1676
|
-
_detailSearchIdx = -1;
|
|
1677
|
-
const countEl = $('detailSearchCount');
|
|
1678
|
-
if (countEl) countEl.textContent = '';
|
|
1679
|
-
}
|
|
1680
|
-
|
|
1681
|
-
$('detailSearchInput')?.addEventListener('input', () => {
|
|
1682
|
-
clearTimeout($('detailSearchInput')._debounce);
|
|
1683
|
-
$('detailSearchInput')._debounce = setTimeout(_detailSearch, 200);
|
|
1684
|
-
});
|
|
1685
|
-
$('detailSearchInput')?.addEventListener('keydown', (e) => {
|
|
1686
|
-
if (e.key === 'Enter') {
|
|
1687
|
-
e.preventDefault();
|
|
1688
|
-
if (!_detailSearchMatches.length) return;
|
|
1689
|
-
const next = e.shiftKey
|
|
1690
|
-
? (_detailSearchIdx - 1 + _detailSearchMatches.length) % _detailSearchMatches.length
|
|
1691
|
-
: (_detailSearchIdx + 1) % _detailSearchMatches.length;
|
|
1692
|
-
_detailNavTo(next);
|
|
1693
|
-
}
|
|
1694
|
-
if (e.key === 'Escape') {
|
|
1695
|
-
_detailClearSearch();
|
|
1696
|
-
$('detailSearchWrap').style.display = 'none';
|
|
1697
|
-
}
|
|
1698
|
-
});
|
|
1699
|
-
$('detailSearchNext')?.addEventListener('click', () => {
|
|
1700
|
-
if (!_detailSearchMatches.length) return;
|
|
1701
|
-
_detailNavTo((_detailSearchIdx + 1) % _detailSearchMatches.length);
|
|
1702
|
-
});
|
|
1703
|
-
$('detailSearchPrev')?.addEventListener('click', () => {
|
|
1704
|
-
if (!_detailSearchMatches.length) return;
|
|
1705
|
-
_detailNavTo((_detailSearchIdx - 1 + _detailSearchMatches.length) % _detailSearchMatches.length);
|
|
1706
|
-
});
|
|
1707
|
-
$('detailSearchClose')?.addEventListener('click', () => {
|
|
1708
|
-
_detailClearSearch();
|
|
1709
|
-
$('detailSearchInput').value = '';
|
|
1710
|
-
$('detailSearchWrap').style.display = 'none';
|
|
1711
|
-
});
|
|
1712
|
-
|
|
1713
|
-
buildNetHeader();
|
|
1714
|
-
}
|
|
1715
|
-
|
|
1716
|
-
// ─── Column header with sort icons + full-height resize handles ──────────────
|
|
1717
|
-
function buildNetHeader() {
|
|
1718
|
-
const header = $('netHeader');
|
|
1719
|
-
header.innerHTML = '';
|
|
1720
|
-
NET_COLS.forEach((col, i) => {
|
|
1721
|
-
const cell = document.createElement('div');
|
|
1722
|
-
cell.className = 'net-hcell';
|
|
1723
|
-
cell.style.width = col.width + 'px';
|
|
1724
|
-
cell.dataset.col = col.key;
|
|
1725
|
-
|
|
1726
|
-
const label = document.createElement('span');
|
|
1727
|
-
label.className = 'net-hcell-label';
|
|
1728
|
-
label.textContent = col.label;
|
|
1729
|
-
cell.appendChild(label);
|
|
1730
|
-
|
|
1731
|
-
if (col.key !== 'waterfall') {
|
|
1732
|
-
const sortIcon = document.createElement('span');
|
|
1733
|
-
sortIcon.className = 'net-sort-icon';
|
|
1734
|
-
if (state.network.sortCol === col.key) {
|
|
1735
|
-
sortIcon.textContent = state.network.sortDir === 'asc' ? ' \u25B2' : ' \u25BC';
|
|
1736
|
-
sortIcon.classList.add('active');
|
|
1737
|
-
}
|
|
1738
|
-
cell.appendChild(sortIcon);
|
|
1739
|
-
cell.addEventListener('click', (e) => {
|
|
1740
|
-
if (e.target.closest('.net-hcell-resize')) return;
|
|
1741
|
-
if (state.network.sortCol === col.key) {
|
|
1742
|
-
state.network.sortDir = state.network.sortDir === 'asc' ? 'desc' : 'asc';
|
|
1743
|
-
} else {
|
|
1744
|
-
state.network.sortCol = col.key;
|
|
1745
|
-
state.network.sortDir = col.key === 'name' ? 'asc' : 'desc';
|
|
1746
|
-
}
|
|
1747
|
-
buildNetHeader();
|
|
1748
|
-
renderNetwork();
|
|
1749
|
-
});
|
|
1750
|
-
cell.style.cursor = 'pointer';
|
|
1751
|
-
}
|
|
1752
|
-
|
|
1753
|
-
// Resize handle in header
|
|
1754
|
-
if (i < NET_COLS.length - 1) {
|
|
1755
|
-
const handle = document.createElement('div');
|
|
1756
|
-
handle.className = 'net-hcell-resize';
|
|
1757
|
-
handle.addEventListener('mousedown', (e) => startColResize(e, col));
|
|
1758
|
-
cell.appendChild(handle);
|
|
1759
|
-
}
|
|
1760
|
-
header.appendChild(cell);
|
|
1761
|
-
});
|
|
1762
|
-
|
|
1763
|
-
// Build full-height resize overlay lines
|
|
1764
|
-
buildResizeOverlays();
|
|
1765
|
-
}
|
|
1766
|
-
|
|
1767
|
-
function buildResizeOverlays() {
|
|
1768
|
-
// Remove old overlays
|
|
1769
|
-
document.querySelectorAll('.net-resize-overlay').forEach(e => e.remove());
|
|
1770
|
-
const tableWrap = $('netTableWrap');
|
|
1771
|
-
if (!tableWrap) return;
|
|
1772
|
-
// Make the table wrap position:relative for overlay positioning
|
|
1773
|
-
tableWrap.style.position = 'relative';
|
|
1774
|
-
|
|
1775
|
-
let leftOffset = 0;
|
|
1776
|
-
NET_COLS.forEach((col, i) => {
|
|
1777
|
-
leftOffset += col.width;
|
|
1778
|
-
if (i >= NET_COLS.length - 1) return; // no handle after last column
|
|
1779
|
-
|
|
1780
|
-
const overlay = document.createElement('div');
|
|
1781
|
-
overlay.className = 'net-resize-overlay';
|
|
1782
|
-
overlay.style.left = (leftOffset - 3) + 'px';
|
|
1783
|
-
overlay.addEventListener('mousedown', (e) => startColResize(e, col));
|
|
1784
|
-
tableWrap.appendChild(overlay);
|
|
1785
|
-
});
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
function startColResize(e, col) {
|
|
1789
|
-
e.preventDefault();
|
|
1790
|
-
e.stopPropagation();
|
|
1791
|
-
const startX = e.clientX;
|
|
1792
|
-
const startW = col.width;
|
|
1793
|
-
|
|
1794
|
-
// Add visual feedback
|
|
1795
|
-
document.body.style.cursor = 'col-resize';
|
|
1796
|
-
document.body.style.userSelect = 'none';
|
|
1797
|
-
|
|
1798
|
-
function onMove(ev) {
|
|
1799
|
-
const delta = ev.clientX - startX;
|
|
1800
|
-
col.width = Math.max(col.min, startW + delta);
|
|
1801
|
-
// Update header + all data cells for this column
|
|
1802
|
-
document.querySelectorAll(`.net-cell[data-col="${col.key}"], .net-hcell[data-col="${col.key}"]`)
|
|
1803
|
-
.forEach(el => el.style.width = col.width + 'px');
|
|
1804
|
-
// Keep detail pane aligned with Name column
|
|
1805
|
-
if (col.key === 'name' && state.network.selectedId) {
|
|
1806
|
-
const pane = $('netDetailPane');
|
|
1807
|
-
if (pane) pane.style.left = (col.width + 1) + 'px';
|
|
1808
|
-
}
|
|
1809
|
-
// Reposition overlays
|
|
1810
|
-
buildResizeOverlays();
|
|
1811
|
-
}
|
|
1812
|
-
function onUp() {
|
|
1813
|
-
document.body.style.cursor = '';
|
|
1814
|
-
document.body.style.userSelect = '';
|
|
1815
|
-
document.removeEventListener('mousemove', onMove);
|
|
1816
|
-
document.removeEventListener('mouseup', onUp);
|
|
1817
|
-
}
|
|
1818
|
-
document.addEventListener('mousemove', onMove);
|
|
1819
|
-
document.addEventListener('mouseup', onUp);
|
|
1820
|
-
}
|
|
1821
|
-
|
|
1822
|
-
// ─── Network type matching ──────────────────────────────────────────────────
|
|
1823
|
-
function matchNetType(r, type) {
|
|
1824
|
-
const ct = (r.responseHeaders?.['content-type'] || r.responseHeaders?.['Content-Type'] || '').toLowerCase();
|
|
1825
|
-
const url = (r.url || '').toLowerCase();
|
|
1826
|
-
switch (type) {
|
|
1827
|
-
case 'fetch': // Fetch/XHR — show API calls (JSON, text, form data), exclude static assets
|
|
1828
|
-
return !ct.includes('image') && !ct.includes('font') && !ct.includes('video') && !ct.includes('audio')
|
|
1829
|
-
&& !/\.(png|jpg|jpeg|gif|svg|webp|ico|woff2?|ttf|otf|eot|mp4|mp3|css)(\?|$)/.test(url);
|
|
1830
|
-
case 'js': return ct.includes('javascript') || /\.(js|jsx|bundle)(\?|$)/.test(url);
|
|
1831
|
-
case 'css': return ct.includes('css') || /\.css(\?|$)/.test(url);
|
|
1832
|
-
case 'img': return ct.includes('image') || /\.(png|jpg|jpeg|gif|svg|webp|ico|avif|bmp)(\?|$)/.test(url);
|
|
1833
|
-
case 'media': return ct.includes('video') || ct.includes('audio') || /\.(mp4|mp3|wav|webm|ogg|m3u8)(\?|$)/.test(url);
|
|
1834
|
-
case 'font': return ct.includes('font') || /\.(woff2?|ttf|otf|eot)(\?|$)/.test(url);
|
|
1835
|
-
case 'doc': return ct.includes('html') || ct.includes('xml') || /\.(html?|xml)(\?|$)/.test(url);
|
|
1836
|
-
case 'ws': return url.startsWith('ws://') || url.startsWith('wss://');
|
|
1837
|
-
default: return true;
|
|
1838
|
-
}
|
|
333
|
+
const origText = btn.innerHTML;
|
|
334
|
+
btn.innerHTML = '<span style="opacity:0.6">Saving...</span>';
|
|
335
|
+
window.electronAPI?.captureScreenshot();
|
|
336
|
+
btn.innerHTML = '<span style="color:var(--green)">Saved!</span>';
|
|
337
|
+
setTimeout(() => { btn.innerHTML = origText; }, 2000);
|
|
1839
338
|
}
|
|
1840
|
-
|
|
1841
|
-
let _netRAF = null;
|
|
1842
|
-
|
|
1843
|
-
function handleNetworkEvent(event) {
|
|
1844
|
-
if (event.type === 'console') { addConsoleLog(event); return; }
|
|
1845
|
-
if (event.type !== 'network') return;
|
|
1846
|
-
if (!state.network.enabled) return;
|
|
1847
|
-
|
|
1848
|
-
const { id, phase } = event;
|
|
1849
|
-
if (phase === 'request') {
|
|
1850
|
-
state.network.requests[id] = { ...event, _tab: 'headers' };
|
|
1851
|
-
if (!state.network.order.includes(id)) state.network.order.push(id);
|
|
1852
|
-
// Cap network history to prevent memory leak
|
|
1853
|
-
const MAX_NET_HISTORY = 1000;
|
|
1854
|
-
if (state.network.order.length > MAX_NET_HISTORY) {
|
|
1855
|
-
const trimIds = state.network.order.splice(0, state.network.order.length - MAX_NET_HISTORY);
|
|
1856
|
-
trimIds.forEach(tid => delete state.network.requests[tid]);
|
|
1857
|
-
}
|
|
1858
|
-
$('nBadge').textContent = state.network.order.length;
|
|
1859
|
-
} else {
|
|
1860
|
-
Object.assign(state.network.requests[id] || (state.network.requests[id] = {}), event);
|
|
1861
|
-
// Toast for errors and slow APIs
|
|
1862
|
-
const r = state.network.requests[id];
|
|
1863
|
-
if (r && (phase === 'response' || phase === 'error')) {
|
|
1864
|
-
const name = r.url?.split('/').pop()?.split('?')[0] || r.url || '?';
|
|
1865
|
-
if (r.phase === 'error' || (r.status && r.status >= 400)) {
|
|
1866
|
-
showToast(`API Error: ${r.status || 'ERR'} ${name}`, 'error', 'network');
|
|
1867
|
-
} else if ((r.duration || 0) >= 3000) {
|
|
1868
|
-
showToast(`Slow API: ${(r.duration/1000).toFixed(1)}s — ${name}`, 'warn', 'network');
|
|
1869
|
-
}
|
|
1870
|
-
}
|
|
1871
|
-
}
|
|
1872
|
-
if (!_netRAF) {
|
|
1873
|
-
_netRAF = requestAnimationFrame(() => {
|
|
1874
|
-
_netRAF = null;
|
|
1875
|
-
renderNetwork();
|
|
1876
|
-
});
|
|
1877
|
-
}
|
|
1878
|
-
}
|
|
1879
|
-
|
|
1880
|
-
// ─── Sort network IDs ───────────────────────────────────────────────────────
|
|
1881
|
-
function sortNetworkIds(ids) {
|
|
1882
|
-
const { sortCol, sortDir } = state.network;
|
|
1883
|
-
const reqs = state.network.requests;
|
|
1884
|
-
const sorted = [...ids].sort((a, b) => {
|
|
1885
|
-
const ra = reqs[a], rb = reqs[b];
|
|
1886
|
-
if (!ra || !rb) return 0;
|
|
1887
|
-
let va, vb;
|
|
1888
|
-
switch (sortCol) {
|
|
1889
|
-
case 'name':
|
|
1890
|
-
va = (ra.url || '').toLowerCase(); vb = (rb.url || '').toLowerCase();
|
|
1891
|
-
return va < vb ? -1 : va > vb ? 1 : 0;
|
|
1892
|
-
case 'status':
|
|
1893
|
-
va = ra.status || 0; vb = rb.status || 0;
|
|
1894
|
-
return va - vb;
|
|
1895
|
-
case 'type':
|
|
1896
|
-
va = (ra.responseHeaders?.['content-type'] || '').toLowerCase();
|
|
1897
|
-
vb = (rb.responseHeaders?.['content-type'] || '').toLowerCase();
|
|
1898
|
-
return va < vb ? -1 : va > vb ? 1 : 0;
|
|
1899
|
-
case 'size':
|
|
1900
|
-
// Use cached size or estimate — avoid JSON.stringify in sort comparator
|
|
1901
|
-
va = ra._cachedSize ?? (ra._cachedSize = typeof ra.responseBody === 'string' ? ra.responseBody.length : (ra.responseBody != null ? 100 : 0));
|
|
1902
|
-
vb = rb._cachedSize ?? (rb._cachedSize = typeof rb.responseBody === 'string' ? rb.responseBody.length : (rb.responseBody != null ? 100 : 0));
|
|
1903
|
-
return va - vb;
|
|
1904
|
-
case 'time':
|
|
1905
|
-
default:
|
|
1906
|
-
va = ra.ts || 0; vb = rb.ts || 0;
|
|
1907
|
-
return va - vb;
|
|
1908
|
-
}
|
|
1909
|
-
});
|
|
1910
|
-
if (sortDir === 'desc') sorted.reverse();
|
|
1911
|
-
return sorted;
|
|
1912
|
-
}
|
|
1913
|
-
|
|
1914
|
-
// ─── Render network rows ────────────────────────────────────────────────────
|
|
1915
|
-
function renderNetwork() {
|
|
1916
|
-
const rows = $('netRows');
|
|
1917
|
-
const empty = $('networkEmpty');
|
|
1918
|
-
if (!rows) return;
|
|
1919
|
-
|
|
1920
|
-
const { statusFilter, typeFilter, searchFilter } = state.network;
|
|
1921
|
-
const visible = state.network.order.filter(id => {
|
|
1922
|
-
const r = state.network.requests[id];
|
|
1923
|
-
if (!r) return false;
|
|
1924
|
-
if (statusFilter === '2xx' && !(r.status >= 200 && r.status < 300)) return false;
|
|
1925
|
-
if (statusFilter === 'errors' && !(r.phase === 'error' || r.status >= 400)) return false;
|
|
1926
|
-
if (statusFilter === 'slow' && !((r.duration || 0) >= 1000)) return false;
|
|
1927
|
-
if (searchFilter && !r.url?.toLowerCase().includes(searchFilter)) return false;
|
|
1928
|
-
if (typeFilter !== 'all' && !matchNetType(r, typeFilter)) return false;
|
|
1929
|
-
if (isURLHidden(r.url || '')) return false;
|
|
1930
|
-
return true;
|
|
1931
|
-
});
|
|
1932
|
-
|
|
1933
|
-
// Sort: apply current sort, default = newest first
|
|
1934
|
-
const sortedVisible = sortNetworkIds(visible);
|
|
1935
|
-
|
|
1936
|
-
empty.style.display = sortedVisible.length ? 'none' : 'flex';
|
|
1937
|
-
rows.querySelectorAll('.net-row').forEach(e => e.remove());
|
|
1938
|
-
|
|
1939
|
-
// Waterfall scale: find min/max timestamps
|
|
1940
|
-
let wfMin = Infinity, wfMax = 0;
|
|
1941
|
-
sortedVisible.forEach(id => {
|
|
1942
|
-
const r = state.network.requests[id];
|
|
1943
|
-
if (r.ts) { wfMin = Math.min(wfMin, r.ts); wfMax = Math.max(wfMax, r.ts + (r.duration || 0)); }
|
|
1944
|
-
});
|
|
1945
|
-
const wfRange = Math.max(wfMax - wfMin, 1);
|
|
1946
|
-
|
|
1947
|
-
// Render max 300 rows for performance
|
|
1948
|
-
const MAX_NET_ROWS = 300;
|
|
1949
|
-
const toRender = sortedVisible.length > MAX_NET_ROWS ? sortedVisible.slice(0, MAX_NET_ROWS) : sortedVisible;
|
|
1950
|
-
|
|
1951
|
-
const frag = document.createDocumentFragment();
|
|
1952
|
-
if (sortedVisible.length > MAX_NET_ROWS) {
|
|
1953
|
-
const info = document.createElement('div');
|
|
1954
|
-
info.className = 'net-row';
|
|
1955
|
-
info.style.cssText = 'color:var(--text-dim);font-size:10px;padding:6px 14px;justify-content:center;font-style:italic';
|
|
1956
|
-
info.textContent = `Showing ${MAX_NET_ROWS} of ${sortedVisible.length} requests`;
|
|
1957
|
-
frag.appendChild(info);
|
|
1958
|
-
}
|
|
1959
|
-
toRender.forEach(id => {
|
|
1960
|
-
const r = state.network.requests[id];
|
|
1961
|
-
frag.appendChild(buildNetRow(r, wfMin, wfRange));
|
|
1962
|
-
});
|
|
1963
|
-
rows.appendChild(frag);
|
|
1964
|
-
_updateNetStats();
|
|
1965
|
-
}
|
|
1966
|
-
|
|
1967
|
-
function _updateNetStats() {
|
|
1968
|
-
const allReqs = state.network.order.map(id => state.network.requests[id]).filter(Boolean);
|
|
1969
|
-
const completed = allReqs.filter(r => r.duration != null);
|
|
1970
|
-
const total = allReqs.length;
|
|
1971
|
-
const errors = allReqs.filter(r => r.phase === 'error' || (r.status && r.status >= 400)).length;
|
|
1972
|
-
const slow = completed.filter(r => r.duration >= 1000).length;
|
|
1973
|
-
const durations = completed.map(r => r.duration);
|
|
1974
|
-
const avg = durations.length ? Math.round(durations.reduce((a, b) => a + b, 0) / durations.length) : 0;
|
|
1975
|
-
const slowest = durations.length ? Math.max(...durations) : 0;
|
|
1976
|
-
const slowestReq = completed.find(r => r.duration === slowest);
|
|
1977
|
-
const slowestName = slowestReq ? (tryURL(slowestReq.url)?.pathname?.split('/').pop() || slowestReq.url?.split('/').pop() || '?') : '—';
|
|
1978
|
-
|
|
1979
|
-
const el = (id, text) => { const e = $(id); if (e) e.textContent = text; };
|
|
1980
|
-
el('netStatsTotal', `${total} requests`);
|
|
1981
|
-
el('netStatsAvg', `Avg: ${avg ? (avg > 999 ? `${(avg/1000).toFixed(1)}s` : `${avg}ms`) : '—'}`);
|
|
1982
|
-
el('netStatsSlowest', `Slowest: ${slowest ? (slowest > 999 ? `${(slowest/1000).toFixed(1)}s` : `${slowest}ms`) + ` (${slowestName})` : '—'}`);
|
|
1983
|
-
el('netStatsErrors', `Errors: ${errors}`);
|
|
1984
|
-
el('netStatsSlow', `Slow (>1s): ${slow}`);
|
|
1985
|
-
// Highlight if there are slow or errored requests
|
|
1986
|
-
if (slow > 0) $('netStatsSlow')?.classList.add('warn');
|
|
1987
|
-
else $('netStatsSlow')?.classList.remove('warn');
|
|
1988
|
-
if (errors > 0) $('netStatsErrors')?.classList.add('err');
|
|
1989
|
-
else $('netStatsErrors')?.classList.remove('err');
|
|
1990
|
-
}
|
|
1991
|
-
|
|
1992
|
-
function _isHttpError(r) {
|
|
1993
|
-
return r.phase === 'error' || (r.status && r.status >= 400);
|
|
1994
|
-
}
|
|
1995
|
-
|
|
1996
|
-
function buildNetRow(r, wfMin, wfRange) {
|
|
1997
|
-
const row = document.createElement('div');
|
|
1998
|
-
const rowSlow = !_isHttpError(r) && (r.duration || 0) >= 1000;
|
|
1999
|
-
const rowVerySlow = !_isHttpError(r) && (r.duration || 0) >= 3000;
|
|
2000
|
-
row.className = 'net-row' + (r.id === state.network.selectedId ? ' selected' : '') + (_isHttpError(r) ? ' error' : '') + (rowVerySlow ? ' very-slow' : rowSlow ? ' slow' : '');
|
|
2001
|
-
row.dataset.id = r.id;
|
|
2002
|
-
|
|
2003
|
-
const urlObj = tryURL(r.url);
|
|
2004
|
-
const pathname = urlObj ? urlObj.pathname : r.url || '';
|
|
2005
|
-
const filename = pathname.split('/').filter(Boolean).pop() || pathname;
|
|
2006
|
-
const host = urlObj ? urlObj.host : '';
|
|
2007
|
-
|
|
2008
|
-
// Name — show method + full path (expands with column)
|
|
2009
|
-
const nameCell = document.createElement('div');
|
|
2010
|
-
nameCell.className = 'net-cell net-cell-name';
|
|
2011
|
-
nameCell.dataset.col = 'name';
|
|
2012
|
-
nameCell.style.width = NET_COLS[0].width + 'px';
|
|
2013
|
-
const method = r.method || '?';
|
|
2014
|
-
const mClass = ['GET','POST','PUT','PATCH','DELETE'].includes(method) ? `m-${method}` : 'm-other';
|
|
2015
|
-
const fullPath = urlObj ? urlObj.pathname + urlObj.search : r.url || '';
|
|
2016
|
-
const isErr = _isHttpError(r);
|
|
2017
|
-
const pathCls = isErr ? ' net-path-error' : '';
|
|
2018
|
-
nameCell.innerHTML = `<span class="method-badge ${mClass}">${method}</span> <span class="net-path${pathCls}" title="${esc(r.url)}">${esc(fullPath)}</span><span class="net-host">${esc(host)}</span>`;
|
|
2019
|
-
row.appendChild(nameCell);
|
|
2020
|
-
|
|
2021
|
-
// Status
|
|
2022
|
-
const statusCell = document.createElement('div');
|
|
2023
|
-
statusCell.className = 'net-cell net-status';
|
|
2024
|
-
statusCell.dataset.col = 'status';
|
|
2025
|
-
statusCell.style.width = NET_COLS[1].width + 'px';
|
|
2026
|
-
let statusStr = '...', sCls = 's-pending';
|
|
2027
|
-
if (r.phase === 'error') { statusStr = 'ERR'; sCls = 's-err'; }
|
|
2028
|
-
else if (r.status) {
|
|
2029
|
-
statusStr = String(r.status);
|
|
2030
|
-
const group = Math.floor(r.status / 100);
|
|
2031
|
-
// 1xx info, 2xx success, 3xx redirect, 4xx client error, 5xx server error
|
|
2032
|
-
if (group >= 4) sCls = 's-err';
|
|
2033
|
-
else sCls = `s-${group}`;
|
|
2034
|
-
}
|
|
2035
|
-
statusCell.className += ` ${sCls}`;
|
|
2036
|
-
statusCell.textContent = statusStr;
|
|
2037
|
-
row.appendChild(statusCell);
|
|
2038
|
-
|
|
2039
|
-
// Type (content-type from response headers)
|
|
2040
|
-
const typeCell = document.createElement('div');
|
|
2041
|
-
typeCell.className = 'net-cell net-type';
|
|
2042
|
-
typeCell.dataset.col = 'type';
|
|
2043
|
-
typeCell.style.width = NET_COLS[2].width + 'px';
|
|
2044
|
-
const ct = r.responseHeaders?.['content-type'] || r.responseHeaders?.['Content-Type'] || '';
|
|
2045
|
-
typeCell.textContent = ct.split(';')[0].replace('application/', '').replace('text/', '') || '—';
|
|
2046
|
-
row.appendChild(typeCell);
|
|
2047
|
-
|
|
2048
|
-
// Initiator
|
|
2049
|
-
const initCell = document.createElement('div');
|
|
2050
|
-
initCell.className = 'net-cell net-initiator';
|
|
2051
|
-
initCell.dataset.col = 'initiator';
|
|
2052
|
-
initCell.style.width = NET_COLS[3].width + 'px';
|
|
2053
|
-
initCell.textContent = r.initiator || 'xhr';
|
|
2054
|
-
row.appendChild(initCell);
|
|
2055
|
-
|
|
2056
|
-
// Size
|
|
2057
|
-
const sizeCell = document.createElement('div');
|
|
2058
|
-
sizeCell.className = 'net-cell net-size';
|
|
2059
|
-
sizeCell.dataset.col = 'size';
|
|
2060
|
-
sizeCell.style.width = NET_COLS[4].width + 'px';
|
|
2061
|
-
const bodyStr = typeof r.responseBody === 'string' ? r.responseBody : (r.responseBody != null ? JSON.stringify(r.responseBody) : '');
|
|
2062
|
-
sizeCell.textContent = bodyStr.length > 0 ? formatSize(bodyStr.length) : '—';
|
|
2063
|
-
row.appendChild(sizeCell);
|
|
2064
|
-
|
|
2065
|
-
// Time
|
|
2066
|
-
const timeCell = document.createElement('div');
|
|
2067
|
-
const dur = r.duration || 0;
|
|
2068
|
-
const slowClass = dur >= 3000 ? ' very-slow' : dur >= 1000 ? ' slow' : '';
|
|
2069
|
-
timeCell.className = 'net-cell net-time' + slowClass;
|
|
2070
|
-
timeCell.dataset.col = 'time';
|
|
2071
|
-
timeCell.style.width = NET_COLS[5].width + 'px';
|
|
2072
|
-
timeCell.textContent = r.duration != null ? (r.duration > 999 ? `${(r.duration/1000).toFixed(1)}s` : `${r.duration}ms`) : '...';
|
|
2073
|
-
row.appendChild(timeCell);
|
|
2074
|
-
|
|
2075
|
-
// Waterfall
|
|
2076
|
-
const wfCell = document.createElement('div');
|
|
2077
|
-
wfCell.className = 'net-cell net-waterfall';
|
|
2078
|
-
wfCell.dataset.col = 'waterfall';
|
|
2079
|
-
wfCell.style.width = NET_COLS[6].width + 'px';
|
|
2080
|
-
if (r.ts) {
|
|
2081
|
-
const left = ((r.ts - wfMin) / wfRange) * 100;
|
|
2082
|
-
const width = Math.max(2, ((r.duration || 50) / wfRange) * 100);
|
|
2083
|
-
let barCls = 'pending';
|
|
2084
|
-
if (r.phase === 'error') barCls = 'err';
|
|
2085
|
-
else if (r.status && r.status >= 400) barCls = 'err';
|
|
2086
|
-
else if (r.status) barCls = `s${Math.floor(r.status/100)}`;
|
|
2087
|
-
wfCell.innerHTML = `<div class="wf-bar ${barCls}" style="left:${left}%;width:${width}%"></div>`;
|
|
2088
|
-
}
|
|
2089
|
-
row.appendChild(wfCell);
|
|
2090
|
-
|
|
2091
|
-
// Click to select and show detail
|
|
2092
|
-
row.addEventListener('click', () => selectNetRequest(r.id));
|
|
2093
|
-
|
|
2094
|
-
// Right-click for context menu (copy as cURL)
|
|
2095
|
-
row.addEventListener('contextmenu', (e) => {
|
|
2096
|
-
e.preventDefault();
|
|
2097
|
-
showNetContextMenu(e, r);
|
|
2098
|
-
});
|
|
2099
|
-
|
|
2100
|
-
return row;
|
|
2101
|
-
}
|
|
2102
|
-
|
|
2103
|
-
// ─── Select request → overlay detail pane over Status/Type/etc columns ───────
|
|
2104
|
-
function selectNetRequest(id) {
|
|
2105
|
-
state.network.selectedId = id;
|
|
2106
|
-
const r = state.network.requests[id];
|
|
2107
|
-
if (!r) return;
|
|
2108
|
-
|
|
2109
|
-
// Highlight selected row
|
|
2110
|
-
document.querySelectorAll('#netRows .net-row').forEach(el =>
|
|
2111
|
-
el.classList.toggle('selected', el.dataset.id === id)
|
|
2112
|
-
);
|
|
2113
|
-
|
|
2114
|
-
// Position detail pane to overlay everything after the Name column
|
|
2115
|
-
const pane = $('netDetailPane');
|
|
2116
|
-
const nameColWidth = NET_COLS[0].width;
|
|
2117
|
-
pane.style.left = (nameColWidth + 1) + 'px'; // +1 for the border
|
|
2118
|
-
pane.classList.add('open');
|
|
2119
|
-
r._tab = r._tab || 'headers';
|
|
2120
|
-
renderNetDetailTabs(r);
|
|
2121
|
-
renderNetDetailContent(r);
|
|
2122
|
-
}
|
|
2123
|
-
|
|
2124
|
-
function closeNetDetail() {
|
|
2125
|
-
state.network.selectedId = null;
|
|
2126
|
-
const pane = $('netDetailPane');
|
|
2127
|
-
if (pane) pane.classList.remove('open');
|
|
2128
|
-
document.querySelectorAll('#netRows .net-row').forEach(el =>
|
|
2129
|
-
el.classList.remove('selected')
|
|
2130
|
-
);
|
|
2131
|
-
}
|
|
2132
|
-
|
|
2133
|
-
function _estimateSize(val) {
|
|
2134
|
-
if (val == null) return 0;
|
|
2135
|
-
if (typeof val === 'string') return val.length;
|
|
2136
|
-
try { return JSON.stringify(val).length; } catch { return 0; }
|
|
2137
|
-
}
|
|
2138
|
-
|
|
2139
|
-
function _formatBytes(bytes) {
|
|
2140
|
-
if (bytes < 1024) return `${bytes}B`;
|
|
2141
|
-
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
2142
|
-
return `${(bytes / 1048576).toFixed(1)}MB`;
|
|
2143
|
-
}
|
|
2144
|
-
|
|
2145
|
-
function renderNetDetailTabs(r) {
|
|
2146
|
-
const tabs = $('netDetailTabs');
|
|
2147
|
-
tabs.innerHTML = '';
|
|
2148
|
-
|
|
2149
|
-
const tabDefs = [
|
|
2150
|
-
{ label: 'Headers', key: 'headers' },
|
|
2151
|
-
{ label: 'Request', key: 'request', sizeFrom: 'requestBody' },
|
|
2152
|
-
{ label: 'Preview', key: 'preview', sizeFrom: 'responseBody' },
|
|
2153
|
-
{ label: 'Response', key: 'response', sizeFrom: 'responseBody' },
|
|
2154
|
-
];
|
|
2155
|
-
|
|
2156
|
-
tabDefs.forEach(({ label, key, sizeFrom }) => {
|
|
2157
|
-
const btn = document.createElement('button');
|
|
2158
|
-
btn.className = 'detail-tab' + (r._tab === key ? ' active' : '');
|
|
2159
|
-
let text = label;
|
|
2160
|
-
if (sizeFrom && r[sizeFrom]) {
|
|
2161
|
-
const size = _estimateSize(r[sizeFrom]);
|
|
2162
|
-
if (size > 0) text += ` (${_formatBytes(size)})`;
|
|
2163
|
-
}
|
|
2164
|
-
btn.textContent = text;
|
|
2165
|
-
btn.addEventListener('click', () => {
|
|
2166
|
-
r._tab = key;
|
|
2167
|
-
tabs.querySelectorAll('.detail-tab').forEach(b => b.classList.remove('active'));
|
|
2168
|
-
btn.classList.add('active');
|
|
2169
|
-
renderNetDetailContent(r);
|
|
2170
|
-
});
|
|
2171
|
-
tabs.appendChild(btn);
|
|
2172
|
-
});
|
|
2173
|
-
|
|
2174
|
-
// Show search box for Preview/Response tabs
|
|
2175
|
-
const searchWrap = $('detailSearchWrap');
|
|
2176
|
-
if (searchWrap) {
|
|
2177
|
-
searchWrap.style.display = (r._tab === 'preview' || r._tab === 'response' || r._tab === 'headers') ? 'flex' : 'none';
|
|
2178
|
-
}
|
|
2179
|
-
}
|
|
2180
|
-
|
|
2181
|
-
function renderNetDetailContent(r) {
|
|
2182
|
-
let body = $('netDetailContent');
|
|
2183
|
-
if (!body) return;
|
|
2184
|
-
// Clone-replace to remove all stale event listeners (prevents contextmenu leak)
|
|
2185
|
-
const fresh = body.cloneNode(false);
|
|
2186
|
-
body.parentNode.replaceChild(fresh, body);
|
|
2187
|
-
body = fresh;
|
|
2188
|
-
const tab = r._tab || 'headers';
|
|
2189
|
-
|
|
2190
|
-
if (tab === 'headers') {
|
|
2191
|
-
const rqH = r.requestHeaders || {};
|
|
2192
|
-
const rsH = r.responseHeaders || {};
|
|
2193
|
-
const renderH = (title, h) => {
|
|
2194
|
-
const keys = Object.keys(h);
|
|
2195
|
-
if (!keys.length) return `<div class="section-label">${title}</div><span style="color:var(--text-dim)">none</span>`;
|
|
2196
|
-
return `<div class="section-label">${title}</div><div class="kv-grid">${keys.map(k => {
|
|
2197
|
-
let val = h[k];
|
|
2198
|
-
if (val && typeof val === 'object') { try { val = JSON.stringify(val); } catch { val = String(val); } }
|
|
2199
|
-
return `<span class="kv-key">${esc(k)}</span><span class="kv-val">${esc(val)}</span>`;
|
|
2200
|
-
}).join('')}</div>`;
|
|
2201
|
-
};
|
|
2202
|
-
body.innerHTML = `<div class="section-label" style="margin-top:0">General</div>
|
|
2203
|
-
<div class="kv-grid">
|
|
2204
|
-
<span class="kv-key">Request URL</span><span class="kv-val">${esc(r.url)}</span>
|
|
2205
|
-
<span class="kv-key">Method</span><span class="kv-val">${esc(r.method)}</span>
|
|
2206
|
-
<span class="kv-key">Status</span><span class="kv-val ${r.phase === 'error' ? 's-err' : r.status ? (r.status >= 400 ? 's-err' : 's-' + Math.floor(r.status/100)) : 's-pending'}">${r.phase === 'error' ? (r.status || 'ERR') : (r.status || 'Pending')} ${r.statusText || (r.phase === 'error' ? r.error || 'Network Error' : '')}</span>
|
|
2207
|
-
</div>
|
|
2208
|
-
${renderH('Response Headers', rsH)}
|
|
2209
|
-
${renderH('Request Headers', rqH)}`;
|
|
2210
|
-
} else if (tab === 'request') {
|
|
2211
|
-
if (!r.requestBody) {
|
|
2212
|
-
body.innerHTML = '<span style="color:var(--text-dim)">No request body</span>';
|
|
2213
|
-
} else {
|
|
2214
|
-
body.innerHTML = '';
|
|
2215
|
-
let reqData = r.requestBody;
|
|
2216
|
-
if (typeof reqData === 'string') {
|
|
2217
|
-
try { reqData = JSON.parse(reqData); } catch {}
|
|
2218
|
-
}
|
|
2219
|
-
if (reqData && typeof reqData === 'object') {
|
|
2220
|
-
body.appendChild(createTreeNode(null, reqData, false));
|
|
2221
|
-
body.addEventListener('contextmenu', (e) => {
|
|
2222
|
-
e.preventDefault();
|
|
2223
|
-
showPreviewCopyMenu(e, reqData);
|
|
2224
|
-
});
|
|
2225
|
-
} else {
|
|
2226
|
-
body.innerHTML = renderJSON(r.requestBody);
|
|
2227
|
-
}
|
|
2228
|
-
}
|
|
2229
|
-
} else if (tab === 'preview') {
|
|
2230
|
-
const isErrStatus = _isHttpError(r);
|
|
2231
|
-
if (r.phase === 'error' && !r.responseBody) { body.innerHTML = `<span style="color:var(--red)">${esc(r.error || 'Request failed')}</span>`; return; }
|
|
2232
|
-
if (!r.responseBody && r.phase !== 'response') { body.innerHTML = '<span style="color:var(--text-dim)">Pending...</span>'; return; }
|
|
2233
|
-
// Render as collapsible JSON tree with right-click copy
|
|
2234
|
-
const val = r.responseBody;
|
|
2235
|
-
let treeData = val;
|
|
2236
|
-
if (typeof val === 'string') {
|
|
2237
|
-
try { treeData = JSON.parse(val); } catch {
|
|
2238
|
-
body.innerHTML = `<span style="color:${isErrStatus ? 'var(--red)' : 'inherit'}">${esc(val)}</span>`;
|
|
2239
|
-
return;
|
|
2240
|
-
}
|
|
2241
|
-
}
|
|
2242
|
-
if (treeData && typeof treeData === 'object') {
|
|
2243
|
-
body.innerHTML = '';
|
|
2244
|
-
// Show error status banner above the response body
|
|
2245
|
-
if (isErrStatus) {
|
|
2246
|
-
const errBanner = document.createElement('div');
|
|
2247
|
-
errBanner.style.cssText = 'color:var(--red);font-weight:600;padding:4px 0 8px;font-size:11px;border-bottom:1px solid rgba(255,94,114,.15);margin-bottom:8px';
|
|
2248
|
-
errBanner.textContent = `${r.status || 'ERR'} ${r.statusText || r.error || 'Error'}`;
|
|
2249
|
-
body.appendChild(errBanner);
|
|
2250
|
-
}
|
|
2251
|
-
body.appendChild(createTreeNode(null, treeData, false));
|
|
2252
|
-
// Right-click on preview to copy the whole object or clicked node value
|
|
2253
|
-
body.addEventListener('contextmenu', (e) => {
|
|
2254
|
-
e.preventDefault();
|
|
2255
|
-
showPreviewCopyMenu(e, treeData);
|
|
2256
|
-
});
|
|
2257
|
-
} else {
|
|
2258
|
-
body.innerHTML = isErrStatus
|
|
2259
|
-
? `<span style="color:var(--red)">${esc(String(r.responseBody))}</span>`
|
|
2260
|
-
: '<span style="color:var(--text-dim)">No preview available</span>';
|
|
2261
|
-
}
|
|
2262
|
-
} else if (tab === 'response') {
|
|
2263
|
-
const isErrStatus = _isHttpError(r);
|
|
2264
|
-
if (r.phase === 'error' && !r.responseBody) { body.innerHTML = `<span style="color:var(--red)">${esc(r.error || 'Request failed')}</span>`; return; }
|
|
2265
|
-
if (!r.responseBody && r.phase !== 'response') { body.innerHTML = '<span style="color:var(--text-dim)">Pending...</span>'; return; }
|
|
2266
|
-
if (isErrStatus) {
|
|
2267
|
-
const errBanner = document.createElement('div');
|
|
2268
|
-
errBanner.style.cssText = 'color:var(--red);font-weight:600;padding:4px 0 8px;font-size:11px;border-bottom:1px solid rgba(255,94,114,.15);margin-bottom:8px';
|
|
2269
|
-
errBanner.textContent = `${r.status || 'ERR'} ${r.statusText || r.error || 'Error'}`;
|
|
2270
|
-
body.innerHTML = '';
|
|
2271
|
-
body.appendChild(errBanner);
|
|
2272
|
-
const raw = document.createElement('div');
|
|
2273
|
-
raw.style.color = 'var(--red)';
|
|
2274
|
-
raw.innerHTML = renderJSON(r.responseBody);
|
|
2275
|
-
body.appendChild(raw);
|
|
2276
|
-
} else {
|
|
2277
|
-
body.innerHTML = renderJSON(r.responseBody);
|
|
2278
|
-
}
|
|
2279
|
-
}
|
|
2280
|
-
}
|
|
2281
|
-
|
|
2282
|
-
// ─── Network context menus ──────────────────────────────────────────────────
|
|
2283
|
-
function showNetContextMenu(e, r) {
|
|
2284
|
-
const items = [
|
|
2285
|
-
{ label: 'Copy as cURL', action: () => navigator.clipboard.writeText(buildCurlCommand(r)) },
|
|
2286
|
-
{ label: 'Copy URL', action: () => navigator.clipboard.writeText(r.url || '') },
|
|
2287
|
-
];
|
|
2288
|
-
if (r.responseBody) {
|
|
2289
|
-
items.push({ label: 'Copy Response', action: () => {
|
|
2290
|
-
const text = typeof r.responseBody === 'string' ? r.responseBody : JSON.stringify(r.responseBody, null, 2);
|
|
2291
|
-
navigator.clipboard.writeText(text);
|
|
2292
|
-
}});
|
|
2293
|
-
}
|
|
2294
|
-
// Hide URL option
|
|
2295
|
-
items.push({ label: '—', action: null }); // separator
|
|
2296
|
-
items.push({ label: 'Hide this URL', action: () => {
|
|
2297
|
-
addHiddenURL(r.url || '');
|
|
2298
|
-
renderNetwork();
|
|
2299
|
-
}});
|
|
2300
|
-
showContextMenu(e, items);
|
|
2301
|
-
}
|
|
2302
|
-
|
|
2303
|
-
function showPreviewCopyMenu(e, fullData) {
|
|
2304
|
-
const items = [
|
|
2305
|
-
{ label: 'Copy Object', action: () => navigator.clipboard.writeText(JSON.stringify(fullData, null, 2)) },
|
|
2306
|
-
];
|
|
2307
|
-
const sel = window.getSelection();
|
|
2308
|
-
if (sel && sel.toString().length > 0) {
|
|
2309
|
-
items.push({ label: 'Copy Selection', action: () => navigator.clipboard.writeText(sel.toString()) });
|
|
2310
|
-
}
|
|
2311
|
-
const keyEl = e.target.closest('.ov-key');
|
|
2312
|
-
const leafEl = e.target.closest('.ov-leaf');
|
|
2313
|
-
if (keyEl || leafEl) {
|
|
2314
|
-
items.push({ label: 'Copy Value', action: () => navigator.clipboard.writeText((leafEl || keyEl.parentElement).textContent) });
|
|
2315
|
-
}
|
|
2316
|
-
showContextMenu(e, items);
|
|
2317
|
-
}
|
|
2318
|
-
|
|
2319
|
-
function buildCurlCommand(r) {
|
|
2320
|
-
let cmd = `curl '${r.url}'`;
|
|
2321
|
-
if (r.method && r.method !== 'GET') cmd += ` -X ${r.method}`;
|
|
2322
|
-
const headers = r.requestHeaders || {};
|
|
2323
|
-
Object.entries(headers).forEach(([k, v]) => {
|
|
2324
|
-
cmd += ` \\\n -H '${k}: ${v}'`;
|
|
2325
|
-
});
|
|
2326
|
-
if (r.requestBody) {
|
|
2327
|
-
const body = typeof r.requestBody === 'string' ? r.requestBody : JSON.stringify(r.requestBody);
|
|
2328
|
-
cmd += ` \\\n --data-raw '${body.replace(/'/g, "'\\''")}'`;
|
|
2329
|
-
}
|
|
2330
|
-
return cmd;
|
|
2331
|
-
}
|
|
2332
|
-
|
|
2333
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
2334
|
-
// GA4 EVENT INSPECTOR
|
|
2335
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
2336
|
-
const ga4State = { events: [], selected: -1, searchFilter: '', sortDir: 'desc' };
|
|
2337
|
-
|
|
2338
|
-
function initGA4Panel() {
|
|
2339
|
-
const panel = $('panel-ga4');
|
|
2340
|
-
panel.innerHTML = `
|
|
2341
|
-
<div class="panel-toolbar">
|
|
2342
|
-
<span class="panel-label">GA4 Events</span>
|
|
2343
|
-
<span class="badge" id="ga4Badge">0</span>
|
|
2344
|
-
<input id="ga4Search" class="net-search-input" style="margin-left:12px" placeholder="Filter events..." />
|
|
2345
|
-
<div class="ml-auto" style="display:flex;align-items:center;gap:6px">
|
|
2346
|
-
<label class="toggle-label" for="ga4ColorToggle" style="font-size:10px;gap:4px">
|
|
2347
|
-
<span style="color:var(--text-dim)">Colors</span>
|
|
2348
|
-
<input type="checkbox" id="ga4ColorToggle" class="toggle-input" ${getGA4ColorsEnabled() ? 'checked' : ''} />
|
|
2349
|
-
<span class="toggle-slider"></span>
|
|
2350
|
-
</label>
|
|
2351
|
-
<button class="panel-clear-btn" id="ga4Clear" title="Clear GA4 events">Clear</button>
|
|
2352
|
-
</div>
|
|
2353
|
-
</div>
|
|
2354
|
-
<div class="ga4-layout">
|
|
2355
|
-
<div class="ga4-list-pane">
|
|
2356
|
-
<div class="ga4-list-header">
|
|
2357
|
-
<span class="ga4-hcell ga4-sort-btn" id="ga4SortBtn" style="width:90px;cursor:pointer" title="Click to toggle sort order">Time <span id="ga4SortIcon">\u25BC</span></span>
|
|
2358
|
-
<span class="ga4-hcell" style="flex:1">Event</span>
|
|
2359
|
-
</div>
|
|
2360
|
-
<div class="scroll-area" id="ga4List">
|
|
2361
|
-
<div class="empty-state" id="ga4Empty">
|
|
2362
|
-
<div class="icon" style="font-size:28px;opacity:.2">📊</div>
|
|
2363
|
-
<div class="label">No GA4 events yet</div>
|
|
2364
|
-
<div class="hint">Events from @react-native-firebase/analytics will appear here</div>
|
|
2365
|
-
</div>
|
|
2366
|
-
</div>
|
|
2367
|
-
</div>
|
|
2368
|
-
<div class="ga4-resize-handle" id="ga4ResizeHandle"></div>
|
|
2369
|
-
<div class="ga4-detail-pane" id="ga4DetailPane">
|
|
2370
|
-
<div class="ga4-detail-header">EVENT DETAIL</div>
|
|
2371
|
-
<div class="scroll-area ga4-detail-content" id="ga4Detail">
|
|
2372
|
-
<span style="color:var(--text-dim);padding:16px;display:block">Click an event to inspect</span>
|
|
2373
|
-
</div>
|
|
2374
|
-
</div>
|
|
2375
|
-
</div>
|
|
2376
|
-
<div class="ga4-summary" id="ga4Summary">
|
|
2377
|
-
<span class="ga4-summary-label">Total: 0</span>
|
|
2378
|
-
</div>`;
|
|
2379
|
-
|
|
2380
|
-
$('ga4Search').addEventListener('input', (e) => {
|
|
2381
|
-
ga4State.searchFilter = e.target.value.toLowerCase().trim();
|
|
2382
|
-
renderGA4List();
|
|
2383
|
-
renderGA4Summary(); // update active chip highlight
|
|
2384
|
-
});
|
|
2385
|
-
|
|
2386
|
-
$('ga4ColorToggle')?.addEventListener('change', (e) => {
|
|
2387
|
-
setGA4ColorsEnabled(e.target.checked);
|
|
2388
|
-
renderGA4List();
|
|
2389
|
-
renderGA4Summary();
|
|
2390
|
-
});
|
|
2391
|
-
|
|
2392
|
-
$('ga4Clear').addEventListener('click', () => {
|
|
2393
|
-
ga4State.events = [];
|
|
2394
|
-
ga4State.selected = -1;
|
|
2395
|
-
ga4State.searchFilter = '';
|
|
2396
|
-
const search = $('ga4Search');
|
|
2397
|
-
if (search) search.value = '';
|
|
2398
|
-
$('ga4Badge').textContent = '0';
|
|
2399
|
-
renderGA4List();
|
|
2400
|
-
renderGA4Summary();
|
|
2401
|
-
// Clear detail pane
|
|
2402
|
-
const detail = $('ga4Detail');
|
|
2403
|
-
if (detail) detail.innerHTML = '<div class="ga4-detail-empty" style="color:var(--text-dim);padding:20px;text-align:center;font-size:11px">Select an event to view details</div>';
|
|
2404
|
-
});
|
|
2405
|
-
|
|
2406
|
-
$('ga4SortBtn').addEventListener('click', () => {
|
|
2407
|
-
ga4State.sortDir = ga4State.sortDir === 'desc' ? 'asc' : 'desc';
|
|
2408
|
-
$('ga4SortIcon').textContent = ga4State.sortDir === 'desc' ? '\u25BC' : '\u25B2';
|
|
2409
|
-
renderGA4List();
|
|
2410
|
-
});
|
|
2411
|
-
|
|
2412
|
-
// Resizable divider between list and detail
|
|
2413
|
-
const resizeHandle = $('ga4ResizeHandle');
|
|
2414
|
-
const detailPane = $('ga4DetailPane');
|
|
2415
|
-
resizeHandle.addEventListener('mousedown', (e) => {
|
|
2416
|
-
e.preventDefault();
|
|
2417
|
-
const startX = e.clientX;
|
|
2418
|
-
const startWidth = detailPane.offsetWidth;
|
|
2419
|
-
document.body.style.cursor = 'col-resize';
|
|
2420
|
-
document.body.style.userSelect = 'none';
|
|
2421
|
-
function onMove(ev) {
|
|
2422
|
-
const delta = startX - ev.clientX;
|
|
2423
|
-
detailPane.style.width = Math.max(200, Math.min(window.innerWidth * 0.8, startWidth + delta)) + 'px';
|
|
2424
|
-
}
|
|
2425
|
-
function onUp() {
|
|
2426
|
-
document.body.style.cursor = '';
|
|
2427
|
-
document.body.style.userSelect = '';
|
|
2428
|
-
document.removeEventListener('mousemove', onMove);
|
|
2429
|
-
document.removeEventListener('mouseup', onUp);
|
|
2430
|
-
}
|
|
2431
|
-
document.addEventListener('mousemove', onMove);
|
|
2432
|
-
document.addEventListener('mouseup', onUp);
|
|
2433
|
-
});
|
|
2434
|
-
}
|
|
2435
|
-
|
|
2436
|
-
function handleGA4Event(event) {
|
|
2437
|
-
if (!isTabEnabled('ga4')) return;
|
|
2438
|
-
ga4State.events.push({
|
|
2439
|
-
name: event.name || '?',
|
|
2440
|
-
params: event.params || {},
|
|
2441
|
-
tag: event.tag || 'GA4',
|
|
2442
|
-
source: event.source || '',
|
|
2443
|
-
ts: event.ts || Date.now(),
|
|
2444
|
-
index: ga4State.events.length,
|
|
2445
|
-
});
|
|
2446
|
-
$('ga4Badge').textContent = ga4State.events.length;
|
|
2447
|
-
|
|
2448
|
-
// Append to list (batched via rAF)
|
|
2449
|
-
if (!ga4State._raf) {
|
|
2450
|
-
ga4State._raf = requestAnimationFrame(() => {
|
|
2451
|
-
ga4State._raf = null;
|
|
2452
|
-
renderGA4List();
|
|
2453
|
-
renderGA4Summary();
|
|
2454
|
-
});
|
|
2455
|
-
}
|
|
2456
|
-
}
|
|
2457
|
-
|
|
2458
|
-
// Assign consistent color to each GA4 event name
|
|
2459
|
-
const _ga4EventColors = {};
|
|
2460
|
-
const _ga4ColorPalette = [
|
|
2461
|
-
'#4facff', // blue
|
|
2462
|
-
'#3dd68c', // green
|
|
2463
|
-
'#ff813f', // orange
|
|
2464
|
-
'#c678dd', // purple
|
|
2465
|
-
'#e06c75', // coral
|
|
2466
|
-
'#56b6c2', // teal
|
|
2467
|
-
'#d19a66', // gold
|
|
2468
|
-
'#98c379', // lime
|
|
2469
|
-
'#e5c07b', // yellow
|
|
2470
|
-
'#ff5e72', // red
|
|
2471
|
-
'#61afef', // light blue
|
|
2472
|
-
'#be5046', // rust
|
|
2473
|
-
];
|
|
2474
|
-
let _ga4ColorIdx = 0;
|
|
2475
|
-
function _ga4EventColor(name) {
|
|
2476
|
-
if (!getGA4ColorsEnabled()) return ''; // empty = inherit default text color
|
|
2477
|
-
if (!_ga4EventColors[name]) {
|
|
2478
|
-
_ga4EventColors[name] = _ga4ColorPalette[_ga4ColorIdx % _ga4ColorPalette.length];
|
|
2479
|
-
_ga4ColorIdx++;
|
|
2480
|
-
}
|
|
2481
|
-
return _ga4EventColors[name];
|
|
2482
|
-
}
|
|
2483
|
-
function getGA4ColorsEnabled() {
|
|
2484
|
-
try { return localStorage.getItem('rn-debug-ga4-colors') === 'true'; } catch { return false; }
|
|
2485
|
-
}
|
|
2486
|
-
function setGA4ColorsEnabled(v) {
|
|
2487
|
-
try { localStorage.setItem('rn-debug-ga4-colors', v ? 'true' : 'false'); } catch {}
|
|
2488
|
-
}
|
|
2489
|
-
|
|
2490
|
-
function renderGA4List() {
|
|
2491
|
-
const list = $('ga4List');
|
|
2492
|
-
const empty = $('ga4Empty');
|
|
2493
|
-
if (!list) return;
|
|
2494
|
-
|
|
2495
|
-
const { searchFilter, sortDir } = ga4State;
|
|
2496
|
-
let visible = ga4State.events.filter(e =>
|
|
2497
|
-
!searchFilter || e.name.toLowerCase().includes(searchFilter)
|
|
2498
|
-
);
|
|
2499
|
-
|
|
2500
|
-
// Sort: newest first (desc) or oldest first (asc)
|
|
2501
|
-
if (sortDir === 'desc') {
|
|
2502
|
-
visible = [...visible].reverse();
|
|
2503
|
-
}
|
|
2504
|
-
|
|
2505
|
-
empty.style.display = visible.length ? 'none' : 'flex';
|
|
2506
|
-
list.querySelectorAll('.ga4-row').forEach(e => e.remove());
|
|
2507
|
-
|
|
2508
|
-
// Cap at 500 rows
|
|
2509
|
-
const MAX = 500;
|
|
2510
|
-
const toRender = visible.length > MAX ? visible.slice(0, MAX) : visible;
|
|
2511
|
-
|
|
2512
|
-
const frag = document.createDocumentFragment();
|
|
2513
|
-
toRender.forEach(e => {
|
|
2514
|
-
const row = document.createElement('div');
|
|
2515
|
-
row.className = 'ga4-row' + (e.index === ga4State.selected ? ' selected' : '');
|
|
2516
|
-
|
|
2517
|
-
const time = new Date(e.ts).toLocaleTimeString('en', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
2518
|
-
|
|
2519
|
-
const evtColor = _ga4EventColor(e.name);
|
|
2520
|
-
const colorStyle = evtColor ? `color:${evtColor}` : '';
|
|
2521
|
-
row.innerHTML = `
|
|
2522
|
-
<span class="ga4-cell ga4-time">${time}</span>
|
|
2523
|
-
<span class="ga4-cell ga4-name" style="${colorStyle}">${esc(e.name)}</span>`;
|
|
2524
|
-
|
|
2525
|
-
row.addEventListener('click', () => {
|
|
2526
|
-
ga4State.selected = e.index;
|
|
2527
|
-
list.querySelectorAll('.ga4-row').forEach(r => r.classList.remove('selected'));
|
|
2528
|
-
row.classList.add('selected');
|
|
2529
|
-
renderGA4Detail(e);
|
|
2530
|
-
});
|
|
2531
|
-
|
|
2532
|
-
// Right-click to copy
|
|
2533
|
-
row.addEventListener('contextmenu', (ev) => {
|
|
2534
|
-
ev.preventDefault();
|
|
2535
|
-
showContextMenu(ev, [
|
|
2536
|
-
{ label: 'Copy Event Name', action: () => navigator.clipboard.writeText(e.name) },
|
|
2537
|
-
{ label: 'Copy as JSON', action: () => navigator.clipboard.writeText(JSON.stringify({ event: e.name, params: e.params }, null, 2)) },
|
|
2538
|
-
]);
|
|
2539
|
-
});
|
|
2540
|
-
|
|
2541
|
-
frag.appendChild(row);
|
|
2542
|
-
});
|
|
2543
|
-
list.appendChild(frag);
|
|
2544
|
-
}
|
|
2545
|
-
|
|
2546
|
-
function renderGA4Detail(e) {
|
|
2547
|
-
let detail = $('ga4Detail');
|
|
2548
|
-
if (!detail) return;
|
|
2549
|
-
|
|
2550
|
-
const time = new Date(e.ts).toLocaleTimeString('en', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
2551
|
-
|
|
2552
|
-
// Clone-replace to remove stale event listeners
|
|
2553
|
-
const fresh = detail.cloneNode(false);
|
|
2554
|
-
detail.parentNode.replaceChild(fresh, detail);
|
|
2555
|
-
detail = fresh;
|
|
2556
|
-
|
|
2557
|
-
// Header info
|
|
2558
|
-
const header = document.createElement('div');
|
|
2559
|
-
header.className = 'ga4-detail-info';
|
|
2560
|
-
header.innerHTML = `
|
|
2561
|
-
<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>
|
|
2562
|
-
<div class="ga4-detail-row"><span class="ga4-detail-key">Timestamp</span><span class="ga4-detail-val">${time}</span></div>
|
|
2563
|
-
`;
|
|
2564
|
-
detail.appendChild(header);
|
|
2565
|
-
|
|
2566
|
-
// Separator
|
|
2567
|
-
const sep = document.createElement('div');
|
|
2568
|
-
sep.className = 'ga4-detail-sep';
|
|
2569
|
-
detail.appendChild(sep);
|
|
2570
|
-
|
|
2571
|
-
// Parameters as key-value list with collapsible objects
|
|
2572
|
-
if (e.params && typeof e.params === 'object') {
|
|
2573
|
-
const keys = Object.keys(e.params).sort();
|
|
2574
|
-
keys.forEach(key => {
|
|
2575
|
-
const val = e.params[key];
|
|
2576
|
-
const row = document.createElement('div');
|
|
2577
|
-
row.className = 'ga4-param-row';
|
|
2578
|
-
|
|
2579
|
-
const keyEl = document.createElement('span');
|
|
2580
|
-
keyEl.className = 'ga4-param-key';
|
|
2581
|
-
keyEl.textContent = key;
|
|
2582
|
-
row.appendChild(keyEl);
|
|
2583
|
-
|
|
2584
|
-
if (val && typeof val === 'object') {
|
|
2585
|
-
// Collapsible object tree
|
|
2586
|
-
const treeWrap = document.createElement('span');
|
|
2587
|
-
treeWrap.className = 'ga4-param-val';
|
|
2588
|
-
treeWrap.appendChild(createTreeNode(null, val, true));
|
|
2589
|
-
row.appendChild(treeWrap);
|
|
2590
|
-
} else {
|
|
2591
|
-
const valEl = document.createElement('span');
|
|
2592
|
-
valEl.className = 'ga4-param-val';
|
|
2593
|
-
valEl.textContent = val === null ? 'null' : val === undefined ? 'undefined' : JSON.stringify(val);
|
|
2594
|
-
if (typeof val === 'string') valEl.style.color = 'var(--green)';
|
|
2595
|
-
else if (typeof val === 'number') valEl.style.color = 'var(--orange)';
|
|
2596
|
-
else if (typeof val === 'boolean') valEl.style.color = 'var(--accent2)';
|
|
2597
|
-
row.appendChild(valEl);
|
|
2598
|
-
}
|
|
2599
|
-
|
|
2600
|
-
detail.appendChild(row);
|
|
2601
|
-
});
|
|
2602
|
-
}
|
|
2603
|
-
|
|
2604
|
-
// Right-click on detail
|
|
2605
|
-
detail.addEventListener('contextmenu', (ev) => {
|
|
2606
|
-
ev.preventDefault();
|
|
2607
|
-
showContextMenu(ev, [
|
|
2608
|
-
{ label: 'Copy All Parameters', action: () => navigator.clipboard.writeText(JSON.stringify(e.params, null, 2)) },
|
|
2609
|
-
{ label: 'Copy Event JSON', action: () => navigator.clipboard.writeText(JSON.stringify({ event: e.name, params: e.params, timestamp: e.ts }, null, 2)) },
|
|
2610
|
-
]);
|
|
2611
|
-
});
|
|
2612
|
-
}
|
|
2613
|
-
|
|
2614
|
-
function renderGA4Summary() {
|
|
2615
|
-
const summary = $('ga4Summary');
|
|
2616
|
-
if (!summary) return;
|
|
2617
|
-
|
|
2618
|
-
const counts = {};
|
|
2619
|
-
ga4State.events.forEach(e => {
|
|
2620
|
-
counts[e.name] = (counts[e.name] || 0) + 1;
|
|
2621
|
-
});
|
|
2622
|
-
|
|
2623
|
-
const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]);
|
|
2624
|
-
|
|
2625
|
-
summary.innerHTML = '';
|
|
2626
|
-
|
|
2627
|
-
const totalLabel = document.createElement('span');
|
|
2628
|
-
totalLabel.className = 'ga4-summary-label';
|
|
2629
|
-
totalLabel.textContent = `Total: ${ga4State.events.length}`;
|
|
2630
|
-
summary.appendChild(totalLabel);
|
|
2631
|
-
|
|
2632
|
-
sorted.forEach(([name, count]) => {
|
|
2633
|
-
const chip = document.createElement('span');
|
|
2634
|
-
const isActive = ga4State.searchFilter === name.toLowerCase();
|
|
2635
|
-
const chipColor = _ga4EventColor(name);
|
|
2636
|
-
chip.className = 'ga4-summary-chip' + (isActive ? ' active' : '');
|
|
2637
|
-
if (chipColor) {
|
|
2638
|
-
chip.style.borderColor = chipColor;
|
|
2639
|
-
if (isActive) chip.style.background = chipColor + '22';
|
|
2640
|
-
chip.innerHTML = `<b style="color:${chipColor}">${esc(name)}</b><span class="chip-count">${count}</span>`;
|
|
2641
|
-
} else {
|
|
2642
|
-
chip.innerHTML = `<b>${esc(name)}</b><span class="chip-count">${count}</span>`;
|
|
2643
|
-
}
|
|
2644
|
-
chip.addEventListener('click', () => {
|
|
2645
|
-
const search = $('ga4Search');
|
|
2646
|
-
if (isActive) {
|
|
2647
|
-
// Clear filter
|
|
2648
|
-
ga4State.searchFilter = '';
|
|
2649
|
-
if (search) search.value = '';
|
|
2650
|
-
} else {
|
|
2651
|
-
// Set filter to this event name
|
|
2652
|
-
ga4State.searchFilter = name.toLowerCase();
|
|
2653
|
-
if (search) search.value = name;
|
|
2654
|
-
}
|
|
2655
|
-
renderGA4List();
|
|
2656
|
-
renderGA4Summary();
|
|
2657
|
-
});
|
|
2658
|
-
summary.appendChild(chip);
|
|
2659
|
-
});
|
|
2660
|
-
}
|
|
2661
|
-
|
|
2662
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
2663
|
-
// REDUX PANEL
|
|
2664
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
2665
|
-
function initReduxPanel() {
|
|
2666
|
-
const panel = $('panel-redux');
|
|
2667
|
-
panel.innerHTML = `
|
|
2668
|
-
<div class="panel-toolbar">
|
|
2669
|
-
<span class="panel-label">Redux</span>
|
|
2670
|
-
<span class="badge" id="rBadge">0</span>
|
|
2671
|
-
<input id="reduxSearch" class="net-search-input" style="margin-left:12px" placeholder="Filter actions..." />
|
|
2672
|
-
<div class="ml-auto" style="display:flex;align-items:center;gap:8px">
|
|
2673
|
-
<button class="panel-clear-btn" id="reduxClear" title="Clear redux">Clear</button>
|
|
2674
|
-
<button class="panel-clear-btn" id="reduxSort" title="Toggle sort order">Time ▲</button>
|
|
2675
|
-
<div class="time-travel-bar" style="border:none;padding:0;margin:0">
|
|
2676
|
-
<button class="tt-btn" onclick="reduxJumpTo(state.redux.selected-1)">◀</button>
|
|
2677
|
-
<span class="tt-label" id="ttLabel">—/—</span>
|
|
2678
|
-
<button class="tt-btn" onclick="reduxJumpTo(state.redux.selected+1)">▶</button>
|
|
2679
|
-
</div>
|
|
2680
|
-
</div>
|
|
2681
|
-
</div>
|
|
2682
|
-
<div class="scroll-area" id="reduxContent">
|
|
2683
|
-
<div class="empty-state" id="reduxEmpty">
|
|
2684
|
-
<div class="icon">🔲</div>
|
|
2685
|
-
<div class="label">No actions dispatched</div>
|
|
2686
|
-
<div class="hint">Connect Redux store to RNDebugSDK</div>
|
|
2687
|
-
</div>
|
|
2688
|
-
</div>`;
|
|
2689
|
-
|
|
2690
|
-
$('reduxSearch').addEventListener('input', (e) => {
|
|
2691
|
-
state.redux.searchFilter = e.target.value.toLowerCase().trim();
|
|
2692
|
-
renderRedux();
|
|
2693
|
-
});
|
|
2694
|
-
|
|
2695
|
-
$('reduxClear').addEventListener('click', () => {
|
|
2696
|
-
state.redux.actions = [];
|
|
2697
|
-
state.redux.states = [];
|
|
2698
|
-
state.redux.selected = -1;
|
|
2699
|
-
$('rBadge').textContent = '0';
|
|
2700
|
-
renderRedux();
|
|
2701
|
-
});
|
|
2702
|
-
|
|
2703
|
-
$('reduxSort').addEventListener('click', () => {
|
|
2704
|
-
state.redux.sortDir = state.redux.sortDir === 'desc' ? 'asc' : 'desc';
|
|
2705
|
-
$('reduxSort').textContent = state.redux.sortDir === 'desc' ? 'Time \u25BC' : 'Time \u25B2';
|
|
2706
|
-
renderRedux();
|
|
2707
|
-
});
|
|
2708
|
-
}
|
|
2709
|
-
|
|
2710
|
-
window.reduxJumpTo = idx => {
|
|
2711
|
-
const { actions } = state.redux;
|
|
2712
|
-
if (!actions.length) return;
|
|
2713
|
-
idx = Math.max(0, Math.min(actions.length - 1, idx));
|
|
2714
|
-
state.redux.selected = idx;
|
|
2715
|
-
renderRedux();
|
|
2716
|
-
};
|
|
2717
|
-
|
|
2718
|
-
// Fast deep equality check for Redux state comparison
|
|
2719
|
-
function _deepEqual(a, b) {
|
|
2720
|
-
if (a === b) return true;
|
|
2721
|
-
if (a == null || b == null) return false;
|
|
2722
|
-
if (typeof a !== typeof b) return false;
|
|
2723
|
-
if (typeof a !== 'object') return false;
|
|
2724
|
-
try {
|
|
2725
|
-
return JSON.stringify(a) === JSON.stringify(b);
|
|
2726
|
-
} catch { return false; }
|
|
2727
|
-
}
|
|
2728
|
-
|
|
2729
|
-
// Find leaf-level changes between two values (for Redux store diff)
|
|
2730
|
-
function _findLeafChanges(oldVal, newVal, basePath, maxDepth) {
|
|
2731
|
-
const changes = [];
|
|
2732
|
-
if (maxDepth === undefined) maxDepth = 5;
|
|
2733
|
-
|
|
2734
|
-
function walk(a, b, path, depth) {
|
|
2735
|
-
if (depth > maxDepth) {
|
|
2736
|
-
if (!_deepEqual(a, b)) changes.push({ path, oldVal: a, newVal: b });
|
|
2737
|
-
return;
|
|
2738
|
-
}
|
|
2739
|
-
if (a === b) return;
|
|
2740
|
-
if (a == null || b == null || typeof a !== 'object' || typeof b !== 'object' || Array.isArray(a) !== Array.isArray(b)) {
|
|
2741
|
-
changes.push({ path, oldVal: a, newVal: b });
|
|
2742
|
-
return;
|
|
2743
|
-
}
|
|
2744
|
-
const allKeys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
2745
|
-
allKeys.forEach(k => {
|
|
2746
|
-
if (!_deepEqual(a[k], b[k])) {
|
|
2747
|
-
const childPath = path ? `${path}.${k}` : k;
|
|
2748
|
-
if (a[k] != null && b[k] != null && typeof a[k] === 'object' && typeof b[k] === 'object' && !Array.isArray(a[k])) {
|
|
2749
|
-
walk(a[k], b[k], childPath, depth + 1);
|
|
2750
|
-
} else {
|
|
2751
|
-
changes.push({ path: childPath, oldVal: a[k], newVal: b[k] });
|
|
2752
|
-
}
|
|
2753
|
-
}
|
|
2754
|
-
});
|
|
2755
|
-
}
|
|
2756
|
-
|
|
2757
|
-
walk(oldVal, newVal, '', 0);
|
|
2758
|
-
return changes;
|
|
2759
|
-
}
|
|
2760
|
-
|
|
2761
|
-
// Create a tree node with changed paths highlighted in a different color
|
|
2762
|
-
function _createHighlightedTree(key, val, changedPaths, currentPath, isOld) {
|
|
2763
|
-
const isArray = Array.isArray(val);
|
|
2764
|
-
const isObj = val !== null && typeof val === 'object';
|
|
2765
|
-
const myPath = key !== null ? (currentPath ? `${currentPath}.${key}` : String(key)) : currentPath;
|
|
2766
|
-
const isChanged = changedPaths.has(myPath);
|
|
2767
|
-
|
|
2768
|
-
if (!isObj) {
|
|
2769
|
-
// Leaf value
|
|
2770
|
-
const row = document.createElement('div');
|
|
2771
|
-
row.className = 'ov-leaf' + (isChanged ? ' rdx-highlight' : '');
|
|
2772
|
-
if (isChanged) row.style.cssText = isOld
|
|
2773
|
-
? 'background:rgba(255,94,114,.12);border-radius:3px;padding:1px 4px;'
|
|
2774
|
-
: 'background:rgba(61,214,140,.12);border-radius:3px;padding:1px 4px;';
|
|
2775
|
-
if (key !== null) {
|
|
2776
|
-
const k = document.createElement('span');
|
|
2777
|
-
k.className = 'ov-key';
|
|
2778
|
-
k.style.color = isChanged ? (isOld ? 'var(--red)' : 'var(--green)') : '';
|
|
2779
|
-
k.textContent = `${key}: `;
|
|
2780
|
-
row.appendChild(k);
|
|
2781
|
-
}
|
|
2782
|
-
const v = document.createElement('span');
|
|
2783
|
-
v.className = 'ov-prim';
|
|
2784
|
-
if (isChanged) v.style.fontWeight = '700';
|
|
2785
|
-
if (val === null) { v.textContent = 'null'; v.style.color = isChanged ? (isOld ? 'var(--red)' : 'var(--green)') : 'var(--text-dim)'; }
|
|
2786
|
-
else if (typeof val === 'string') { v.textContent = `"${val}"`; v.style.color = isChanged ? (isOld ? 'var(--red)' : 'var(--green)') : 'var(--green)'; }
|
|
2787
|
-
else if (typeof val === 'number') { v.textContent = String(val); v.style.color = isChanged ? (isOld ? 'var(--red)' : 'var(--green)') : 'var(--accent2)'; }
|
|
2788
|
-
else if (typeof val === 'boolean') { v.textContent = String(val); v.style.color = isChanged ? (isOld ? 'var(--red)' : 'var(--green)') : 'var(--accent2)'; }
|
|
2789
|
-
else { v.textContent = _safeStr(val); }
|
|
2790
|
-
row.appendChild(v);
|
|
2791
|
-
return row;
|
|
2792
|
-
}
|
|
2793
|
-
|
|
2794
|
-
// Object/Array — check if any descendants changed
|
|
2795
|
-
const hasChangedDescendant = [...changedPaths].some(p => p === myPath || p.startsWith(myPath ? myPath + '.' : ''));
|
|
2796
|
-
const container = document.createElement('div');
|
|
2797
|
-
container.className = 'ov-node';
|
|
2798
|
-
|
|
2799
|
-
const header = document.createElement('div');
|
|
2800
|
-
header.className = 'ov-header';
|
|
2801
|
-
|
|
2802
|
-
const arrow = document.createElement('span');
|
|
2803
|
-
arrow.className = 'ov-arrow';
|
|
2804
|
-
arrow.textContent = '\u25B6';
|
|
2805
|
-
header.appendChild(arrow);
|
|
2806
|
-
|
|
2807
|
-
if (key !== null) {
|
|
2808
|
-
const k = document.createElement('span');
|
|
2809
|
-
k.className = 'ov-key';
|
|
2810
|
-
if (hasChangedDescendant) k.style.color = isOld ? 'var(--red)' : 'var(--green)';
|
|
2811
|
-
k.textContent = `${key}: `;
|
|
2812
|
-
header.appendChild(k);
|
|
2813
|
-
}
|
|
2814
|
-
|
|
2815
|
-
const preview = document.createElement('span');
|
|
2816
|
-
preview.className = 'ov-preview';
|
|
2817
|
-
preview.textContent = isArray ? `Array(${val.length})` : `{${Object.keys(val).length} keys}`;
|
|
2818
|
-
header.appendChild(preview);
|
|
2819
|
-
|
|
2820
|
-
container.appendChild(header);
|
|
2821
|
-
|
|
2822
|
-
const children = document.createElement('div');
|
|
2823
|
-
children.className = 'ov-children';
|
|
2824
|
-
// Always start collapsed — user expands what they need
|
|
2825
|
-
children.style.display = 'none';
|
|
2826
|
-
|
|
2827
|
-
let populated = false;
|
|
2828
|
-
function populate() {
|
|
2829
|
-
if (populated) return;
|
|
2830
|
-
populated = true;
|
|
2831
|
-
const entries = isArray ? val.map((v, i) => [i, v]) : Object.entries(val);
|
|
2832
|
-
entries.forEach(([k, v]) => {
|
|
2833
|
-
children.appendChild(_createHighlightedTree(k, v, changedPaths, myPath, isOld));
|
|
2834
|
-
});
|
|
2835
|
-
}
|
|
2836
|
-
|
|
2837
|
-
header.addEventListener('click', (e) => {
|
|
2838
|
-
e.stopPropagation();
|
|
2839
|
-
const open = children.style.display !== 'none';
|
|
2840
|
-
children.style.display = open ? 'none' : 'block';
|
|
2841
|
-
arrow.textContent = open ? '\u25B6' : '\u25BC';
|
|
2842
|
-
if (!open) populate();
|
|
2843
|
-
});
|
|
2844
|
-
|
|
2845
|
-
container.appendChild(children);
|
|
2846
|
-
return container;
|
|
2847
|
-
}
|
|
2848
|
-
|
|
2849
|
-
function handleReduxEvent(event) {
|
|
2850
|
-
if (event.type !== 'redux') return;
|
|
2851
|
-
// Skip processing if Redux tab is disabled (saves memory)
|
|
2852
|
-
if (!isTabEnabled('redux')) return;
|
|
2853
|
-
const { action, nextState } = event;
|
|
2854
|
-
const idx = state.redux.actions.length;
|
|
2855
|
-
|
|
2856
|
-
const prevState = state.redux.states.length > 0 ? state.redux.states[state.redux.states.length - 1] : null;
|
|
2857
|
-
const changedKeys = [];
|
|
2858
|
-
if (prevState && nextState && typeof prevState === 'object' && typeof nextState === 'object') {
|
|
2859
|
-
const allKeys = new Set([...Object.keys(prevState), ...Object.keys(nextState)]);
|
|
2860
|
-
allKeys.forEach(k => { if (!_deepEqual(prevState[k], nextState[k])) changedKeys.push(k); });
|
|
2861
|
-
}
|
|
2862
|
-
|
|
2863
|
-
const actionEntry = { type: action?.type || '?', payload: action, ts: event.ts, index: idx, changedKeys };
|
|
2864
|
-
state.redux.actions.push(actionEntry);
|
|
2865
|
-
state.redux.states.push(nextState);
|
|
2866
|
-
// Cap Redux history to prevent memory leak (full state stored per action)
|
|
2867
|
-
const MAX_REDUX_HISTORY = 500;
|
|
2868
|
-
if (state.redux.actions.length > MAX_REDUX_HISTORY) {
|
|
2869
|
-
const trim = state.redux.actions.length - MAX_REDUX_HISTORY;
|
|
2870
|
-
state.redux.actions.splice(0, trim);
|
|
2871
|
-
state.redux.states.splice(0, trim);
|
|
2872
|
-
// Re-index remaining actions
|
|
2873
|
-
state.redux.actions.forEach((a, i) => a.index = i);
|
|
2874
|
-
if (state.redux.selected >= 0) state.redux.selected = Math.max(0, state.redux.selected - trim);
|
|
2875
|
-
}
|
|
2876
|
-
// Don't auto-select — keep all collapsed until user clicks
|
|
2877
|
-
$('rBadge').textContent = state.redux.actions.length;
|
|
2878
|
-
renderRedux();
|
|
2879
|
-
|
|
2880
|
-
// Always add Redux actions to console logs — visibility controlled by showRedux filter
|
|
2881
|
-
{
|
|
2882
|
-
const msg = `[Redux] ${actionEntry.type}` + (changedKeys.length ? ` (changed: ${changedKeys.join(', ')})` : '');
|
|
2883
|
-
addConsoleLog({
|
|
2884
|
-
level: 'redux',
|
|
2885
|
-
message: msg,
|
|
2886
|
-
args: [{ t: 'string', v: `[Redux] ${actionEntry.type}` }, { t: 'object', v: action }],
|
|
2887
|
-
ts: event.ts,
|
|
2888
|
-
_isRedux: true,
|
|
2889
|
-
});
|
|
2890
|
-
}
|
|
2891
|
-
}
|
|
2892
|
-
|
|
2893
|
-
// Assign a consistent color to each Redux action category (e.g. ANALYTICS, CART, USER)
|
|
2894
|
-
const _reduxCatColors = {};
|
|
2895
|
-
const _reduxColorPalette = [
|
|
2896
|
-
'var(--accent)', // blue
|
|
2897
|
-
'var(--green)', // green
|
|
2898
|
-
'var(--orange)', // orange
|
|
2899
|
-
'var(--accent2)', // purple
|
|
2900
|
-
'#e06c75', // coral
|
|
2901
|
-
'#56b6c2', // teal
|
|
2902
|
-
'#c678dd', // magenta
|
|
2903
|
-
'#d19a66', // gold
|
|
2904
|
-
'#98c379', // lime
|
|
2905
|
-
'#e5c07b', // yellow
|
|
2906
|
-
];
|
|
2907
|
-
let _reduxColorIdx = 0;
|
|
2908
|
-
function _reduxCategoryColor(category) {
|
|
2909
|
-
if (!_reduxCatColors[category]) {
|
|
2910
|
-
_reduxCatColors[category] = _reduxColorPalette[_reduxColorIdx % _reduxColorPalette.length];
|
|
2911
|
-
_reduxColorIdx++;
|
|
2912
|
-
}
|
|
2913
|
-
return _reduxCatColors[category];
|
|
2914
|
-
}
|
|
2915
|
-
|
|
2916
|
-
function renderRedux() {
|
|
2917
|
-
const content = $('reduxContent');
|
|
2918
|
-
const empty = $('reduxEmpty');
|
|
2919
|
-
if (!content) return;
|
|
2920
|
-
|
|
2921
|
-
const { actions, states, selected, searchFilter, sortDir } = state.redux;
|
|
2922
|
-
let visible = searchFilter ? actions.filter(a => a.type.toLowerCase().includes(searchFilter)) : [...actions];
|
|
2923
|
-
if (sortDir === 'desc') visible = [...visible].reverse();
|
|
2924
|
-
|
|
2925
|
-
empty.style.display = visible.length ? 'none' : 'flex';
|
|
2926
|
-
content.querySelectorAll('.rdx-entry').forEach(e => e.remove());
|
|
2927
|
-
if (!visible.length) return;
|
|
2928
|
-
|
|
2929
|
-
const ttLabel = $('ttLabel');
|
|
2930
|
-
if (ttLabel) ttLabel.textContent = selected >= 0 ? `${selected + 1}/${actions.length}` : `—/${actions.length}`;
|
|
2931
|
-
|
|
2932
|
-
const frag = document.createDocumentFragment();
|
|
2933
|
-
visible.forEach(a => {
|
|
2934
|
-
const isSelected = a.index === selected;
|
|
2935
|
-
|
|
2936
|
-
const entry = document.createElement('div');
|
|
2937
|
-
entry.className = 'rdx-entry' + (isSelected ? ' selected' : '');
|
|
2938
|
-
|
|
2939
|
-
// Row header — always visible
|
|
2940
|
-
const header = document.createElement('div');
|
|
2941
|
-
header.className = 'rdx-entry-header';
|
|
2942
|
-
const changesBadge = a.changedKeys?.length ? `<span class="rdx-changes">${a.changedKeys.length} changed</span>` : '';
|
|
2943
|
-
// Color-code action type by category prefix (e.g. ANALYTICS/, CART/, USER/)
|
|
2944
|
-
const typeParts = a.type.split('/');
|
|
2945
|
-
let typeHtml;
|
|
2946
|
-
if (typeParts.length >= 2) {
|
|
2947
|
-
const catColor = _reduxCategoryColor(typeParts[0]);
|
|
2948
|
-
typeHtml = `<span class="rdx-type-cat" style="color:${catColor}">${esc(typeParts[0])}/</span><span class="rdx-type-name">${esc(typeParts.slice(1).join('/'))}</span>`;
|
|
2949
|
-
} else {
|
|
2950
|
-
typeHtml = `<span class="rdx-type">${esc(a.type)}</span>`;
|
|
2951
|
-
}
|
|
2952
|
-
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>`;
|
|
2953
|
-
// Toggle: click to expand, click again to collapse
|
|
2954
|
-
header.addEventListener('click', () => {
|
|
2955
|
-
state.redux.selected = isSelected ? -1 : a.index;
|
|
2956
|
-
renderRedux();
|
|
2957
|
-
});
|
|
2958
|
-
// Right-click to copy action type
|
|
2959
|
-
header.addEventListener('contextmenu', (e) => {
|
|
2960
|
-
e.preventDefault();
|
|
2961
|
-
e.stopPropagation();
|
|
2962
|
-
showContextMenu(e, [
|
|
2963
|
-
{ label: 'Copy Action Type', action: () => navigator.clipboard.writeText(a.type) },
|
|
2964
|
-
{ label: 'Copy Action Payload', action: () => navigator.clipboard.writeText(JSON.stringify(a.payload, null, 2)) },
|
|
2965
|
-
]);
|
|
2966
|
-
});
|
|
2967
|
-
// Allow text selection on the action type
|
|
2968
|
-
header.style.userSelect = 'text';
|
|
2969
|
-
entry.appendChild(header);
|
|
2970
|
-
|
|
2971
|
-
// Expanded detail — only for explicitly selected action
|
|
2972
|
-
if (isSelected) {
|
|
2973
|
-
const detail = document.createElement('div');
|
|
2974
|
-
detail.className = 'rdx-entry-detail';
|
|
2975
|
-
|
|
2976
|
-
// Close button
|
|
2977
|
-
const closeBtn = document.createElement('button');
|
|
2978
|
-
closeBtn.className = 'rdx-close-btn';
|
|
2979
|
-
closeBtn.textContent = '✕';
|
|
2980
|
-
closeBtn.title = 'Close';
|
|
2981
|
-
closeBtn.addEventListener('click', (e) => {
|
|
2982
|
-
e.stopPropagation();
|
|
2983
|
-
state.redux.selected = -1;
|
|
2984
|
-
renderRedux();
|
|
2985
|
-
});
|
|
2986
|
-
detail.appendChild(closeBtn);
|
|
2987
|
-
|
|
2988
|
-
// Changed keys badges
|
|
2989
|
-
if (a.changedKeys?.length > 0) {
|
|
2990
|
-
const keysEl = document.createElement('div');
|
|
2991
|
-
keysEl.className = 'redux-changed-keys';
|
|
2992
|
-
keysEl.innerHTML = `<span class="redux-changed-label">Changed:</span> ${a.changedKeys.map(k =>
|
|
2993
|
-
`<span class="redux-changed-key">${esc(k)}</span>`).join(' ')}`;
|
|
2994
|
-
detail.appendChild(keysEl);
|
|
2995
|
-
}
|
|
2996
|
-
|
|
2997
|
-
// Payload
|
|
2998
|
-
if (a.payload) {
|
|
2999
|
-
const pLabel = document.createElement('div');
|
|
3000
|
-
pLabel.className = 'redux-section-title';
|
|
3001
|
-
pLabel.textContent = 'Action Payload';
|
|
3002
|
-
detail.appendChild(pLabel);
|
|
3003
|
-
detail.appendChild(createTreeNode(null, a.payload, false));
|
|
3004
|
-
}
|
|
3005
|
-
|
|
3006
|
-
// Store changes — two-column layout: Previous | Current
|
|
3007
|
-
const prevS = a.index > 0 ? states[a.index - 1] : null;
|
|
3008
|
-
const currS = states[a.index];
|
|
3009
|
-
if (currS && typeof currS === 'object' && a.changedKeys?.length > 0) {
|
|
3010
|
-
a.changedKeys.forEach(key => {
|
|
3011
|
-
const keyWrap = document.createElement('div');
|
|
3012
|
-
keyWrap.className = 'rdx-store-diff';
|
|
3013
|
-
|
|
3014
|
-
const kLabel = document.createElement('div');
|
|
3015
|
-
kLabel.className = 'rdx-store-key-label';
|
|
3016
|
-
kLabel.textContent = key;
|
|
3017
|
-
keyWrap.appendChild(kLabel);
|
|
3018
|
-
|
|
3019
|
-
const oldVal = prevS ? prevS[key] : undefined;
|
|
3020
|
-
const newVal = currS[key];
|
|
3021
|
-
|
|
3022
|
-
// Find which sub-keys changed (for highlighting)
|
|
3023
|
-
const changedPaths = new Set();
|
|
3024
|
-
_findLeafChanges(oldVal, newVal, '').forEach(c => changedPaths.add(c.path));
|
|
3025
|
-
|
|
3026
|
-
// Two-column grid: Previous | Current
|
|
3027
|
-
const grid = document.createElement('div');
|
|
3028
|
-
grid.className = 'rdx-diff-grid';
|
|
3029
|
-
|
|
3030
|
-
// Previous column
|
|
3031
|
-
const prevCol = document.createElement('div');
|
|
3032
|
-
prevCol.className = 'rdx-diff-col prev';
|
|
3033
|
-
const prevLabel = document.createElement('div');
|
|
3034
|
-
prevLabel.className = 'rdx-state-label prev';
|
|
3035
|
-
prevLabel.textContent = '- Previous';
|
|
3036
|
-
prevCol.appendChild(prevLabel);
|
|
3037
|
-
if (oldVal !== undefined) {
|
|
3038
|
-
prevCol.appendChild(_createHighlightedTree(null, oldVal, changedPaths, '', true));
|
|
3039
|
-
} else {
|
|
3040
|
-
const na = document.createElement('span');
|
|
3041
|
-
na.style.cssText = 'color:var(--text-dim);font-size:10px;font-style:italic';
|
|
3042
|
-
na.textContent = 'undefined';
|
|
3043
|
-
prevCol.appendChild(na);
|
|
3044
|
-
}
|
|
3045
|
-
grid.appendChild(prevCol);
|
|
3046
|
-
|
|
3047
|
-
// Current column
|
|
3048
|
-
const currCol = document.createElement('div');
|
|
3049
|
-
currCol.className = 'rdx-diff-col curr';
|
|
3050
|
-
const currLabel = document.createElement('div');
|
|
3051
|
-
currLabel.className = 'rdx-state-label curr';
|
|
3052
|
-
currLabel.textContent = '+ Current';
|
|
3053
|
-
currCol.appendChild(currLabel);
|
|
3054
|
-
if (newVal !== undefined) {
|
|
3055
|
-
currCol.appendChild(_createHighlightedTree(null, newVal, changedPaths, '', false));
|
|
3056
|
-
} else {
|
|
3057
|
-
const na = document.createElement('span');
|
|
3058
|
-
na.style.cssText = 'color:var(--text-dim);font-size:10px;font-style:italic';
|
|
3059
|
-
na.textContent = 'undefined';
|
|
3060
|
-
currCol.appendChild(na);
|
|
3061
|
-
}
|
|
3062
|
-
grid.appendChild(currCol);
|
|
3063
|
-
|
|
3064
|
-
// Right-click to copy on each column
|
|
3065
|
-
prevCol.addEventListener('contextmenu', (e) => {
|
|
3066
|
-
e.preventDefault(); e.stopPropagation();
|
|
3067
|
-
showContextMenu(e, [
|
|
3068
|
-
{ label: 'Copy Previous Value', action: () => navigator.clipboard.writeText(JSON.stringify(oldVal, null, 2)) },
|
|
3069
|
-
{ label: 'Copy Current Value', action: () => navigator.clipboard.writeText(JSON.stringify(newVal, null, 2)) },
|
|
3070
|
-
{ label: `Copy "${key}" key`, action: () => navigator.clipboard.writeText(key) },
|
|
3071
|
-
]);
|
|
3072
|
-
});
|
|
3073
|
-
currCol.addEventListener('contextmenu', (e) => {
|
|
3074
|
-
e.preventDefault(); e.stopPropagation();
|
|
3075
|
-
showContextMenu(e, [
|
|
3076
|
-
{ label: 'Copy Current Value', action: () => navigator.clipboard.writeText(JSON.stringify(newVal, null, 2)) },
|
|
3077
|
-
{ label: 'Copy Previous Value', action: () => navigator.clipboard.writeText(JSON.stringify(oldVal, null, 2)) },
|
|
3078
|
-
{ label: `Copy "${key}" key`, action: () => navigator.clipboard.writeText(key) },
|
|
3079
|
-
]);
|
|
3080
|
-
});
|
|
3081
|
-
|
|
3082
|
-
keyWrap.appendChild(grid);
|
|
3083
|
-
detail.appendChild(keyWrap);
|
|
3084
|
-
});
|
|
3085
|
-
}
|
|
3086
|
-
|
|
3087
|
-
entry.appendChild(detail);
|
|
3088
|
-
}
|
|
3089
|
-
|
|
3090
|
-
frag.appendChild(entry);
|
|
3091
|
-
});
|
|
3092
|
-
|
|
3093
|
-
content.appendChild(frag);
|
|
3094
|
-
// Scroll selected entry into view
|
|
3095
|
-
const selEl = content.querySelector('.rdx-entry.selected');
|
|
3096
|
-
if (selEl) {
|
|
3097
|
-
selEl.scrollIntoView({ block: 'nearest', behavior: 'auto' });
|
|
3098
|
-
}
|
|
3099
|
-
}
|
|
3100
|
-
|
|
3101
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
3102
|
-
// ASYNC STORAGE PANEL
|
|
3103
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
3104
|
-
function initStoragePanel() {
|
|
3105
|
-
const panel = $('panel-storage');
|
|
3106
|
-
panel.innerHTML = `
|
|
3107
|
-
<div class="panel-toolbar">
|
|
3108
|
-
<span class="panel-label">AsyncStorage</span>
|
|
3109
|
-
<span class="badge" id="sBadge">0</span>
|
|
3110
|
-
<div class="ml-auto">
|
|
3111
|
-
<input id="storageSearch" class="net-search-input" placeholder="Filter keys..." />
|
|
3112
|
-
</div>
|
|
3113
|
-
</div>
|
|
3114
|
-
<div class="storage-layout" id="storageLayout">
|
|
3115
|
-
<div class="storage-keys" id="storageKeysPane">
|
|
3116
|
-
<div class="panel-toolbar" style="height:32px">
|
|
3117
|
-
<span style="font-size:10px;color:var(--text-dim);text-transform:uppercase;letter-spacing:1px">Keys</span>
|
|
3118
|
-
</div>
|
|
3119
|
-
<div class="scroll-area storage-keys-list" id="storageKeyList">
|
|
3120
|
-
<div class="empty-state" id="storageEmpty">
|
|
3121
|
-
<div class="icon">💾</div>
|
|
3122
|
-
<div class="label">No storage data</div>
|
|
3123
|
-
<div class="hint">AsyncStorage data will appear here</div>
|
|
3124
|
-
</div>
|
|
3125
|
-
</div>
|
|
3126
|
-
</div>
|
|
3127
|
-
<div class="storage-resize-handle" id="storageResizeHandle"></div>
|
|
3128
|
-
<div class="storage-value-view">
|
|
3129
|
-
<div class="storage-value-toolbar">
|
|
3130
|
-
<span style="font-size:10px;color:var(--text-dim);text-transform:uppercase;letter-spacing:1px">Value</span>
|
|
3131
|
-
<span id="storageSelectedKey" style="font-size:11px;color:var(--accent);margin-left:8px"></span>
|
|
3132
|
-
</div>
|
|
3133
|
-
<div class="storage-value-body" id="storageValueBody">
|
|
3134
|
-
<span style="color:var(--text-dim)">Select a key to view its value</span>
|
|
3135
|
-
</div>
|
|
3136
|
-
</div>
|
|
3137
|
-
</div>`;
|
|
3138
|
-
|
|
3139
|
-
$('storageSearch').addEventListener('input', (e) => {
|
|
3140
|
-
state.storage.searchFilter = e.target.value.toLowerCase().trim();
|
|
3141
|
-
renderStorage();
|
|
3142
|
-
});
|
|
3143
|
-
|
|
3144
|
-
// Drag resize handle for key list width
|
|
3145
|
-
const handle = $('storageResizeHandle');
|
|
3146
|
-
const layout = $('storageLayout');
|
|
3147
|
-
const keysPane = $('storageKeysPane');
|
|
3148
|
-
if (handle && layout && keysPane) {
|
|
3149
|
-
let dragging = false;
|
|
3150
|
-
let startX = 0;
|
|
3151
|
-
let startW = 0;
|
|
3152
|
-
handle.addEventListener('mousedown', (e) => {
|
|
3153
|
-
e.preventDefault();
|
|
3154
|
-
dragging = true;
|
|
3155
|
-
startX = e.clientX;
|
|
3156
|
-
startW = keysPane.offsetWidth;
|
|
3157
|
-
document.body.style.cursor = 'col-resize';
|
|
3158
|
-
document.body.style.userSelect = 'none';
|
|
3159
|
-
});
|
|
3160
|
-
document.addEventListener('mousemove', (e) => {
|
|
3161
|
-
if (!dragging) return;
|
|
3162
|
-
const newW = Math.max(120, Math.min(600, startW + (e.clientX - startX)));
|
|
3163
|
-
layout.style.gridTemplateColumns = `${newW}px 4px 1fr`;
|
|
3164
|
-
});
|
|
3165
|
-
document.addEventListener('mouseup', () => {
|
|
3166
|
-
if (!dragging) return;
|
|
3167
|
-
dragging = false;
|
|
3168
|
-
document.body.style.cursor = '';
|
|
3169
|
-
document.body.style.userSelect = '';
|
|
3170
|
-
});
|
|
3171
|
-
}
|
|
3172
|
-
}
|
|
3173
|
-
|
|
3174
|
-
let _storageRAF = null;
|
|
3175
|
-
|
|
3176
|
-
function handleStorageEvent(event) {
|
|
3177
|
-
if (event.type !== 'storage') return;
|
|
3178
|
-
if (!isTabEnabled('storage')) return;
|
|
3179
|
-
const { key, value, action } = event;
|
|
3180
|
-
if (action === 'set' || action === 'snapshot') {
|
|
3181
|
-
if (action === 'snapshot' && typeof key === 'object') {
|
|
3182
|
-
// Skip if data hasn't changed
|
|
3183
|
-
const newKeys = Object.keys(key).slice().sort().join(',');
|
|
3184
|
-
const oldKeys = state.storage.keys.slice().sort().join(',');
|
|
3185
|
-
if (newKeys === oldKeys) {
|
|
3186
|
-
// Check if values changed
|
|
3187
|
-
let same = true;
|
|
3188
|
-
for (const [k, v] of Object.entries(key)) {
|
|
3189
|
-
if (state.storage.entries[k] !== v) { same = false; break; }
|
|
3190
|
-
}
|
|
3191
|
-
if (same) return; // No changes, skip re-render
|
|
3192
|
-
}
|
|
3193
|
-
Object.entries(key).forEach(([k, v]) => {
|
|
3194
|
-
state.storage.entries[k] = v;
|
|
3195
|
-
if (!state.storage.keys.includes(k)) state.storage.keys.push(k);
|
|
3196
|
-
});
|
|
3197
|
-
} else {
|
|
3198
|
-
if (state.storage.entries[key] === value) return; // No change
|
|
3199
|
-
state.storage.entries[key] = value;
|
|
3200
|
-
if (!state.storage.keys.includes(key)) state.storage.keys.push(key);
|
|
3201
|
-
}
|
|
3202
|
-
} else if (action === 'remove') {
|
|
3203
|
-
if (!(key in state.storage.entries)) return; // Already removed
|
|
3204
|
-
delete state.storage.entries[key];
|
|
3205
|
-
state.storage.keys = state.storage.keys.filter(k => k !== key);
|
|
3206
|
-
if (state.storage.selected === key) state.storage.selected = null;
|
|
3207
|
-
}
|
|
3208
|
-
$('sBadge').textContent = state.storage.keys.length;
|
|
3209
|
-
// Debounce render via rAF
|
|
3210
|
-
if (!_storageRAF) {
|
|
3211
|
-
_storageRAF = requestAnimationFrame(() => {
|
|
3212
|
-
_storageRAF = null;
|
|
3213
|
-
renderStorage();
|
|
3214
|
-
});
|
|
3215
|
-
}
|
|
3216
|
-
}
|
|
3217
|
-
|
|
3218
|
-
function renderStorage() {
|
|
3219
|
-
const list = $('storageKeyList');
|
|
3220
|
-
const empty = $('storageEmpty');
|
|
3221
|
-
if (!list) return;
|
|
3222
|
-
|
|
3223
|
-
const { searchFilter } = state.storage;
|
|
3224
|
-
const visible = state.storage.keys.filter(k =>
|
|
3225
|
-
!searchFilter || k.toLowerCase().includes(searchFilter)
|
|
3226
|
-
);
|
|
3227
|
-
|
|
3228
|
-
empty.style.display = visible.length ? 'none' : 'flex';
|
|
3229
|
-
list.querySelectorAll('.storage-key-row').forEach(e => e.remove());
|
|
3230
|
-
|
|
3231
|
-
const frag = document.createDocumentFragment();
|
|
3232
|
-
visible.forEach(k => {
|
|
3233
|
-
const div = document.createElement('div');
|
|
3234
|
-
const val = state.storage.entries[k] || '';
|
|
3235
|
-
div.className = 'storage-key-row entry' + (k === state.storage.selected ? ' selected' : '');
|
|
3236
|
-
div.innerHTML = `
|
|
3237
|
-
<span class="key-name">${highlight(esc(k), searchFilter)}</span>
|
|
3238
|
-
<span class="key-size">${formatSize(val.length)}</span>`;
|
|
3239
|
-
div.onclick = () => { state.storage.selected = k; renderStorage(); renderStorageValue(); };
|
|
3240
|
-
frag.appendChild(div);
|
|
3241
|
-
});
|
|
3242
|
-
list.appendChild(frag);
|
|
3243
|
-
renderStorageValue();
|
|
3244
|
-
}
|
|
3245
|
-
|
|
3246
|
-
function renderStorageValue() {
|
|
3247
|
-
let body = $('storageValueBody');
|
|
3248
|
-
const keyLabel = $('storageSelectedKey');
|
|
3249
|
-
if (!body) return;
|
|
3250
|
-
const { selected, entries } = state.storage;
|
|
3251
|
-
if (!selected) {
|
|
3252
|
-
body.innerHTML = '<span style="color:var(--text-dim)">Select a key</span>';
|
|
3253
|
-
if (keyLabel) keyLabel.textContent = '';
|
|
3254
|
-
return;
|
|
3255
|
-
}
|
|
3256
|
-
if (keyLabel) keyLabel.textContent = selected;
|
|
3257
|
-
// Clone-replace to remove stale event listeners
|
|
3258
|
-
const fresh = body.cloneNode(false);
|
|
3259
|
-
body.parentNode.replaceChild(fresh, body);
|
|
3260
|
-
body = fresh;
|
|
3261
|
-
|
|
3262
|
-
let val = entries[selected];
|
|
3263
|
-
// Try to parse JSON strings into objects for tree display
|
|
3264
|
-
if (typeof val === 'string') {
|
|
3265
|
-
try { val = JSON.parse(val); } catch {}
|
|
3266
|
-
}
|
|
3267
|
-
|
|
3268
|
-
if (val && typeof val === 'object') {
|
|
3269
|
-
body.appendChild(createTreeNode(null, val, false));
|
|
3270
|
-
body.addEventListener('contextmenu', (e) => {
|
|
3271
|
-
e.preventDefault();
|
|
3272
|
-
showContextMenu(e, [
|
|
3273
|
-
{ label: 'Copy Value', action: () => navigator.clipboard.writeText(JSON.stringify(val, null, 2)) },
|
|
3274
|
-
{ label: 'Copy Key', action: () => navigator.clipboard.writeText(selected) },
|
|
3275
|
-
]);
|
|
3276
|
-
});
|
|
3277
|
-
} else {
|
|
3278
|
-
body.innerHTML = renderJSON(val);
|
|
3279
|
-
}
|
|
3280
|
-
}
|
|
3281
|
-
|
|
3282
|
-
function formatSize(bytes) {
|
|
3283
|
-
if (bytes < 1024) return `${bytes}b`;
|
|
3284
|
-
return `${(bytes/1024).toFixed(1)}kb`;
|
|
3285
|
-
}
|
|
3286
|
-
|
|
3287
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
3288
|
-
// REACT TREE PANEL
|
|
3289
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
3290
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
3291
|
-
// NATIVE LOGS PANEL
|
|
3292
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
3293
|
-
const _nativeState = { logs: [], connected: false, platform: null, levelFilter: 'all', searchFilter: '' };
|
|
3294
|
-
const MAX_NATIVE_LOGS = 2000;
|
|
3295
|
-
|
|
3296
|
-
function initNativeLogsPanel() {
|
|
3297
|
-
const panel = $('panel-native');
|
|
3298
|
-
if (!panel) return;
|
|
3299
|
-
panel.innerHTML = `
|
|
3300
|
-
<div class="panel-toolbar">
|
|
3301
|
-
<span class="panel-label">Native Logs</span>
|
|
3302
|
-
<span class="badge" id="nativeBadge">0</span>
|
|
3303
|
-
<div class="ml-auto" style="display:flex;align-items:center;gap:6px">
|
|
3304
|
-
<span class="native-status" id="nativeStatus">Detecting...</span>
|
|
3305
|
-
<button class="panel-clear-btn" id="nativeClear">Clear</button>
|
|
3306
|
-
</div>
|
|
3307
|
-
</div>
|
|
3308
|
-
<div class="native-connect-panel" id="nativeConnectPanel">
|
|
3309
|
-
<div class="native-hero">
|
|
3310
|
-
<div style="font-size:36px;opacity:0.15;margin-bottom:12px">📱</div>
|
|
3311
|
-
<div style="font-size:14px;font-weight:600;color:var(--text);margin-bottom:6px">Native Logs</div>
|
|
3312
|
-
<div style="font-size:11px;color:var(--text-dim);max-width:420px;line-height:1.7;margin-bottom:20px">
|
|
3313
|
-
Stream native crash logs, errors, and warnings directly in ReactoRadar.<br/>
|
|
3314
|
-
No need to open Android Studio or Xcode.
|
|
3315
|
-
</div>
|
|
3316
|
-
<div class="native-platform-cards">
|
|
3317
|
-
<div class="native-card" id="nativeCardAndroid">
|
|
3318
|
-
<div class="native-card-icon">🤖</div>
|
|
3319
|
-
<div class="native-card-title">Android</div>
|
|
3320
|
-
<div class="native-card-hint">Requires: <code>adb</code> in PATH (Android SDK)</div>
|
|
3321
|
-
<div class="native-card-prereq">
|
|
3322
|
-
<div class="native-prereq-step"><b>Prerequisites:</b></div>
|
|
3323
|
-
<div class="native-prereq-step">1. Enable <b>Developer Options</b> on device<br/><span style="color:var(--text-dim);font-size:9px">Settings → About Phone → Tap Build Number 7 times</span></div>
|
|
3324
|
-
<div class="native-prereq-step">2. Enable <b>USB Debugging</b><br/><span style="color:var(--text-dim);font-size:9px">Settings → Developer Options → USB Debugging → ON</span></div>
|
|
3325
|
-
<div class="native-prereq-step">3. Connect device via USB and accept the prompt</div>
|
|
3326
|
-
<div class="native-prereq-step">4. Verify: run <code>adb devices</code> in terminal</div>
|
|
3327
|
-
</div>
|
|
3328
|
-
<div id="nativeAndroidStatus" class="native-detect-status"></div>
|
|
3329
|
-
<button class="native-connect-btn" id="nativeConnectAndroid">Connect Android</button>
|
|
3330
|
-
</div>
|
|
3331
|
-
<div class="native-card" id="nativeCardIOS">
|
|
3332
|
-
<div class="native-card-icon">🍎</div>
|
|
3333
|
-
<div class="native-card-title">iOS</div>
|
|
3334
|
-
<div class="native-card-hint">Simulator or USB device</div>
|
|
3335
|
-
<div class="native-card-prereq">
|
|
3336
|
-
<div class="native-prereq-step"><b>Simulator:</b></div>
|
|
3337
|
-
<div class="native-prereq-step">Requires Xcode Command Line Tools<br/><code>xcode-select --install</code></div>
|
|
3338
|
-
<div class="native-prereq-step" style="margin-top:6px"><b>Real Device (USB):</b></div>
|
|
3339
|
-
<div class="native-prereq-step">1. Install: <code>brew install libimobiledevice</code></div>
|
|
3340
|
-
<div class="native-prereq-step">2. Connect device, tap <b>Trust</b> on the prompt</div>
|
|
3341
|
-
<div class="native-prereq-step">3. Verify: <code>idevice_id -l</code> shows device UDID</div>
|
|
3342
|
-
</div>
|
|
3343
|
-
<div id="nativeIOSStatus" class="native-detect-status"></div>
|
|
3344
|
-
<div style="display:flex;gap:6px;margin-top:8px">
|
|
3345
|
-
<button class="native-connect-btn" id="nativeConnectIOSSim">Simulator</button>
|
|
3346
|
-
<button class="native-connect-btn" id="nativeConnectIOSDevice">USB Device</button>
|
|
3347
|
-
</div>
|
|
3348
|
-
</div>
|
|
3349
|
-
</div>
|
|
3350
|
-
</div>
|
|
3351
|
-
</div>
|
|
3352
|
-
<div class="native-logs-area" id="nativeLogsArea" style="display:none">
|
|
3353
|
-
<div class="native-filter-bar">
|
|
3354
|
-
<input id="nativeSearch" class="net-search-input" placeholder="Filter logs..." />
|
|
3355
|
-
<div class="native-level-filters" id="nativeLevelFilters">
|
|
3356
|
-
<button class="net-status-btn active" data-level="all">All</button>
|
|
3357
|
-
<button class="net-status-btn" data-level="fatal">Fatal</button>
|
|
3358
|
-
<button class="net-status-btn" data-level="error">Error</button>
|
|
3359
|
-
<button class="net-status-btn" data-level="warn">Warn</button>
|
|
3360
|
-
<button class="net-status-btn" data-level="info">Info</button>
|
|
3361
|
-
<button class="net-status-btn" data-level="debug">Debug</button>
|
|
3362
|
-
</div>
|
|
3363
|
-
<div style="margin-left:auto;display:flex;gap:6px;align-items:center">
|
|
3364
|
-
<button class="panel-clear-btn" id="nativeLogsClear">Clear</button>
|
|
3365
|
-
<button class="panel-clear-btn" id="nativeDisconnect" style="color:var(--red)">Disconnect</button>
|
|
3366
|
-
</div>
|
|
3367
|
-
</div>
|
|
3368
|
-
<div class="native-log-list" id="nativeLogList"></div>
|
|
3369
|
-
</div>`;
|
|
3370
|
-
|
|
3371
|
-
// Connect buttons — auto-enable tab when user clicks connect
|
|
3372
|
-
function _enableNativeTab() {
|
|
3373
|
-
const vis = getTabVisibility();
|
|
3374
|
-
if (!vis['native']) {
|
|
3375
|
-
vis['native'] = true;
|
|
3376
|
-
setTabVisibility(vis);
|
|
3377
|
-
applyTabVisibility();
|
|
3378
|
-
}
|
|
3379
|
-
}
|
|
3380
|
-
$('nativeConnectAndroid')?.addEventListener('click', () => { _enableNativeTab(); window.electronAPI?.startNativeLogs('android'); });
|
|
3381
|
-
$('nativeConnectIOSSim')?.addEventListener('click', () => { _enableNativeTab(); window.electronAPI?.startNativeLogs('ios-sim'); });
|
|
3382
|
-
$('nativeConnectIOSDevice')?.addEventListener('click', () => { _enableNativeTab(); window.electronAPI?.startNativeLogs('ios-device'); });
|
|
3383
|
-
$('nativeDisconnect')?.addEventListener('click', () => window.electronAPI?.stopNativeLogs());
|
|
3384
|
-
|
|
3385
|
-
// Clear buttons (toolbar + logs area)
|
|
3386
|
-
$('nativeClear')?.addEventListener('click', _clearNativeLogs);
|
|
3387
|
-
$('nativeLogsClear')?.addEventListener('click', _clearNativeLogs);
|
|
3388
|
-
|
|
3389
|
-
// Level filter
|
|
3390
|
-
$('nativeLevelFilters')?.addEventListener('click', (e) => {
|
|
3391
|
-
const btn = e.target.closest('.net-status-btn');
|
|
3392
|
-
if (!btn) return;
|
|
3393
|
-
$('nativeLevelFilters').querySelectorAll('.net-status-btn').forEach(b => b.classList.remove('active'));
|
|
3394
|
-
btn.classList.add('active');
|
|
3395
|
-
_nativeState.levelFilter = btn.dataset.level;
|
|
3396
|
-
_renderNativeLogs();
|
|
3397
|
-
});
|
|
3398
|
-
|
|
3399
|
-
// Search
|
|
3400
|
-
$('nativeSearch')?.addEventListener('input', (e) => {
|
|
3401
|
-
_nativeState.searchFilter = e.target.value.toLowerCase().trim();
|
|
3402
|
-
_renderNativeLogs();
|
|
3403
|
-
});
|
|
3404
|
-
|
|
3405
|
-
// IPC: receive native logs
|
|
3406
|
-
window.electronAPI?.on('native-log', (log) => {
|
|
3407
|
-
if (!isTabEnabled('native')) return;
|
|
3408
|
-
_nativeState.logs.push(log);
|
|
3409
|
-
if (_nativeState.logs.length > MAX_NATIVE_LOGS) {
|
|
3410
|
-
_nativeState.logs = _nativeState.logs.slice(-MAX_NATIVE_LOGS);
|
|
3411
|
-
}
|
|
3412
|
-
$('nativeBadge').textContent = _nativeState.logs.length;
|
|
3413
|
-
_appendNativeLog(log);
|
|
3414
|
-
});
|
|
3415
|
-
|
|
3416
|
-
// IPC: connection status
|
|
3417
|
-
window.electronAPI?.on('native-status', (status) => {
|
|
3418
|
-
_nativeState.connected = status.connected;
|
|
3419
|
-
_nativeState.platform = status.platform || null;
|
|
3420
|
-
const statusEl = $('nativeStatus');
|
|
3421
|
-
const connectPanel = $('nativeConnectPanel');
|
|
3422
|
-
const logsArea = $('nativeLogsArea');
|
|
3423
|
-
|
|
3424
|
-
if (status.connected) {
|
|
3425
|
-
if (statusEl) { statusEl.textContent = `Connected (${status.platform})`; statusEl.style.color = 'var(--green)'; }
|
|
3426
|
-
if (connectPanel) connectPanel.style.display = 'none';
|
|
3427
|
-
if (logsArea) logsArea.style.display = 'flex';
|
|
3428
|
-
} else {
|
|
3429
|
-
if (statusEl) {
|
|
3430
|
-
statusEl.textContent = status.error || 'Not connected';
|
|
3431
|
-
statusEl.style.color = status.error ? 'var(--red)' : 'var(--text-dim)';
|
|
3432
|
-
}
|
|
3433
|
-
if (connectPanel) connectPanel.style.display = 'flex';
|
|
3434
|
-
if (logsArea) logsArea.style.display = 'none';
|
|
3435
|
-
}
|
|
3436
|
-
});
|
|
3437
|
-
|
|
3438
|
-
// Auto-detect platform and auto-connect
|
|
3439
|
-
_autoDetectNative();
|
|
3440
|
-
}
|
|
3441
|
-
|
|
3442
|
-
function _clearNativeLogs() {
|
|
3443
|
-
_nativeState.logs = [];
|
|
3444
|
-
if ($('nativeBadge')) $('nativeBadge').textContent = '0';
|
|
3445
|
-
const list = $('nativeLogList');
|
|
3446
|
-
if (list) list.innerHTML = '';
|
|
3447
|
-
}
|
|
3448
|
-
|
|
3449
|
-
async function _autoDetectNative() {
|
|
3450
|
-
const statusEl = $('nativeStatus');
|
|
3451
|
-
try {
|
|
3452
|
-
const result = await window.electronAPI?.detectNativePlatform();
|
|
3453
|
-
if (!result) { if (statusEl) { statusEl.textContent = 'Detection unavailable'; statusEl.style.color = 'var(--text-dim)'; } return; }
|
|
3454
|
-
|
|
3455
|
-
// Update card statuses
|
|
3456
|
-
const androidStatus = $('nativeAndroidStatus');
|
|
3457
|
-
const iosStatus = $('nativeIOSStatus');
|
|
3458
|
-
if (androidStatus) {
|
|
3459
|
-
if (result.android) { androidStatus.innerHTML = '<span style="color:var(--green)">Device detected</span>'; }
|
|
3460
|
-
else if (result.adbPath) { androidStatus.innerHTML = '<span style="color:var(--orange)">adb found — no device connected</span>'; }
|
|
3461
|
-
else { androidStatus.innerHTML = '<span style="color:var(--text-dim)">adb not found</span>'; }
|
|
3462
|
-
}
|
|
3463
|
-
if (iosStatus) {
|
|
3464
|
-
const parts = [];
|
|
3465
|
-
if (result.iosSim) parts.push('<span style="color:var(--green)">Simulator running</span>');
|
|
3466
|
-
if (result.iosDevice) parts.push('<span style="color:var(--green)">USB device detected</span>');
|
|
3467
|
-
if (!parts.length) parts.push('<span style="color:var(--text-dim)">No device detected</span>');
|
|
3468
|
-
iosStatus.innerHTML = parts.join(' · ');
|
|
3469
|
-
}
|
|
3470
|
-
|
|
3471
|
-
// Show detection result — user clicks Connect to start
|
|
3472
|
-
if (result.android || result.iosSim || result.iosDevice) {
|
|
3473
|
-
const detected = [result.android ? 'Android' : '', result.iosSim ? 'iOS Sim' : '', result.iosDevice ? 'iOS Device' : ''].filter(Boolean).join(', ');
|
|
3474
|
-
if (statusEl) { statusEl.textContent = `Detected: ${detected} — click Connect to start`; statusEl.style.color = 'var(--accent)'; }
|
|
3475
|
-
} else {
|
|
3476
|
-
if (statusEl) { statusEl.textContent = 'No device detected'; statusEl.style.color = 'var(--text-dim)'; }
|
|
3477
|
-
}
|
|
3478
|
-
} catch {
|
|
3479
|
-
if (statusEl) { statusEl.textContent = 'Detection failed'; statusEl.style.color = 'var(--text-dim)'; }
|
|
3480
|
-
}
|
|
3481
|
-
}
|
|
3482
|
-
|
|
3483
|
-
function _appendNativeLog(log) {
|
|
3484
|
-
const list = $('nativeLogList');
|
|
3485
|
-
if (!list) return;
|
|
3486
|
-
|
|
3487
|
-
// Check filters
|
|
3488
|
-
if (_nativeState.levelFilter !== 'all' && log.level !== _nativeState.levelFilter) return;
|
|
3489
|
-
if (_nativeState.searchFilter && !log.message?.toLowerCase().includes(_nativeState.searchFilter) && !log.tag?.toLowerCase().includes(_nativeState.searchFilter)) return;
|
|
3490
|
-
|
|
3491
|
-
const isExpandable = log.level === 'error' || log.level === 'fatal' || (log.message || '').length > 200;
|
|
3492
|
-
const row = document.createElement('div');
|
|
3493
|
-
row.className = `native-log-row native-${log.level || 'info'}`;
|
|
3494
|
-
|
|
3495
|
-
const time = log.time || new Date(log.ts).toLocaleTimeString('en', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
3496
|
-
|
|
3497
|
-
// Header line (always visible)
|
|
3498
|
-
const header = document.createElement('div');
|
|
3499
|
-
header.className = 'native-log-header';
|
|
3500
|
-
header.innerHTML = `<span class="native-log-time">${esc(time)}</span>`
|
|
3501
|
-
+ `<span class="native-log-level">${esc((log.level || 'info').toUpperCase())}</span>`
|
|
3502
|
-
+ (log.tag ? `<span class="native-log-tag">${esc(log.tag)}</span>` : '')
|
|
3503
|
-
+ `<span class="native-log-preview">${esc((log.message || '').split('\\n')[0].slice(0, 200))}</span>`;
|
|
3504
|
-
row.appendChild(header);
|
|
3505
|
-
|
|
3506
|
-
// Expandable full message (for errors and long messages)
|
|
3507
|
-
if (isExpandable) {
|
|
3508
|
-
const fullMsg = document.createElement('div');
|
|
3509
|
-
fullMsg.className = 'native-log-full';
|
|
3510
|
-
fullMsg.style.display = 'none';
|
|
3511
|
-
fullMsg.textContent = log.message || '';
|
|
3512
|
-
row.appendChild(fullMsg);
|
|
3513
|
-
|
|
3514
|
-
header.style.cursor = 'pointer';
|
|
3515
|
-
header.addEventListener('click', () => {
|
|
3516
|
-
const open = fullMsg.style.display !== 'none';
|
|
3517
|
-
fullMsg.style.display = open ? 'none' : 'block';
|
|
3518
|
-
row.classList.toggle('expanded', !open);
|
|
3519
|
-
});
|
|
3520
|
-
}
|
|
3521
|
-
|
|
3522
|
-
// Right-click to copy
|
|
3523
|
-
row.addEventListener('contextmenu', (e) => {
|
|
3524
|
-
e.preventDefault();
|
|
3525
|
-
showContextMenu(e, [
|
|
3526
|
-
{ label: 'Copy Message', action: () => navigator.clipboard.writeText(log.message || '') },
|
|
3527
|
-
{ label: 'Copy Raw Line', action: () => navigator.clipboard.writeText(log.raw || log.message || '') },
|
|
3528
|
-
...(log.tag ? [{ label: `Copy Tag (${log.tag})`, action: () => navigator.clipboard.writeText(log.tag) }] : []),
|
|
3529
|
-
]);
|
|
3530
|
-
});
|
|
3531
|
-
|
|
3532
|
-
list.appendChild(row);
|
|
3533
|
-
|
|
3534
|
-
// Cap DOM rows
|
|
3535
|
-
while (list.children.length > 1000) list.firstChild.remove();
|
|
3536
|
-
|
|
3537
|
-
// Auto-scroll if near bottom
|
|
3538
|
-
const atBottom = (list.scrollHeight - list.scrollTop - list.clientHeight) < 150;
|
|
3539
|
-
if (atBottom) list.scrollTop = list.scrollHeight;
|
|
3540
|
-
}
|
|
3541
|
-
|
|
3542
|
-
function _renderNativeLogs() {
|
|
3543
|
-
const list = $('nativeLogList');
|
|
3544
|
-
if (!list) return;
|
|
3545
|
-
list.innerHTML = '';
|
|
3546
|
-
_nativeState.logs.forEach(log => _appendNativeLog(log));
|
|
3547
|
-
}
|
|
3548
|
-
|
|
3549
|
-
function initReactPanel() {
|
|
3550
|
-
const panel = $('panel-react');
|
|
3551
|
-
panel.innerHTML = `
|
|
3552
|
-
<div class="panel-toolbar">
|
|
3553
|
-
<span class="panel-label">React Tree</span>
|
|
3554
|
-
</div>
|
|
3555
|
-
<div class="react-panel-inner">
|
|
3556
|
-
<div class="react-connect-hint" id="reactHint">
|
|
3557
|
-
<div class="icon" style="font-size:40px;opacity:.2">⚛️</div>
|
|
3558
|
-
<div class="label">React DevTools</div>
|
|
3559
|
-
<div class="hint">Opens as a separate window connected to your app via port 8097</div>
|
|
3560
|
-
<div class="hint" style="margin-top:8px;color:var(--yellow)">Note: The RN inspector overlay won't work while React DevTools is connected. Close the DevTools window to use the built-in inspector.</div>
|
|
3561
|
-
<button class="btn-launch" id="btnReactDT" style="margin-top:12px">Open React DevTools ↗</button>
|
|
3562
|
-
</div>
|
|
3563
|
-
</div>`;
|
|
3564
|
-
|
|
3565
|
-
$('btnReactDT').addEventListener('click', () => {
|
|
3566
|
-
window.electronAPI?.openReactDevTools();
|
|
3567
|
-
});
|
|
3568
|
-
}
|
|
3569
|
-
|
|
3570
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
3571
|
-
// SETTINGS PANEL
|
|
3572
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
3573
|
-
function getStoredTheme() {
|
|
3574
|
-
try { return localStorage.getItem('rn-debug-theme') || 'dark'; } catch { return 'dark'; }
|
|
3575
|
-
}
|
|
3576
|
-
function setStoredTheme(t) {
|
|
3577
|
-
try { localStorage.setItem('rn-debug-theme', t); } catch {}
|
|
3578
|
-
}
|
|
3579
|
-
function getStoredFontSize() {
|
|
3580
|
-
try { return parseInt(localStorage.getItem('rn-debug-fontsize')) || 12; } catch { return 12; }
|
|
3581
|
-
}
|
|
3582
|
-
function setStoredFontSize(s) {
|
|
3583
|
-
try { localStorage.setItem('rn-debug-fontsize', String(s)); } catch {}
|
|
3584
|
-
}
|
|
3585
|
-
|
|
3586
|
-
const FONT_FAMILIES = [
|
|
3587
|
-
{ label: 'SF Mono', value: "'SFMono-Regular', 'SF Mono', monospace" },
|
|
3588
|
-
{ label: 'Menlo', value: "Menlo, monospace" },
|
|
3589
|
-
{ label: 'Monaco', value: "Monaco, monospace" },
|
|
3590
|
-
{ label: 'Courier New', value: "'Courier New', Courier, monospace" },
|
|
3591
|
-
{ label: 'System Mono', value: "monospace" },
|
|
3592
|
-
];
|
|
3593
|
-
function getStoredFontFamily() {
|
|
3594
|
-
try {
|
|
3595
|
-
const saved = localStorage.getItem('rn-debug-fontfamily');
|
|
3596
|
-
// Reset if saved value was a removed font
|
|
3597
|
-
if (saved && !FONT_FAMILIES.some(f => f.value === saved)) return FONT_FAMILIES[0].value;
|
|
3598
|
-
return saved || FONT_FAMILIES[0].value;
|
|
3599
|
-
} catch { return FONT_FAMILIES[0].value; }
|
|
3600
|
-
}
|
|
3601
|
-
function setStoredFontFamily(f) {
|
|
3602
|
-
try { localStorage.setItem('rn-debug-fontfamily', f); } catch {}
|
|
3603
|
-
}
|
|
3604
|
-
function applyFontFamily(family) {
|
|
3605
|
-
document.body.style.fontFamily = family;
|
|
3606
|
-
}
|
|
3607
|
-
|
|
3608
|
-
// ─── Hidden URLs (Network tab) ───────────────────────────────────────────────
|
|
3609
|
-
function getHiddenURLs() {
|
|
3610
|
-
try { return JSON.parse(localStorage.getItem('rn-debug-hidden-urls') || '[]'); } catch { return []; }
|
|
3611
|
-
}
|
|
3612
|
-
function setHiddenURLs(list) {
|
|
3613
|
-
try { localStorage.setItem('rn-debug-hidden-urls', JSON.stringify(list)); } catch {}
|
|
3614
|
-
}
|
|
3615
|
-
function addHiddenURL(url) {
|
|
3616
|
-
// Extract the base URL (without query params) as the pattern
|
|
3617
|
-
const pattern = url.split('?')[0];
|
|
3618
|
-
const list = getHiddenURLs();
|
|
3619
|
-
if (!list.includes(pattern)) {
|
|
3620
|
-
list.push(pattern);
|
|
3621
|
-
setHiddenURLs(list);
|
|
3622
|
-
}
|
|
3623
|
-
_updateHiddenBadge();
|
|
3624
|
-
}
|
|
3625
|
-
function removeHiddenURL(pattern) {
|
|
3626
|
-
const list = getHiddenURLs().filter(u => u !== pattern);
|
|
3627
|
-
setHiddenURLs(list);
|
|
3628
|
-
_updateHiddenBadge();
|
|
3629
|
-
}
|
|
3630
|
-
function isURLHidden(url) {
|
|
3631
|
-
const hidden = getHiddenURLs();
|
|
3632
|
-
if (!hidden.length) return false;
|
|
3633
|
-
const base = url.split('?')[0];
|
|
3634
|
-
return hidden.some(pattern => base === pattern || base.startsWith(pattern));
|
|
3635
|
-
}
|
|
3636
|
-
function _updateHiddenBadge() {
|
|
3637
|
-
const btn = $('netHiddenBtn');
|
|
3638
|
-
if (!btn) return;
|
|
3639
|
-
const count = getHiddenURLs().length;
|
|
3640
|
-
btn.textContent = count > 0 ? `Hidden (${count})` : 'Hidden';
|
|
3641
|
-
btn.style.display = count > 0 ? '' : 'none';
|
|
3642
|
-
}
|
|
3643
|
-
|
|
3644
|
-
// ─── Tab Visibility ──────────────────────────────────────────────────────────
|
|
3645
|
-
const TAB_CONFIG = [
|
|
3646
|
-
{ id: 'console', label: 'Console', icon: '🖥', essential: true },
|
|
3647
|
-
{ id: 'network', label: 'Network', icon: '📡', essential: true },
|
|
3648
|
-
{ id: 'redux', label: 'Redux', icon: '🔲', essential: false },
|
|
3649
|
-
{ id: 'ga4', label: 'GA4 Events', icon: '📊', essential: false },
|
|
3650
|
-
{ id: 'storage', label: 'AsyncStorage', icon: '💾', essential: false },
|
|
3651
|
-
{ id: 'memory', label: 'Memory', icon: '🧠', essential: false, defaultHidden: true },
|
|
3652
|
-
{ id: 'performance', label: 'Performance', icon: '⚡', essential: false, defaultHidden: true },
|
|
3653
|
-
{ id: 'react', label: 'React Tree', icon: '⚛️', essential: false },
|
|
3654
|
-
{ id: 'native', label: 'Native Logs', icon: '📱', essential: false, defaultHidden: true },
|
|
3655
|
-
];
|
|
3656
|
-
function getTabVisibility() {
|
|
3657
|
-
try {
|
|
3658
|
-
const saved = JSON.parse(localStorage.getItem('rn-debug-tab-visibility') || '{}');
|
|
3659
|
-
const result = {};
|
|
3660
|
-
TAB_CONFIG.forEach(t => { result[t.id] = saved[t.id] !== undefined ? saved[t.id] : !t.defaultHidden; });
|
|
3661
|
-
return result;
|
|
3662
|
-
} catch {
|
|
3663
|
-
const result = {};
|
|
3664
|
-
TAB_CONFIG.forEach(t => { result[t.id] = !t.defaultHidden; });
|
|
3665
|
-
return result;
|
|
3666
|
-
}
|
|
3667
|
-
}
|
|
3668
|
-
function setTabVisibility(vis) {
|
|
3669
|
-
try { localStorage.setItem('rn-debug-tab-visibility', JSON.stringify(vis)); } catch {}
|
|
3670
|
-
}
|
|
3671
|
-
function getTabOrder() {
|
|
3672
|
-
try {
|
|
3673
|
-
const saved = JSON.parse(localStorage.getItem('rn-debug-tab-order') || '[]');
|
|
3674
|
-
if (saved.length) {
|
|
3675
|
-
// Merge: keep saved order, append any new tabs not in saved list
|
|
3676
|
-
const allIds = TAB_CONFIG.map(t => t.id);
|
|
3677
|
-
const merged = saved.filter(id => allIds.includes(id));
|
|
3678
|
-
allIds.forEach(id => { if (!merged.includes(id)) merged.push(id); });
|
|
3679
|
-
return merged;
|
|
3680
|
-
}
|
|
3681
|
-
} catch {}
|
|
3682
|
-
return TAB_CONFIG.map(t => t.id);
|
|
3683
|
-
}
|
|
3684
|
-
function setTabOrder(order) {
|
|
3685
|
-
try { localStorage.setItem('rn-debug-tab-order', JSON.stringify(order)); } catch {}
|
|
3686
|
-
}
|
|
3687
|
-
function applyTabVisibility() {
|
|
3688
|
-
const vis = getTabVisibility();
|
|
3689
|
-
const order = getTabOrder();
|
|
3690
|
-
const nav = $('sidebar');
|
|
3691
|
-
if (!nav) return;
|
|
3692
|
-
// Reorder nav buttons according to saved order + hide disabled ones
|
|
3693
|
-
// Settings button always stays last
|
|
3694
|
-
const settingsBtn = nav.querySelector('.nav-btn[data-panel="settings"]');
|
|
3695
|
-
const spacer = nav.querySelector('.nav-spacer');
|
|
3696
|
-
const anchor = spacer || settingsBtn; // insert before spacer or settings
|
|
3697
|
-
order.forEach(tabId => {
|
|
3698
|
-
const btn = nav.querySelector(`.nav-btn[data-panel="${tabId}"]`);
|
|
3699
|
-
if (btn) {
|
|
3700
|
-
btn.style.display = vis[tabId] ? '' : 'none';
|
|
3701
|
-
nav.insertBefore(btn, anchor);
|
|
3702
|
-
}
|
|
3703
|
-
});
|
|
3704
|
-
// If active panel is now hidden, switch to first visible
|
|
3705
|
-
if (!vis[state.activePanel]) {
|
|
3706
|
-
const first = order.find(id => vis[id]);
|
|
3707
|
-
if (first) switchPanel(first);
|
|
3708
|
-
}
|
|
3709
|
-
}
|
|
3710
|
-
function isTabEnabled(tabId) {
|
|
3711
|
-
return getTabVisibility()[tabId] !== false;
|
|
3712
|
-
}
|
|
3713
|
-
|
|
3714
|
-
function _buildTabVisGrid() {
|
|
3715
|
-
const container = $('tabVisibilityGrid');
|
|
3716
|
-
if (!container) return;
|
|
3717
|
-
container.innerHTML = '';
|
|
3718
|
-
const vis = getTabVisibility();
|
|
3719
|
-
const order = getTabOrder();
|
|
3720
|
-
let dragSrc = null;
|
|
3721
|
-
|
|
3722
|
-
order.forEach(tabId => {
|
|
3723
|
-
const t = TAB_CONFIG.find(c => c.id === tabId);
|
|
3724
|
-
if (!t) return;
|
|
3725
|
-
|
|
3726
|
-
const item = document.createElement('div');
|
|
3727
|
-
item.className = `tab-vis-item ${vis[t.id] ? 'active' : 'inactive'}`;
|
|
3728
|
-
item.dataset.tab = t.id;
|
|
3729
|
-
item.draggable = true;
|
|
3730
|
-
|
|
3731
|
-
// Drag handle
|
|
3732
|
-
const drag = document.createElement('span');
|
|
3733
|
-
drag.className = 'tab-vis-drag';
|
|
3734
|
-
drag.textContent = '⠿';
|
|
3735
|
-
item.appendChild(drag);
|
|
3736
|
-
|
|
3737
|
-
// Checkbox
|
|
3738
|
-
const check = document.createElement('input');
|
|
3739
|
-
check.type = 'checkbox';
|
|
3740
|
-
check.className = 'tab-vis-check';
|
|
3741
|
-
check.checked = vis[t.id];
|
|
3742
|
-
if (t.essential) check.disabled = true;
|
|
3743
|
-
check.addEventListener('change', () => {
|
|
3744
|
-
const v = getTabVisibility();
|
|
3745
|
-
v[t.id] = check.checked;
|
|
3746
|
-
setTabVisibility(v);
|
|
3747
|
-
applyTabVisibility();
|
|
3748
|
-
item.classList.toggle('active', check.checked);
|
|
3749
|
-
item.classList.toggle('inactive', !check.checked);
|
|
3750
|
-
});
|
|
3751
|
-
item.appendChild(check);
|
|
3752
|
-
|
|
3753
|
-
// Icon + label
|
|
3754
|
-
const icon = document.createElement('span');
|
|
3755
|
-
icon.className = 'tab-vis-icon';
|
|
3756
|
-
icon.textContent = t.icon;
|
|
3757
|
-
item.appendChild(icon);
|
|
3758
|
-
|
|
3759
|
-
const label = document.createElement('span');
|
|
3760
|
-
label.className = 'tab-vis-label';
|
|
3761
|
-
label.textContent = t.label;
|
|
3762
|
-
item.appendChild(label);
|
|
3763
|
-
|
|
3764
|
-
if (t.essential) {
|
|
3765
|
-
const req = document.createElement('span');
|
|
3766
|
-
req.className = 'tab-vis-required';
|
|
3767
|
-
req.textContent = 'Required';
|
|
3768
|
-
item.appendChild(req);
|
|
3769
|
-
}
|
|
3770
|
-
|
|
3771
|
-
// Drag events
|
|
3772
|
-
item.addEventListener('dragstart', (e) => {
|
|
3773
|
-
dragSrc = item;
|
|
3774
|
-
item.classList.add('dragging');
|
|
3775
|
-
e.dataTransfer.effectAllowed = 'move';
|
|
3776
|
-
});
|
|
3777
|
-
item.addEventListener('dragend', () => {
|
|
3778
|
-
item.classList.remove('dragging');
|
|
3779
|
-
container.querySelectorAll('.tab-vis-item').forEach(el => el.classList.remove('drag-over'));
|
|
3780
|
-
dragSrc = null;
|
|
3781
|
-
});
|
|
3782
|
-
item.addEventListener('dragover', (e) => {
|
|
3783
|
-
e.preventDefault();
|
|
3784
|
-
e.dataTransfer.dropEffect = 'move';
|
|
3785
|
-
if (dragSrc && dragSrc !== item) item.classList.add('drag-over');
|
|
3786
|
-
});
|
|
3787
|
-
item.addEventListener('dragleave', () => {
|
|
3788
|
-
item.classList.remove('drag-over');
|
|
3789
|
-
});
|
|
3790
|
-
item.addEventListener('drop', (e) => {
|
|
3791
|
-
e.preventDefault();
|
|
3792
|
-
item.classList.remove('drag-over');
|
|
3793
|
-
if (!dragSrc || dragSrc === item) return;
|
|
3794
|
-
// Reorder: move dragSrc before or after this item
|
|
3795
|
-
const items = [...container.querySelectorAll('.tab-vis-item')];
|
|
3796
|
-
const fromIdx = items.indexOf(dragSrc);
|
|
3797
|
-
const toIdx = items.indexOf(item);
|
|
3798
|
-
if (fromIdx < toIdx) {
|
|
3799
|
-
container.insertBefore(dragSrc, item.nextSibling);
|
|
3800
|
-
} else {
|
|
3801
|
-
container.insertBefore(dragSrc, item);
|
|
3802
|
-
}
|
|
3803
|
-
// Save new order
|
|
3804
|
-
const newOrder = [...container.querySelectorAll('.tab-vis-item')].map(el => el.dataset.tab);
|
|
3805
|
-
setTabOrder(newOrder);
|
|
3806
|
-
applyTabVisibility();
|
|
3807
|
-
});
|
|
3808
|
-
|
|
3809
|
-
container.appendChild(item);
|
|
3810
|
-
});
|
|
3811
|
-
}
|
|
3812
|
-
|
|
3813
|
-
function getStoredAppName() {
|
|
3814
|
-
try { return localStorage.getItem('rn-debug-appname') || 'ReactoRadar'; } catch { return 'ReactoRadar'; }
|
|
3815
|
-
}
|
|
3816
|
-
function setStoredAppName(n) {
|
|
3817
|
-
try { localStorage.setItem('rn-debug-appname', n); } catch {}
|
|
3818
|
-
}
|
|
3819
|
-
function getStoredMetroPort() {
|
|
3820
|
-
try { return parseInt(localStorage.getItem('rn-debug-metro-port')) || 8081; } catch { return 8081; }
|
|
3821
|
-
}
|
|
3822
|
-
function setStoredMetroPort(p) {
|
|
3823
|
-
try { localStorage.setItem('rn-debug-metro-port', String(p)); } catch {}
|
|
3824
|
-
}
|
|
3825
|
-
function applyAppName(name) {
|
|
3826
|
-
const logo = document.querySelector('.logo');
|
|
3827
|
-
if (logo) {
|
|
3828
|
-
// Split name — first part normal, last word in accent span
|
|
3829
|
-
const words = name.split(/(?=[A-Z])/);
|
|
3830
|
-
if (words.length >= 2) {
|
|
3831
|
-
logo.innerHTML = words.slice(0, -1).join('') + '<span>' + words[words.length - 1] + '</span>';
|
|
3832
|
-
} else {
|
|
3833
|
-
logo.textContent = name;
|
|
3834
|
-
}
|
|
3835
|
-
}
|
|
3836
|
-
document.title = name;
|
|
3837
|
-
}
|
|
3838
|
-
|
|
3839
|
-
function applyTheme(theme) {
|
|
3840
|
-
document.documentElement.setAttribute('data-theme', theme);
|
|
3841
|
-
// Tell main process (light themes need light nativeTheme for window chrome)
|
|
3842
|
-
const isLight = ['light', 'solarized-light'].includes(theme);
|
|
3843
|
-
window.electronAPI?.setTheme(isLight ? 'light' : 'dark');
|
|
3844
|
-
}
|
|
3845
|
-
|
|
3846
|
-
function applyFontSize(size) {
|
|
3847
|
-
document.documentElement.style.setProperty('--app-font-size', size + 'px');
|
|
3848
|
-
document.body.style.fontSize = size + 'px';
|
|
3849
|
-
// Inject/update a <style> tag so ALL current and future elements get the size
|
|
3850
|
-
let styleEl = document.getElementById('dynamic-font-size');
|
|
3851
|
-
if (!styleEl) {
|
|
3852
|
-
styleEl = document.createElement('style');
|
|
3853
|
-
styleEl.id = 'dynamic-font-size';
|
|
3854
|
-
document.head.appendChild(styleEl);
|
|
3855
|
-
}
|
|
3856
|
-
styleEl.textContent = `
|
|
3857
|
-
.log-preview, .log-body, .log-text, .log-caller-inline,
|
|
3858
|
-
.net-cell, .net-cell-name, .net-type, .net-initiator, .net-size, .net-time, .net-status,
|
|
3859
|
-
.detail-content, .kv-val, .kv-key,
|
|
3860
|
-
.rdx-type, .rdx-entry-detail, .rdx-store-key-label,
|
|
3861
|
-
.storage-value-body, .storage-key-row,
|
|
3862
|
-
.sources-code, .source-line-code,
|
|
3863
|
-
.ov-leaf, .ov-key, .ov-preview, .ov-str, .ov-num, .ov-bool, .ov-null, .ov-undef,
|
|
3864
|
-
.perf-meter-label,
|
|
3865
|
-
.settings-label, .settings-hint {
|
|
3866
|
-
font-size: ${size}px !important;
|
|
3867
|
-
}
|
|
3868
|
-
`;
|
|
3869
|
-
const display = $('fontSizeDisplay');
|
|
3870
|
-
if (display) display.textContent = size + 'px';
|
|
3871
|
-
}
|
|
3872
|
-
|
|
3873
|
-
function initSettingsPanel() {
|
|
3874
|
-
const panel = $('panel-settings');
|
|
3875
|
-
const current = getStoredTheme();
|
|
3876
|
-
const currentSize = getStoredFontSize();
|
|
3877
|
-
panel.innerHTML = `
|
|
3878
|
-
<div class="panel-toolbar">
|
|
3879
|
-
<span class="panel-label">Settings</span>
|
|
3880
|
-
</div>
|
|
3881
|
-
<div class="scroll-area">
|
|
3882
|
-
<div class="settings-two-col">
|
|
3883
|
-
<div class="settings-col-left">
|
|
3884
|
-
<div class="settings-section">
|
|
3885
|
-
<div class="settings-section-title">Appearance</div>
|
|
3886
|
-
<div class="settings-row" style="flex-direction:column;align-items:flex-start;gap:8px">
|
|
3887
|
-
<div>
|
|
3888
|
-
<div class="settings-label">Theme</div>
|
|
3889
|
-
<div class="settings-hint">Choose a color theme</div>
|
|
3890
|
-
</div>
|
|
3891
|
-
<div class="theme-grid" id="themeSwitcher"></div>
|
|
3892
|
-
</div>
|
|
3893
|
-
<div class="settings-row">
|
|
3894
|
-
<div>
|
|
3895
|
-
<div class="settings-label">Font Size</div>
|
|
3896
|
-
<div class="settings-hint">Adjust text size</div>
|
|
3897
|
-
</div>
|
|
3898
|
-
<div class="font-size-control">
|
|
3899
|
-
<button class="font-size-btn" id="fontSizeDown">A-</button>
|
|
3900
|
-
<span class="font-size-display" id="fontSizeDisplay">${currentSize}px</span>
|
|
3901
|
-
<button class="font-size-btn" id="fontSizeUp">A+</button>
|
|
3902
|
-
</div>
|
|
3903
|
-
</div>
|
|
3904
|
-
<div class="settings-row">
|
|
3905
|
-
<div>
|
|
3906
|
-
<div class="settings-label">Font Family</div>
|
|
3907
|
-
</div>
|
|
3908
|
-
<select id="fontFamilySelect" class="net-throttle-select" style="width:150px">
|
|
3909
|
-
${FONT_FAMILIES.map(f => `<option value="${esc(f.value)}" ${f.value === getStoredFontFamily() ? 'selected' : ''}>${esc(f.label)}</option>`).join('')}
|
|
3910
|
-
</select>
|
|
3911
|
-
</div>
|
|
3912
|
-
<div class="settings-row">
|
|
3913
|
-
<div>
|
|
3914
|
-
<div class="settings-label">App Name</div>
|
|
3915
|
-
</div>
|
|
3916
|
-
<div style="display:flex;align-items:center;gap:6px">
|
|
3917
|
-
<input id="appNameInput" class="net-search-input" style="width:120px;text-align:center" value="${getStoredAppName()}" />
|
|
3918
|
-
<button class="font-size-btn" id="appNameReset" title="Reset">Reset</button>
|
|
3919
|
-
</div>
|
|
3920
|
-
</div>
|
|
3921
|
-
<div class="settings-row">
|
|
3922
|
-
<div>
|
|
3923
|
-
<div class="settings-label">Toast Notifications</div>
|
|
3924
|
-
<div class="settings-hint">Show alerts for API errors and slow requests</div>
|
|
3925
|
-
</div>
|
|
3926
|
-
<label class="toggle-label" for="toastToggle">
|
|
3927
|
-
<input type="checkbox" id="toastToggle" class="toggle-input" ${getToastsEnabled() ? 'checked' : ''} />
|
|
3928
|
-
<span class="toggle-slider"></span>
|
|
3929
|
-
</label>
|
|
3930
|
-
</div>
|
|
3931
|
-
</div>
|
|
3932
|
-
<div class="settings-section">
|
|
3933
|
-
<div class="settings-section-title">Connection</div>
|
|
3934
|
-
<div class="settings-row">
|
|
3935
|
-
<div>
|
|
3936
|
-
<div class="settings-label">Bridge Ports</div>
|
|
3937
|
-
<div class="settings-hint">Redux :9090 · Storage :9091 · Network :9092</div>
|
|
3938
|
-
</div>
|
|
3939
|
-
</div>
|
|
3940
|
-
<div class="settings-row">
|
|
3941
|
-
<div>
|
|
3942
|
-
<div class="settings-label">Metro Port</div>
|
|
3943
|
-
</div>
|
|
3944
|
-
<input id="metroPortInput" type="number" class="net-search-input" style="width:70px;text-align:center" value="${getStoredMetroPort()}" />
|
|
3945
|
-
</div>
|
|
3946
|
-
</div>
|
|
3947
|
-
<div class="settings-section">
|
|
3948
|
-
<div class="settings-section-title">About</div>
|
|
3949
|
-
<div class="settings-about">
|
|
3950
|
-
<div class="about-name" id="aboutAppName">${getStoredAppName()}</div>
|
|
3951
|
-
<div class="about-version" id="aboutVersion">v${state._appVersion || '...'}</div>
|
|
3952
|
-
<div class="about-desc">Standalone macOS debugger for React Native.<br/>Supports Hermes, New Arch, and RN 0.74+.</div>
|
|
3953
|
-
<div class="about-links" style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap">
|
|
3954
|
-
<span class="about-link" id="linkGithub">GitHub</span>
|
|
3955
|
-
<span class="about-link" id="linkDocs">Docs</span>
|
|
3956
|
-
<span class="about-link" id="linkLinkedIn">LinkedIn</span>
|
|
3957
|
-
</div>
|
|
3958
|
-
<div style="margin-top:12px;text-align:center">
|
|
3959
|
-
<button class="support-btn" id="linkSupport" title="Support ReactoRadar development">☕ Support this project</button>
|
|
3960
|
-
</div>
|
|
3961
|
-
</div>
|
|
3962
|
-
</div>
|
|
3963
|
-
</div>
|
|
3964
|
-
<div class="settings-col-right">
|
|
3965
|
-
<div class="settings-section">
|
|
3966
|
-
<div class="settings-section-title">Panels</div>
|
|
3967
|
-
<div class="settings-hint" style="margin-bottom:8px">Show/hide tabs and drag to reorder. Disabled tabs save memory.</div>
|
|
3968
|
-
<div class="tab-visibility-grid" id="tabVisibilityGrid"></div>
|
|
3969
|
-
</div>
|
|
3970
|
-
<div class="settings-section">
|
|
3971
|
-
<div class="settings-section-title">Keyboard Shortcuts</div>
|
|
3972
|
-
<div class="settings-shortcut-grid">
|
|
3973
|
-
<span class="sc-key">⌘K</span><span class="sc-label">Clear All</span>
|
|
3974
|
-
<span class="sc-key">⌘D</span><span class="sc-label">JS Debugger</span>
|
|
3975
|
-
<span class="sc-key">⌘R</span><span class="sc-label">React DevTools</span>
|
|
3976
|
-
<span class="sc-key">⌘⇧T</span><span class="sc-label">Toggle Theme</span>
|
|
3977
|
-
<span class="sc-key">⌘F</span><span class="sc-label">Find</span>
|
|
3978
|
-
<span class="sc-key">⌘1–9</span><span class="sc-label">Switch Panels</span>
|
|
3979
|
-
<span class="sc-key">⌘+/−</span><span class="sc-label">Zoom</span>
|
|
3980
|
-
</div>
|
|
3981
|
-
</div>
|
|
3982
|
-
<div class="settings-section">
|
|
3983
|
-
<div class="settings-section-title">Quick Start</div>
|
|
3984
|
-
<div class="settings-hint" style="line-height:1.8;font-size:11px">
|
|
3985
|
-
<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/>
|
|
3986
|
-
<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/>
|
|
3987
|
-
<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/>
|
|
3988
|
-
<b style="color:var(--text)">4.</b> Console, Network, Redux auto-connect<br/>
|
|
3989
|
-
<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
|
|
3990
|
-
</div>
|
|
3991
|
-
</div>
|
|
3992
|
-
<div class="settings-section">
|
|
3993
|
-
<div class="settings-section-title">Version History</div>
|
|
3994
|
-
<div class="settings-hint" style="margin-bottom:4px">Roll back to a previous version if you notice issues.</div>
|
|
3995
|
-
<div class="settings-hint rollback-steps" id="rollbackSteps" style="margin-bottom:10px;line-height:1.8;font-size:10px">
|
|
3996
|
-
<b style="color:var(--text)">How to roll back:</b><br/>
|
|
3997
|
-
<span id="rollbackDmgSteps" style="display:none">
|
|
3998
|
-
<b style="color:var(--text)">1.</b> Click <b>Download</b> on the version you want<br/>
|
|
3999
|
-
<b style="color:var(--text)">2.</b> Open the downloaded <code style="color:var(--accent);background:var(--bg3);padding:1px 4px;border-radius:3px">.dmg</code> file<br/>
|
|
4000
|
-
<b style="color:var(--text)">3.</b> Drag the app to Applications (replace existing)<br/>
|
|
4001
|
-
<b style="color:var(--text)">4.</b> Relaunch ReactoRadar
|
|
4002
|
-
</span>
|
|
4003
|
-
<span id="rollbackNpmSteps" style="display:none">
|
|
4004
|
-
<b style="color:var(--text)">1.</b> Run <code style="color:var(--accent);background:var(--bg3);padding:1px 4px;border-radius:3px">npx reactoradar@<version></code> e.g. <code style="color:var(--accent);background:var(--bg3);padding:1px 4px;border-radius:3px">npx reactoradar@1.6.4</code><br/>
|
|
4005
|
-
<b style="color:var(--text)">2.</b> Or pin globally: <code style="color:var(--accent);background:var(--bg3);padding:1px 4px;border-radius:3px">npm i -g reactoradar@1.6.4</code><br/>
|
|
4006
|
-
<b style="color:var(--text)">3.</b> Run <code style="color:var(--accent);background:var(--bg3);padding:1px 4px;border-radius:3px">reactoradar</code> to launch
|
|
4007
|
-
</span>
|
|
4008
|
-
</div>
|
|
4009
|
-
<div id="versionHistoryList" class="version-history-list">
|
|
4010
|
-
<div style="color:var(--text-dim);font-size:11px;padding:12px;text-align:center">Loading versions...</div>
|
|
4011
|
-
</div>
|
|
4012
|
-
</div>
|
|
4013
|
-
</div>
|
|
4014
|
-
</div>
|
|
4015
|
-
</div>`;
|
|
4016
|
-
|
|
4017
|
-
// Build theme cards
|
|
4018
|
-
const themes = [
|
|
4019
|
-
{ id: 'dark', name: 'Dark', colors: ['#0d0e11','#4facff','#3dd68c','#ff5e72'] },
|
|
4020
|
-
{ id: 'light', name: 'Light', colors: ['#f5f6f8','#0969da','#1a7f37','#cf222e'] },
|
|
4021
|
-
{ id: 'monokai', name: 'Monokai', colors: ['#272822','#66d9ef','#a6e22e','#f92672'] },
|
|
4022
|
-
{ id: 'dracula', name: 'Dracula', colors: ['#282a36','#8be9fd','#50fa7b','#ff5555'] },
|
|
4023
|
-
{ id: 'solarized-dark', name: 'Solarized Dark', colors: ['#002b36','#268bd2','#859900','#dc322f'] },
|
|
4024
|
-
{ id: 'solarized-light', name: 'Solarized Light', colors: ['#fdf6e3','#268bd2','#859900','#dc322f'] },
|
|
4025
|
-
{ id: 'nord', name: 'Nord', colors: ['#2e3440','#88c0d0','#a3be8c','#bf616a'] },
|
|
4026
|
-
{ id: 'github-dark', name: 'GitHub Dark', colors: ['#0d1117','#58a6ff','#3fb950','#f85149'] },
|
|
4027
|
-
{ id: 'one-dark', name: 'One Dark', colors: ['#282c34','#61afef','#98c379','#e06c75'] },
|
|
4028
|
-
];
|
|
4029
|
-
// Tab visibility + drag reorder
|
|
4030
|
-
_buildTabVisGrid();
|
|
4031
|
-
|
|
4032
|
-
const grid = $('themeSwitcher');
|
|
4033
|
-
themes.forEach(t => {
|
|
4034
|
-
const btn = document.createElement('button');
|
|
4035
|
-
btn.className = 'theme-card' + (current === t.id ? ' active' : '');
|
|
4036
|
-
btn.dataset.theme = t.id;
|
|
4037
|
-
btn.innerHTML = '<div class="theme-preview" style="background:' + t.colors[0] + '">' +
|
|
4038
|
-
'<span style="background:' + t.colors[1] + '"></span>' +
|
|
4039
|
-
'<span style="background:' + t.colors[2] + '"></span>' +
|
|
4040
|
-
'<span style="background:' + t.colors[3] + '"></span>' +
|
|
4041
|
-
'</div><div class="theme-name">' + t.name + '</div>';
|
|
4042
|
-
grid.appendChild(btn);
|
|
4043
|
-
});
|
|
4044
|
-
|
|
4045
|
-
// Theme switcher
|
|
4046
|
-
$('themeSwitcher').addEventListener('click', (e) => {
|
|
4047
|
-
const btn = e.target.closest('.theme-card');
|
|
4048
|
-
if (!btn) return;
|
|
4049
|
-
const theme = btn.dataset.theme;
|
|
4050
|
-
document.querySelectorAll('#themeSwitcher .theme-card').forEach(b => b.classList.remove('active'));
|
|
4051
|
-
btn.classList.add('active');
|
|
4052
|
-
setStoredTheme(theme);
|
|
4053
|
-
applyTheme(theme);
|
|
4054
|
-
});
|
|
4055
|
-
|
|
4056
|
-
// About links
|
|
4057
|
-
$('linkGithub')?.addEventListener('click', () => {
|
|
4058
|
-
window.electronAPI?.openExternal('https://github.com/sharanagouda/reactoradar');
|
|
4059
|
-
});
|
|
4060
|
-
$('linkDocs')?.addEventListener('click', () => {
|
|
4061
|
-
window.electronAPI?.openExternal('https://github.com/sharanagouda/reactoradar#readme');
|
|
4062
|
-
});
|
|
4063
|
-
$('linkLinkedIn')?.addEventListener('click', () => {
|
|
4064
|
-
window.electronAPI?.openExternal('https://www.linkedin.com/in/sharanagoudamk/');
|
|
4065
|
-
});
|
|
4066
|
-
$('linkSupport')?.addEventListener('click', () => {
|
|
4067
|
-
window.electronAPI?.openExternal('https://razorpay.me/@reactoradar');
|
|
4068
|
-
});
|
|
4069
|
-
|
|
4070
|
-
// App name
|
|
4071
|
-
$('appNameInput').addEventListener('change', (e) => {
|
|
4072
|
-
const name = e.target.value.trim() || 'ReactoRadar';
|
|
4073
|
-
setStoredAppName(name);
|
|
4074
|
-
applyAppName(name);
|
|
4075
|
-
});
|
|
4076
|
-
$('appNameReset').addEventListener('click', () => {
|
|
4077
|
-
setStoredAppName('ReactoRadar');
|
|
4078
|
-
$('appNameInput').value = 'ReactoRadar';
|
|
4079
|
-
applyAppName('ReactoRadar');
|
|
4080
|
-
});
|
|
4081
|
-
|
|
4082
|
-
// Metro Port
|
|
4083
|
-
$('metroPortInput')?.addEventListener('change', (e) => {
|
|
4084
|
-
let port = parseInt(e.target.value.trim());
|
|
4085
|
-
if (isNaN(port) || port < 1024 || port > 65535) port = 8081;
|
|
4086
|
-
e.target.value = port;
|
|
4087
|
-
setStoredMetroPort(port);
|
|
4088
|
-
window.electronAPI?.setMetroPort(port);
|
|
4089
|
-
});
|
|
4090
|
-
|
|
4091
|
-
// Font size controls
|
|
4092
|
-
$('fontSizeDown').addEventListener('click', () => {
|
|
4093
|
-
let size = getStoredFontSize();
|
|
4094
|
-
size = Math.max(8, size - 1);
|
|
4095
|
-
setStoredFontSize(size);
|
|
4096
|
-
applyFontSize(size);
|
|
4097
|
-
});
|
|
4098
|
-
$('fontSizeUp').addEventListener('click', () => {
|
|
4099
|
-
let size = getStoredFontSize();
|
|
4100
|
-
size = Math.min(20, size + 1);
|
|
4101
|
-
setStoredFontSize(size);
|
|
4102
|
-
applyFontSize(size);
|
|
4103
|
-
});
|
|
4104
|
-
|
|
4105
|
-
// Font family
|
|
4106
|
-
$('fontFamilySelect')?.addEventListener('change', (e) => {
|
|
4107
|
-
const family = e.target.value;
|
|
4108
|
-
setStoredFontFamily(family);
|
|
4109
|
-
applyFontFamily(family);
|
|
4110
|
-
});
|
|
4111
|
-
|
|
4112
|
-
// Toast toggle
|
|
4113
|
-
$('toastToggle')?.addEventListener('change', (e) => {
|
|
4114
|
-
setToastsEnabled(e.target.checked);
|
|
4115
|
-
});
|
|
4116
|
-
|
|
4117
|
-
// Apply update banner if update info arrived before settings panel was created
|
|
4118
|
-
_applyUpdateBanner();
|
|
4119
|
-
|
|
4120
|
-
// Fetch and render version history for rollback
|
|
4121
|
-
_loadVersionHistory();
|
|
4122
|
-
}
|
|
4123
|
-
|
|
4124
|
-
function _loadVersionHistory() {
|
|
4125
|
-
const container = $('versionHistoryList');
|
|
4126
|
-
if (!container) return;
|
|
4127
|
-
if (!window.electronAPI || typeof window.electronAPI.fetchReleases !== 'function') {
|
|
4128
|
-
container.innerHTML = '<div style="color:var(--text-dim);font-size:11px;padding:12px;text-align:center">Version history not available.</div>';
|
|
4129
|
-
return;
|
|
4130
|
-
}
|
|
4131
|
-
|
|
4132
|
-
// Show appropriate rollback steps based on install type
|
|
4133
|
-
const isPackaged = !!state._isPackaged;
|
|
4134
|
-
const dmgSteps = $('rollbackDmgSteps');
|
|
4135
|
-
const npmSteps = $('rollbackNpmSteps');
|
|
4136
|
-
if (dmgSteps) dmgSteps.style.display = isPackaged ? '' : 'none';
|
|
4137
|
-
if (npmSteps) npmSteps.style.display = isPackaged ? 'none' : '';
|
|
4138
|
-
|
|
4139
|
-
window.electronAPI.fetchReleases().then(releases => {
|
|
4140
|
-
if (!Array.isArray(releases) || releases.length === 0) {
|
|
4141
|
-
container.innerHTML = '<div style="color:var(--text-dim);font-size:11px;padding:12px;text-align:center">Could not load versions.</div>';
|
|
4142
|
-
return;
|
|
4143
|
-
}
|
|
4144
|
-
|
|
4145
|
-
const currentVersion = state._appVersion || '';
|
|
4146
|
-
container.innerHTML = '';
|
|
4147
|
-
|
|
4148
|
-
releases.forEach(r => {
|
|
4149
|
-
if (!r || !r.version) return; // skip malformed entries
|
|
4150
|
-
|
|
4151
|
-
const isCurrent = r.version === currentVersion;
|
|
4152
|
-
const row = document.createElement('div');
|
|
4153
|
-
row.className = 'version-row' + (isCurrent ? ' version-current' : '');
|
|
4154
|
-
|
|
4155
|
-
// Safe date formatting
|
|
4156
|
-
let dateStr = '';
|
|
4157
|
-
if (r.date) {
|
|
4158
|
-
try {
|
|
4159
|
-
const d = new Date(r.date);
|
|
4160
|
-
if (!isNaN(d.getTime())) {
|
|
4161
|
-
dateStr = d.toLocaleDateString('en', { year: 'numeric', month: 'short', day: 'numeric' });
|
|
4162
|
-
}
|
|
4163
|
-
} catch { /* skip bad date */ }
|
|
4164
|
-
}
|
|
4165
|
-
|
|
4166
|
-
// Build action button based on install type
|
|
4167
|
-
let actionHtml = '';
|
|
4168
|
-
if (isCurrent) {
|
|
4169
|
-
actionHtml = '<span class="version-installed">Installed</span>';
|
|
4170
|
-
} else if (isPackaged) {
|
|
4171
|
-
actionHtml = '<button class="version-install-btn" title="Download .dmg for this version">Download</button>';
|
|
4172
|
-
} else {
|
|
4173
|
-
actionHtml = `<button class="version-npm-btn" title="Copy npm install command">npx @${esc(r.version)}</button>`;
|
|
4174
|
-
}
|
|
4175
|
-
|
|
4176
|
-
row.innerHTML = `
|
|
4177
|
-
<div class="version-info">
|
|
4178
|
-
<span class="version-tag">v${esc(r.version)}${r.prerelease ? ' <span class="version-pre">pre</span>' : ''}${isCurrent ? ' <span class="version-badge">current</span>' : ''}</span>
|
|
4179
|
-
<span class="version-date">${esc(dateStr)}</span>
|
|
4180
|
-
</div>
|
|
4181
|
-
<div class="version-actions">
|
|
4182
|
-
${actionHtml}
|
|
4183
|
-
<button class="version-notes-btn" title="View release notes">Notes</button>
|
|
4184
|
-
</div>`;
|
|
4185
|
-
|
|
4186
|
-
// DMG download button — opens the .dmg asset or release page
|
|
4187
|
-
const installBtn = row.querySelector('.version-install-btn');
|
|
4188
|
-
if (installBtn) {
|
|
4189
|
-
installBtn.addEventListener('click', () => {
|
|
4190
|
-
const url = r.dmgUrl || r.htmlUrl || '';
|
|
4191
|
-
if (url) {
|
|
4192
|
-
window.electronAPI.openExternal(url);
|
|
4193
|
-
}
|
|
4194
|
-
});
|
|
4195
|
-
}
|
|
4196
|
-
|
|
4197
|
-
// NPM copy button — copies the npx command to clipboard
|
|
4198
|
-
const npmBtn = row.querySelector('.version-npm-btn');
|
|
4199
|
-
if (npmBtn) {
|
|
4200
|
-
npmBtn.addEventListener('click', () => {
|
|
4201
|
-
const cmd = `npx reactoradar@${r.version}`;
|
|
4202
|
-
navigator.clipboard.writeText(cmd).then(() => {
|
|
4203
|
-
const orig = npmBtn.textContent;
|
|
4204
|
-
npmBtn.textContent = 'Copied!';
|
|
4205
|
-
npmBtn.style.color = 'var(--green)';
|
|
4206
|
-
setTimeout(() => { npmBtn.textContent = orig; npmBtn.style.color = ''; }, 2000);
|
|
4207
|
-
}).catch(() => {});
|
|
4208
|
-
});
|
|
4209
|
-
}
|
|
4210
|
-
|
|
4211
|
-
// Notes button — show changelog in modal
|
|
4212
|
-
const notesBtn = row.querySelector('.version-notes-btn');
|
|
4213
|
-
if (notesBtn) {
|
|
4214
|
-
notesBtn.addEventListener('click', () => {
|
|
4215
|
-
if (r.version && typeof _showChangelog === 'function') {
|
|
4216
|
-
_showChangelog(r.version);
|
|
4217
|
-
}
|
|
4218
|
-
});
|
|
4219
|
-
}
|
|
4220
|
-
|
|
4221
|
-
container.appendChild(row);
|
|
4222
|
-
});
|
|
4223
|
-
|
|
4224
|
-
// If no rows were rendered (all entries were malformed)
|
|
4225
|
-
if (container.children.length === 0) {
|
|
4226
|
-
container.innerHTML = '<div style="color:var(--text-dim);font-size:11px;padding:12px;text-align:center">No versions found.</div>';
|
|
4227
|
-
}
|
|
4228
|
-
}).catch(() => {
|
|
4229
|
-
if (container) {
|
|
4230
|
-
container.innerHTML = '<div style="color:var(--text-dim);font-size:11px;padding:12px;text-align:center">Could not load versions. Check your internet connection.</div>';
|
|
4231
|
-
}
|
|
4232
|
-
});
|
|
4233
|
-
}
|
|
4234
|
-
|
|
4235
|
-
// ─── Memory Monitor ──────────────────────────────────────────────────────────
|
|
4236
|
-
// Check memory usage periodically and warn user before it causes blank screen
|
|
4237
|
-
let _memoryWarningShown = false;
|
|
4238
|
-
setInterval(() => {
|
|
4239
|
-
if (!window.performance || !performance.memory) return;
|
|
4240
|
-
const used = performance.memory.usedJSHeapSize;
|
|
4241
|
-
const limit = performance.memory.jsHeapSizeLimit;
|
|
4242
|
-
const pct = used / limit;
|
|
4243
|
-
// Warn at 70% usage
|
|
4244
|
-
if (pct > 0.7 && !_memoryWarningShown) {
|
|
4245
|
-
_memoryWarningShown = true;
|
|
4246
|
-
const banner = document.createElement('div');
|
|
4247
|
-
banner.id = 'memoryWarning';
|
|
4248
|
-
banner.className = 'memory-warning';
|
|
4249
|
-
const usedMB = Math.round(used / 1024 / 1024);
|
|
4250
|
-
banner.innerHTML = `<span>High memory usage (${usedMB}MB) — ReactoRadar may become unresponsive.</span>`
|
|
4251
|
-
+ `<button class="memory-warn-btn" id="memWarnClear">Clear All Data</button>`
|
|
4252
|
-
+ `<button class="memory-warn-btn" id="memWarnDismiss">Dismiss</button>`;
|
|
4253
|
-
document.body.prepend(banner);
|
|
4254
|
-
$('memWarnClear')?.addEventListener('click', () => {
|
|
4255
|
-
// Clear all panel data
|
|
4256
|
-
state.console.logs = []; _consolePending = [];
|
|
4257
|
-
_lastLogMsg = ''; _lastLogRow = null; _lastLogCount = 1;
|
|
4258
|
-
$('cBadge').textContent = '0'; renderConsole();
|
|
4259
|
-
state.network.requests = {}; state.network.order = []; state.network.selectedId = null;
|
|
4260
|
-
$('nBadge').textContent = '0'; renderNetwork();
|
|
4261
|
-
state.redux.actions = []; state.redux.states = []; state.redux.selected = -1;
|
|
4262
|
-
$('rBadge').textContent = '0'; renderRedux();
|
|
4263
|
-
banner.remove(); _memoryWarningShown = false;
|
|
4264
|
-
});
|
|
4265
|
-
$('memWarnDismiss')?.addEventListener('click', () => { banner.remove(); });
|
|
4266
|
-
}
|
|
4267
|
-
// Reset flag when memory drops
|
|
4268
|
-
if (pct < 0.5) _memoryWarningShown = false;
|
|
4269
|
-
}, 30000); // Check every 30 seconds
|
|
4270
|
-
|
|
4271
|
-
// Apply saved theme + font size + font family + app name on load
|
|
4272
|
-
applyTheme(getStoredTheme());
|
|
4273
|
-
applyFontSize(getStoredFontSize());
|
|
4274
|
-
applyFontFamily(getStoredFontFamily());
|
|
4275
|
-
applyAppName(getStoredAppName());
|
|
4276
|
-
applyTabVisibility();
|
|
4277
|
-
|
|
4278
|
-
// Send stored metro port to backend
|
|
4279
|
-
window.electronAPI?.setMetroPort(getStoredMetroPort());
|
|
4280
|
-
|
|
4281
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
4282
|
-
// SOURCES PANEL (placeholder — use JS Debugger button for breakpoints)
|
|
4283
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
4284
|
-
function initSourcesPanel() {
|
|
4285
|
-
const panel = $('panel-sources');
|
|
4286
|
-
panel.innerHTML = `
|
|
4287
|
-
<div class="panel-toolbar">
|
|
4288
|
-
<span class="panel-label">Sources</span>
|
|
4289
|
-
<div class="ml-auto" style="display:flex;gap:6px">
|
|
4290
|
-
<button class="tb-btn" id="btnOpenSourcesExt" title="Open in separate DevTools window">Breakpoints ↗</button>
|
|
4291
|
-
</div>
|
|
4292
|
-
</div>
|
|
4293
|
-
<div class="sources-layout">
|
|
4294
|
-
<div class="sources-sidebar" id="sourcesSidebar">
|
|
4295
|
-
<div class="panel-toolbar" style="height:32px">
|
|
4296
|
-
<input id="sourcesSearch" class="net-search-input" style="width:100%" placeholder="Search files..." />
|
|
4297
|
-
</div>
|
|
4298
|
-
<div class="scroll-area sources-file-list" id="sourcesFileList">
|
|
4299
|
-
<div class="empty-state" id="sourcesEmpty">
|
|
4300
|
-
<div class="icon" style="font-size:28px;opacity:.2"></></div>
|
|
4301
|
-
<div class="label">Waiting for Metro...</div>
|
|
4302
|
-
<div class="hint">Source files will load when Metro is running</div>
|
|
4303
|
-
</div>
|
|
4304
|
-
</div>
|
|
4305
|
-
</div>
|
|
4306
|
-
<div class="sources-editor" id="sourcesEditor">
|
|
4307
|
-
<div class="panel-toolbar" style="height:32px">
|
|
4308
|
-
<span id="sourcesFileName" style="font-size:10px;color:var(--accent)"></span>
|
|
4309
|
-
<span id="sourcesLineInfo" style="font-size:10px;color:var(--text-dim);margin-left:auto"></span>
|
|
4310
|
-
</div>
|
|
4311
|
-
<div class="scroll-area sources-code" id="sourcesCode">
|
|
4312
|
-
<span style="color:var(--text-dim);padding:20px;display:block">Select a file to view its source</span>
|
|
4313
|
-
</div>
|
|
4314
|
-
</div>
|
|
4315
|
-
</div>`;
|
|
4316
|
-
|
|
4317
|
-
// Open JS Debugger for breakpoints
|
|
4318
|
-
$('btnOpenSourcesExt').addEventListener('click', () => {
|
|
4319
|
-
window.electronAPI?.openCDPTarget(null);
|
|
4320
|
-
});
|
|
4321
|
-
|
|
4322
|
-
// Search filter for file tree
|
|
4323
|
-
$('sourcesSearch').addEventListener('input', (e) => {
|
|
4324
|
-
const term = e.target.value.toLowerCase().trim();
|
|
4325
|
-
document.querySelectorAll('#sourcesFileList .src-tree-file').forEach(row => {
|
|
4326
|
-
const filepath = row.dataset.file || '';
|
|
4327
|
-
const match = !term || filepath.toLowerCase().includes(term);
|
|
4328
|
-
row.style.display = match ? '' : 'none';
|
|
4329
|
-
});
|
|
4330
|
-
// Show/hide folder nodes based on whether they have visible children
|
|
4331
|
-
document.querySelectorAll('#sourcesFileList .src-tree-folder').forEach(folder => {
|
|
4332
|
-
const visibleFiles = folder.querySelectorAll('.src-tree-file:not([style*="display: none"])');
|
|
4333
|
-
folder.style.display = (!term || visibleFiles.length > 0) ? '' : 'none';
|
|
4334
|
-
// Auto-expand folders when searching
|
|
4335
|
-
if (term && visibleFiles.length > 0) {
|
|
4336
|
-
const children = folder.querySelector('.src-tree-children');
|
|
4337
|
-
const arrow = folder.querySelector('.src-tree-arrow');
|
|
4338
|
-
if (children) children.style.display = 'block';
|
|
4339
|
-
if (arrow) { arrow.textContent = '\u25BC'; arrow.classList.add('expanded'); }
|
|
4340
|
-
}
|
|
4341
|
-
});
|
|
4342
|
-
});
|
|
4343
|
-
|
|
4344
|
-
// Fetch the source map / bundle modules list from Metro
|
|
4345
|
-
fetchSourceFileList();
|
|
4346
|
-
}
|
|
4347
|
-
|
|
4348
|
-
async function fetchSourceFileList() {
|
|
4349
|
-
if (!window.electronAPI?.getSourceFileList) {
|
|
4350
|
-
console.log('[Sources] electronAPI.getSourceFileList not available, retrying...');
|
|
4351
|
-
setTimeout(fetchSourceFileList, 5000);
|
|
4352
|
-
return;
|
|
4353
|
-
}
|
|
4354
|
-
try {
|
|
4355
|
-
console.log('[Sources] Fetching file list from Metro...');
|
|
4356
|
-
const result = await window.electronAPI.getSourceFileList();
|
|
4357
|
-
console.log('[Sources] Got result:', result?.files?.length, 'files, root:', result?.root?.slice(-30));
|
|
4358
|
-
if (result?.files && result.files.length > 0) {
|
|
4359
|
-
state._sourcesRoot = result.root;
|
|
4360
|
-
// Limit to 500 files max to avoid DOM overload
|
|
4361
|
-
const files = result.files.length > 500 ? result.files.slice(0, 500) : result.files;
|
|
4362
|
-
renderSourceFileList(files);
|
|
4363
|
-
console.log('[Sources] Rendered', files.length, 'files');
|
|
4364
|
-
} else {
|
|
4365
|
-
console.log('[Sources] No files, retrying in 5s...');
|
|
4366
|
-
setTimeout(fetchSourceFileList, 5000);
|
|
4367
|
-
}
|
|
4368
|
-
} catch (e) {
|
|
4369
|
-
console.log('[Sources] Error:', e?.message || e);
|
|
4370
|
-
setTimeout(fetchSourceFileList, 5000);
|
|
4371
|
-
}
|
|
4372
|
-
}
|
|
4373
|
-
|
|
4374
|
-
function renderSourceFileList(files) {
|
|
4375
|
-
const list = $('sourcesFileList');
|
|
4376
|
-
const empty = $('sourcesEmpty');
|
|
4377
|
-
if (!list) return;
|
|
4378
|
-
if (!files.length) return;
|
|
4379
|
-
if (empty) empty.style.display = 'none';
|
|
4380
|
-
list.querySelectorAll('.src-tree-node').forEach(e => e.remove());
|
|
4381
|
-
|
|
4382
|
-
// Build folder tree from file paths
|
|
4383
|
-
const tree = {};
|
|
4384
|
-
files.forEach(filepath => {
|
|
4385
|
-
const parts = filepath.split('/').filter(Boolean);
|
|
4386
|
-
let node = tree;
|
|
4387
|
-
parts.forEach((part, i) => {
|
|
4388
|
-
if (i === parts.length - 1) {
|
|
4389
|
-
// File leaf
|
|
4390
|
-
node[part] = filepath; // string = file
|
|
4391
|
-
} else {
|
|
4392
|
-
// Folder
|
|
4393
|
-
if (!node[part] || typeof node[part] === 'string') node[part] = {};
|
|
4394
|
-
node = node[part];
|
|
4395
|
-
}
|
|
4396
|
-
});
|
|
4397
|
-
});
|
|
4398
|
-
|
|
4399
|
-
// Render tree recursively
|
|
4400
|
-
const frag = document.createDocumentFragment();
|
|
4401
|
-
|
|
4402
|
-
// Project folders first, node_modules last
|
|
4403
|
-
const topKeys = Object.keys(tree).sort((a, b) => {
|
|
4404
|
-
if (a === 'node_modules') return 1;
|
|
4405
|
-
if (b === 'node_modules') return -1;
|
|
4406
|
-
return a.localeCompare(b);
|
|
4407
|
-
});
|
|
4408
|
-
|
|
4409
|
-
topKeys.forEach(key => {
|
|
4410
|
-
frag.appendChild(buildSourceTreeNode(key, tree[key], 0));
|
|
4411
|
-
});
|
|
4412
|
-
list.appendChild(frag);
|
|
4413
|
-
}
|
|
4414
|
-
|
|
4415
|
-
function buildSourceTreeNode(name, value, depth) {
|
|
4416
|
-
if (typeof value === 'string') {
|
|
4417
|
-
// File leaf
|
|
4418
|
-
const row = document.createElement('div');
|
|
4419
|
-
row.className = 'src-tree-node src-tree-file';
|
|
4420
|
-
row.dataset.file = value;
|
|
4421
|
-
row.style.paddingLeft = (12 + depth * 16) + 'px';
|
|
4422
|
-
const isNM = value.includes('node_modules');
|
|
4423
|
-
const ext = name.split('.').pop();
|
|
4424
|
-
const iconColor = ext === 'tsx' || ext === 'ts' ? '#3178c6'
|
|
4425
|
-
: ext === 'jsx' || ext === 'js' ? '#f0db4f'
|
|
4426
|
-
: ext === 'json' ? '#a0a0a0'
|
|
4427
|
-
: ext === 'css' ? '#264de4'
|
|
4428
|
-
: 'var(--text-dim)';
|
|
4429
|
-
row.innerHTML = `<span class="src-file-icon" style="color:${iconColor}">●</span><span class="src-file-name" style="color:${isNM ? 'var(--text-dim)' : 'var(--text-bright)'}">${esc(name)}</span>`;
|
|
4430
|
-
row.addEventListener('click', () => {
|
|
4431
|
-
const fileList = $('sourcesFileList');
|
|
4432
|
-
fileList.querySelectorAll('.src-tree-file').forEach(el => el.classList.remove('selected'));
|
|
4433
|
-
row.classList.add('selected');
|
|
4434
|
-
loadSourceFile(value);
|
|
4435
|
-
});
|
|
4436
|
-
// Search filter support
|
|
4437
|
-
const searchInput = $('sourcesSearch');
|
|
4438
|
-
if (searchInput && searchInput.value) {
|
|
4439
|
-
const term = searchInput.value.toLowerCase();
|
|
4440
|
-
if (!name.toLowerCase().includes(term) && !value.toLowerCase().includes(term)) {
|
|
4441
|
-
row.style.display = 'none';
|
|
4442
|
-
}
|
|
4443
|
-
}
|
|
4444
|
-
return row;
|
|
4445
|
-
}
|
|
4446
|
-
|
|
4447
|
-
// Folder node
|
|
4448
|
-
const container = document.createElement('div');
|
|
4449
|
-
container.className = 'src-tree-node src-tree-folder';
|
|
4450
|
-
|
|
4451
|
-
const header = document.createElement('div');
|
|
4452
|
-
header.className = 'src-tree-folder-header';
|
|
4453
|
-
header.style.paddingLeft = (8 + depth * 16) + 'px';
|
|
4454
|
-
|
|
4455
|
-
const arrow = document.createElement('span');
|
|
4456
|
-
arrow.className = 'src-tree-arrow';
|
|
4457
|
-
arrow.textContent = '\u25B6';
|
|
4458
|
-
|
|
4459
|
-
const folderName = document.createElement('span');
|
|
4460
|
-
folderName.className = 'src-folder-name';
|
|
4461
|
-
const isNM = name === 'node_modules';
|
|
4462
|
-
folderName.style.color = isNM ? 'var(--text-dim)' : 'var(--text)';
|
|
4463
|
-
folderName.textContent = name;
|
|
4464
|
-
|
|
4465
|
-
header.appendChild(arrow);
|
|
4466
|
-
header.appendChild(folderName);
|
|
4467
|
-
container.appendChild(header);
|
|
4468
|
-
|
|
4469
|
-
const children = document.createElement('div');
|
|
4470
|
-
children.className = 'src-tree-children';
|
|
4471
|
-
// Start all folders collapsed
|
|
4472
|
-
children.style.display = 'none';
|
|
4473
|
-
|
|
4474
|
-
// Sort: folders first, then files
|
|
4475
|
-
const entries = Object.entries(value).sort((a, b) => {
|
|
4476
|
-
const aIsFolder = typeof a[1] === 'object';
|
|
4477
|
-
const bIsFolder = typeof b[1] === 'object';
|
|
4478
|
-
if (aIsFolder !== bIsFolder) return aIsFolder ? -1 : 1;
|
|
4479
|
-
return a[0].localeCompare(b[0]);
|
|
4480
|
-
});
|
|
4481
|
-
|
|
4482
|
-
let populated = false;
|
|
4483
|
-
function populate() {
|
|
4484
|
-
if (populated) return;
|
|
4485
|
-
populated = true;
|
|
4486
|
-
entries.forEach(([childName, childValue]) => {
|
|
4487
|
-
children.appendChild(buildSourceTreeNode(childName, childValue, depth + 1));
|
|
4488
|
-
});
|
|
4489
|
-
}
|
|
4490
|
-
|
|
4491
|
-
// Folders start collapsed — populate lazily on first expand
|
|
4492
|
-
header.addEventListener('click', () => {
|
|
4493
|
-
const isOpen = children.style.display !== 'none';
|
|
4494
|
-
if (!isOpen) {
|
|
4495
|
-
populate();
|
|
4496
|
-
children.style.display = 'block';
|
|
4497
|
-
arrow.textContent = '\u25BC';
|
|
4498
|
-
arrow.classList.add('expanded');
|
|
4499
|
-
} else {
|
|
4500
|
-
children.style.display = 'none';
|
|
4501
|
-
arrow.textContent = '\u25B6';
|
|
4502
|
-
arrow.classList.remove('expanded');
|
|
4503
|
-
}
|
|
4504
|
-
});
|
|
4505
|
-
|
|
4506
|
-
container.appendChild(children);
|
|
4507
|
-
return container;
|
|
4508
|
-
}
|
|
4509
|
-
|
|
4510
|
-
async function loadSourceFile(filepath) {
|
|
4511
|
-
const codeEl = $('sourcesCode');
|
|
4512
|
-
const nameEl = $('sourcesFileName');
|
|
4513
|
-
const lineEl = $('sourcesLineInfo');
|
|
4514
|
-
if (!codeEl) return;
|
|
4515
|
-
if (nameEl) nameEl.textContent = filepath.split('/').pop();
|
|
4516
|
-
if (lineEl) lineEl.textContent = filepath;
|
|
4517
|
-
codeEl.innerHTML = '<span style="color:var(--text-dim)">Loading...</span>';
|
|
4518
|
-
|
|
4519
|
-
let source = null;
|
|
4520
|
-
const root = state._sourcesRoot || '';
|
|
4521
|
-
const fullPath = root ? `${root}/${filepath}` : filepath;
|
|
4522
|
-
|
|
4523
|
-
// Strategy 1: Read from disk via IPC (most reliable)
|
|
4524
|
-
if (window.electronAPI?.readSourceFile) {
|
|
4525
|
-
source = await window.electronAPI.readSourceFile(fullPath);
|
|
4526
|
-
}
|
|
4527
|
-
|
|
4528
|
-
// Strategy 2: Fetch from Metro
|
|
4529
|
-
if (!source) {
|
|
4530
|
-
try {
|
|
4531
|
-
const port = getStoredMetroPort();
|
|
4532
|
-
const resp = await fetch(`http://localhost:${port}/${filepath}?platform=ios&dev=true`);
|
|
4533
|
-
if (resp.ok) source = await resp.text();
|
|
4534
|
-
} catch {}
|
|
4535
|
-
}
|
|
4536
|
-
|
|
4537
|
-
if (!source) {
|
|
4538
|
-
codeEl.innerHTML = `<span style="color:var(--text-dim);padding:20px;display:block">Could not load: ${esc(filepath)}</span>`;
|
|
4539
|
-
return;
|
|
4540
|
-
}
|
|
4541
|
-
|
|
4542
|
-
// Render with line numbers
|
|
4543
|
-
const lines = source.split('\n');
|
|
4544
|
-
if (lineEl) lineEl.textContent = `${filepath} (${lines.length} lines)`;
|
|
4545
|
-
codeEl.innerHTML = '';
|
|
4546
|
-
const pre = document.createElement('pre');
|
|
4547
|
-
pre.className = 'source-pre';
|
|
4548
|
-
lines.forEach((line, i) => {
|
|
4549
|
-
const lineDiv = document.createElement('div');
|
|
4550
|
-
lineDiv.className = 'source-line';
|
|
4551
|
-
lineDiv.innerHTML = `<span class="source-line-num">${i + 1}</span><span class="source-line-code">${syntaxHighlight(esc(line))}</span>`;
|
|
4552
|
-
pre.appendChild(lineDiv);
|
|
4553
|
-
});
|
|
4554
|
-
codeEl.appendChild(pre);
|
|
4555
|
-
}
|
|
4556
|
-
|
|
4557
|
-
// Called from cdp-targets IPC handler (no longer opens external window)
|
|
4558
|
-
|
|
4559
|
-
// Called from cdp-targets IPC handler (shared, no duplicate registration)
|
|
4560
|
-
// Sources panel uses Metro source map for file tree — CDP targets are only
|
|
4561
|
-
// used for the "Breakpoints" button, not for the file list.
|
|
4562
|
-
function updateSourcesPanel(targets) {
|
|
4563
|
-
// No-op: file list is populated by fetchSourceFileList from Metro source map
|
|
4564
|
-
}
|
|
4565
|
-
|
|
4566
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
4567
|
-
// PERFORMANCE PANEL — FPS, render timing, JS thread
|
|
4568
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
4569
|
-
const perfState = { fps: [], jsThread: [], uiThread: [], recording: false, data: [] };
|
|
4570
|
-
|
|
4571
|
-
function initPerformancePanel() {
|
|
4572
|
-
const panel = $('panel-performance');
|
|
4573
|
-
panel.innerHTML = `
|
|
4574
|
-
<div class="panel-toolbar">
|
|
4575
|
-
<span class="panel-label">Performance</span>
|
|
4576
|
-
<div class="ml-auto" style="display:flex;gap:6px">
|
|
4577
|
-
<button class="tb-btn" id="btnPerfRecord">Record</button>
|
|
4578
|
-
<button class="tb-btn" id="btnPerfClear">Clear</button>
|
|
4579
|
-
</div>
|
|
4580
|
-
</div>
|
|
4581
|
-
<div class="perf-layout">
|
|
4582
|
-
<div class="perf-meters">
|
|
4583
|
-
<div class="perf-meter">
|
|
4584
|
-
<div class="perf-meter-label">FPS</div>
|
|
4585
|
-
<div class="perf-meter-value" id="perfFPS">—</div>
|
|
4586
|
-
<canvas class="perf-canvas" id="perfFPSCanvas" width="200" height="60"></canvas>
|
|
4587
|
-
</div>
|
|
4588
|
-
<div class="perf-meter">
|
|
4589
|
-
<div class="perf-meter-label">JS Thread</div>
|
|
4590
|
-
<div class="perf-meter-value" id="perfJS">—</div>
|
|
4591
|
-
<canvas class="perf-canvas" id="perfJSCanvas" width="200" height="60"></canvas>
|
|
4592
|
-
</div>
|
|
4593
|
-
<div class="perf-meter">
|
|
4594
|
-
<div class="perf-meter-label">UI Thread</div>
|
|
4595
|
-
<div class="perf-meter-value" id="perfUI">—</div>
|
|
4596
|
-
<canvas class="perf-canvas" id="perfUICanvas" width="200" height="60"></canvas>
|
|
4597
|
-
</div>
|
|
4598
|
-
</div>
|
|
4599
|
-
<div class="scroll-area perf-timeline" id="perfTimeline">
|
|
4600
|
-
<div class="empty-state" id="perfEmpty">
|
|
4601
|
-
<div class="icon" style="font-size:28px;opacity:.2">📊</div>
|
|
4602
|
-
<div class="label">No performance data</div>
|
|
4603
|
-
<div class="hint">Click "Record" to start capturing performance metrics</div>
|
|
4604
|
-
<div class="hint">The SDK sends FPS + thread usage automatically when connected</div>
|
|
4605
|
-
</div>
|
|
4606
|
-
</div>
|
|
4607
|
-
</div>`;
|
|
4608
|
-
|
|
4609
|
-
$('btnPerfRecord').addEventListener('click', () => {
|
|
4610
|
-
perfState.recording = !perfState.recording;
|
|
4611
|
-
$('btnPerfRecord').textContent = perfState.recording ? 'Stop' : 'Record';
|
|
4612
|
-
$('btnPerfRecord').classList.toggle('primary', perfState.recording);
|
|
4613
|
-
if (perfState.recording) {
|
|
4614
|
-
// Tell SDK to start sending perf data
|
|
4615
|
-
window.electronAPI?.setNetworkCapture(true); // reuse channel
|
|
4616
|
-
}
|
|
4617
|
-
});
|
|
4618
|
-
|
|
4619
|
-
$('btnPerfClear').addEventListener('click', () => {
|
|
4620
|
-
perfState.fps = [];
|
|
4621
|
-
perfState.jsThread = [];
|
|
4622
|
-
perfState.uiThread = [];
|
|
4623
|
-
perfState.data = [];
|
|
4624
|
-
$('perfFPS').textContent = '—';
|
|
4625
|
-
$('perfJS').textContent = '—';
|
|
4626
|
-
$('perfUI').textContent = '—';
|
|
4627
|
-
clearPerfCanvas('perfFPSCanvas');
|
|
4628
|
-
clearPerfCanvas('perfJSCanvas');
|
|
4629
|
-
clearPerfCanvas('perfUICanvas');
|
|
4630
|
-
});
|
|
4631
|
-
}
|
|
4632
|
-
|
|
4633
|
-
function clearPerfCanvas(id) {
|
|
4634
|
-
const canvas = $(id);
|
|
4635
|
-
if (!canvas) return;
|
|
4636
|
-
const ctx = canvas.getContext('2d');
|
|
4637
|
-
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
4638
|
-
}
|
|
4639
|
-
|
|
4640
|
-
function drawPerfGraph(canvasId, data, maxVal, color) {
|
|
4641
|
-
const canvas = $(canvasId);
|
|
4642
|
-
if (!canvas || !data.length) return;
|
|
4643
|
-
const ctx = canvas.getContext('2d');
|
|
4644
|
-
const w = canvas.width, h = canvas.height;
|
|
4645
|
-
ctx.clearRect(0, 0, w, h);
|
|
4646
|
-
|
|
4647
|
-
// Grid lines
|
|
4648
|
-
ctx.strokeStyle = 'rgba(255,255,255,0.05)';
|
|
4649
|
-
ctx.lineWidth = 1;
|
|
4650
|
-
for (let y = 0; y < h; y += h/4) {
|
|
4651
|
-
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke();
|
|
4652
|
-
}
|
|
4653
|
-
|
|
4654
|
-
// Data line
|
|
4655
|
-
ctx.strokeStyle = color;
|
|
4656
|
-
ctx.lineWidth = 1.5;
|
|
4657
|
-
ctx.beginPath();
|
|
4658
|
-
const step = w / Math.max(data.length - 1, 1);
|
|
4659
|
-
data.forEach((v, i) => {
|
|
4660
|
-
const x = i * step;
|
|
4661
|
-
const y = h - (v / maxVal) * h;
|
|
4662
|
-
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
|
|
4663
|
-
});
|
|
4664
|
-
ctx.stroke();
|
|
4665
|
-
|
|
4666
|
-
// Fill under
|
|
4667
|
-
ctx.lineTo(w, h);
|
|
4668
|
-
ctx.lineTo(0, h);
|
|
4669
|
-
ctx.closePath();
|
|
4670
|
-
ctx.fillStyle = color.replace('1)', '0.1)');
|
|
4671
|
-
ctx.fill();
|
|
4672
|
-
}
|
|
4673
|
-
|
|
4674
|
-
// Handle performance events from SDK (always updates meters, graphs only when recording)
|
|
4675
|
-
function handlePerfEvent(event) {
|
|
4676
|
-
if (!isTabEnabled('performance') && !isTabEnabled('memory')) return;
|
|
4677
|
-
if (event.fps != null) {
|
|
4678
|
-
perfState.fps.push(event.fps);
|
|
4679
|
-
if (perfState.fps.length > 100) perfState.fps.shift();
|
|
4680
|
-
const fpsEl = $('perfFPS');
|
|
4681
|
-
if (fpsEl) fpsEl.textContent = event.fps + ' fps';
|
|
4682
|
-
drawPerfGraph('perfFPSCanvas', perfState.fps, 60, 'rgba(61,214,140,1)');
|
|
4683
|
-
}
|
|
4684
|
-
if (event.jsThread != null) {
|
|
4685
|
-
perfState.jsThread.push(event.jsThread);
|
|
4686
|
-
if (perfState.jsThread.length > 100) perfState.jsThread.shift();
|
|
4687
|
-
const jsEl = $('perfJS');
|
|
4688
|
-
if (jsEl) jsEl.textContent = event.jsThread.toFixed(1) + 'ms';
|
|
4689
|
-
drawPerfGraph('perfJSCanvas', perfState.jsThread, 32, 'rgba(79,172,255,1)');
|
|
4690
|
-
}
|
|
4691
|
-
if (event.uiThread != null) {
|
|
4692
|
-
perfState.uiThread.push(event.uiThread);
|
|
4693
|
-
if (perfState.uiThread.length > 100) perfState.uiThread.shift();
|
|
4694
|
-
const uiEl = $('perfUI');
|
|
4695
|
-
if (uiEl) uiEl.textContent = event.uiThread.toFixed(1) + 'ms';
|
|
4696
|
-
drawPerfGraph('perfUICanvas', perfState.uiThread, 32, 'rgba(155,127,255,1)');
|
|
4697
|
-
}
|
|
4698
|
-
}
|
|
4699
|
-
|
|
4700
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
4701
|
-
// MEMORY PANEL — Heap snapshot summary via Hermes CDP
|
|
4702
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
4703
|
-
function initMemoryPanel() {
|
|
4704
|
-
const panel = $('panel-memory');
|
|
4705
|
-
panel.innerHTML = `
|
|
4706
|
-
<div class="panel-toolbar">
|
|
4707
|
-
<span class="panel-label">Memory</span>
|
|
4708
|
-
<div class="ml-auto" style="display:flex;gap:6px">
|
|
4709
|
-
<button class="tb-btn primary" id="btnHeapSnapshot">Take Heap Snapshot</button>
|
|
4710
|
-
</div>
|
|
4711
|
-
</div>
|
|
4712
|
-
<div class="memory-layout">
|
|
4713
|
-
<div class="perf-meters" style="padding:14px">
|
|
4714
|
-
<div class="perf-meter">
|
|
4715
|
-
<div class="perf-meter-label">JS Heap Used</div>
|
|
4716
|
-
<div class="perf-meter-value" id="memHeapUsed">—</div>
|
|
4717
|
-
</div>
|
|
4718
|
-
<div class="perf-meter">
|
|
4719
|
-
<div class="perf-meter-label">JS Heap Total</div>
|
|
4720
|
-
<div class="perf-meter-value" id="memHeapTotal">—</div>
|
|
4721
|
-
</div>
|
|
4722
|
-
<div class="perf-meter">
|
|
4723
|
-
<div class="perf-meter-label">Native Memory</div>
|
|
4724
|
-
<div class="perf-meter-value" id="memNative">—</div>
|
|
4725
|
-
</div>
|
|
4726
|
-
</div>
|
|
4727
|
-
<div class="scroll-area" id="memoryContent">
|
|
4728
|
-
<div class="empty-state" id="memoryEmpty">
|
|
4729
|
-
<div class="icon" style="font-size:28px;opacity:.2">🧠</div>
|
|
4730
|
-
<div class="label">No memory data</div>
|
|
4731
|
-
<div class="hint">Click "Take Heap Snapshot" to capture memory usage</div>
|
|
4732
|
-
<div class="hint">Requires Hermes CDP connection (press Cmd+D first)</div>
|
|
4733
|
-
</div>
|
|
4734
|
-
</div>
|
|
4735
|
-
</div>`;
|
|
4736
|
-
|
|
4737
|
-
$('btnHeapSnapshot').addEventListener('click', () => {
|
|
4738
|
-
// Request heap snapshot via CDP - this opens the DevTools window
|
|
4739
|
-
// which has built-in Memory profiler
|
|
4740
|
-
window.electronAPI?.openCDPTarget(null);
|
|
4741
|
-
});
|
|
4742
|
-
}
|
|
4743
|
-
|
|
4744
|
-
// Handle memory events from SDK
|
|
4745
|
-
function handleMemoryEvent(event) {
|
|
4746
|
-
const hu = $('memHeapUsed'), ht = $('memHeapTotal'), mn = $('memNative');
|
|
4747
|
-
if (event.heapUsed != null && hu) hu.textContent = formatSize(event.heapUsed);
|
|
4748
|
-
if (event.heapTotal != null && ht) ht.textContent = formatSize(event.heapTotal);
|
|
4749
|
-
if (event.native != null && mn) mn.textContent = formatSize(event.native);
|
|
4750
|
-
}
|
|
4751
|
-
|
|
4752
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
4753
|
-
// INIT
|
|
4754
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
4755
|
-
initConsolePanel();
|
|
4756
|
-
initNetworkPanel();
|
|
4757
|
-
initGA4Panel();
|
|
4758
|
-
initPerformancePanel();
|
|
4759
|
-
initMemoryPanel();
|
|
4760
|
-
initReduxPanel();
|
|
4761
|
-
initStoragePanel();
|
|
4762
|
-
initReactPanel();
|
|
4763
|
-
initNativeLogsPanel();
|
|
4764
|
-
initSettingsPanel();
|