reactoradar 1.6.6 → 1.6.8
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 +18 -19
- package/init.js +199 -0
- package/package.json +4 -2
- package/panels/console.js +789 -0
- package/panels/ga4.js +331 -0
- package/panels/native.js +260 -0
- package/panels/network.js +972 -0
- package/panels/performance.js +188 -0
- package/panels/react.js +23 -0
- package/panels/redux.js +441 -0
- package/panels/settings.js +791 -0
- package/panels/sources.js +289 -0
- package/panels/storage.js +191 -0
- package/sdk/RNDebugSDK.js +100 -38
package/panels/ga4.js
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
// ─── GA4 Events Panel ──────────────────────────────────────────────────────
|
|
2
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
3
|
+
// GA4 EVENT INSPECTOR
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
const ga4State = { events: [], selected: -1, searchFilter: '', sortDir: 'desc' };
|
|
6
|
+
|
|
7
|
+
function initGA4Panel() {
|
|
8
|
+
const panel = $('panel-ga4');
|
|
9
|
+
if (!panel) return;
|
|
10
|
+
panel.innerHTML = `
|
|
11
|
+
<div class="panel-toolbar">
|
|
12
|
+
<span class="panel-label">GA4 Events</span>
|
|
13
|
+
<span class="badge" id="ga4Badge">0</span>
|
|
14
|
+
<input id="ga4Search" class="net-search-input" style="margin-left:12px" placeholder="Filter events..." />
|
|
15
|
+
<div class="ml-auto" style="display:flex;align-items:center;gap:6px">
|
|
16
|
+
<label class="toggle-label" for="ga4ColorToggle" style="font-size:10px;gap:4px">
|
|
17
|
+
<span style="color:var(--text-dim)">Colors</span>
|
|
18
|
+
<input type="checkbox" id="ga4ColorToggle" class="toggle-input" ${getGA4ColorsEnabled() ? 'checked' : ''} />
|
|
19
|
+
<span class="toggle-slider"></span>
|
|
20
|
+
</label>
|
|
21
|
+
<button class="panel-clear-btn" id="ga4Clear" title="Clear GA4 events">Clear</button>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="ga4-layout">
|
|
25
|
+
<div class="ga4-list-pane">
|
|
26
|
+
<div class="ga4-list-header">
|
|
27
|
+
<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>
|
|
28
|
+
<span class="ga4-hcell" style="flex:1">Event</span>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="scroll-area" id="ga4List">
|
|
31
|
+
<div class="empty-state" id="ga4Empty">
|
|
32
|
+
<div class="icon" style="font-size:28px;opacity:.2">📊</div>
|
|
33
|
+
<div class="label">No GA4 events yet</div>
|
|
34
|
+
<div class="hint">Events from @react-native-firebase/analytics will appear here</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="ga4-resize-handle" id="ga4ResizeHandle"></div>
|
|
39
|
+
<div class="ga4-detail-pane" id="ga4DetailPane">
|
|
40
|
+
<div class="ga4-detail-header">EVENT DETAIL</div>
|
|
41
|
+
<div class="scroll-area ga4-detail-content" id="ga4Detail">
|
|
42
|
+
<span style="color:var(--text-dim);padding:16px;display:block">Click an event to inspect</span>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="ga4-summary" id="ga4Summary">
|
|
47
|
+
<span class="ga4-summary-label">Total: 0</span>
|
|
48
|
+
</div>`;
|
|
49
|
+
|
|
50
|
+
$('ga4Search').addEventListener('input', (e) => {
|
|
51
|
+
ga4State.searchFilter = e.target.value.toLowerCase().trim();
|
|
52
|
+
renderGA4List();
|
|
53
|
+
renderGA4Summary(); // update active chip highlight
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
$('ga4ColorToggle')?.addEventListener('change', (e) => {
|
|
57
|
+
setGA4ColorsEnabled(e.target.checked);
|
|
58
|
+
renderGA4List();
|
|
59
|
+
renderGA4Summary();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
$('ga4Clear').addEventListener('click', () => {
|
|
63
|
+
ga4State.events = [];
|
|
64
|
+
ga4State.selected = -1;
|
|
65
|
+
ga4State.searchFilter = '';
|
|
66
|
+
const search = $('ga4Search');
|
|
67
|
+
if (search) search.value = '';
|
|
68
|
+
$('ga4Badge').textContent = '0';
|
|
69
|
+
renderGA4List();
|
|
70
|
+
renderGA4Summary();
|
|
71
|
+
// Clear detail pane
|
|
72
|
+
const detail = $('ga4Detail');
|
|
73
|
+
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>';
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
$('ga4SortBtn').addEventListener('click', () => {
|
|
77
|
+
ga4State.sortDir = ga4State.sortDir === 'desc' ? 'asc' : 'desc';
|
|
78
|
+
$('ga4SortIcon').textContent = ga4State.sortDir === 'desc' ? '\u25BC' : '\u25B2';
|
|
79
|
+
renderGA4List();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Resizable divider between list and detail
|
|
83
|
+
const resizeHandle = $('ga4ResizeHandle');
|
|
84
|
+
const detailPane = $('ga4DetailPane');
|
|
85
|
+
resizeHandle.addEventListener('mousedown', (e) => {
|
|
86
|
+
e.preventDefault();
|
|
87
|
+
const startX = e.clientX;
|
|
88
|
+
const startWidth = detailPane.offsetWidth;
|
|
89
|
+
document.body.style.cursor = 'col-resize';
|
|
90
|
+
document.body.style.userSelect = 'none';
|
|
91
|
+
function onMove(ev) {
|
|
92
|
+
const delta = startX - ev.clientX;
|
|
93
|
+
detailPane.style.width = Math.max(200, Math.min(window.innerWidth * 0.8, startWidth + delta)) + 'px';
|
|
94
|
+
}
|
|
95
|
+
function onUp() {
|
|
96
|
+
document.body.style.cursor = '';
|
|
97
|
+
document.body.style.userSelect = '';
|
|
98
|
+
document.removeEventListener('mousemove', onMove);
|
|
99
|
+
document.removeEventListener('mouseup', onUp);
|
|
100
|
+
}
|
|
101
|
+
document.addEventListener('mousemove', onMove);
|
|
102
|
+
document.addEventListener('mouseup', onUp);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function handleGA4Event(event) {
|
|
107
|
+
if (!isTabEnabled('ga4')) return;
|
|
108
|
+
ga4State.events.push({
|
|
109
|
+
name: event.name || '?',
|
|
110
|
+
params: event.params || {},
|
|
111
|
+
tag: event.tag || 'GA4',
|
|
112
|
+
source: event.source || '',
|
|
113
|
+
ts: event.ts || Date.now(),
|
|
114
|
+
index: ga4State.events.length,
|
|
115
|
+
});
|
|
116
|
+
$('ga4Badge').textContent = ga4State.events.length;
|
|
117
|
+
|
|
118
|
+
// Append to list (batched via rAF)
|
|
119
|
+
if (!ga4State._raf) {
|
|
120
|
+
ga4State._raf = requestAnimationFrame(() => {
|
|
121
|
+
ga4State._raf = null;
|
|
122
|
+
renderGA4List();
|
|
123
|
+
renderGA4Summary();
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Assign consistent color to each GA4 event name
|
|
129
|
+
const _ga4EventColors = {};
|
|
130
|
+
const _ga4ColorPalette = [
|
|
131
|
+
'#4facff', // blue
|
|
132
|
+
'#3dd68c', // green
|
|
133
|
+
'#ff813f', // orange
|
|
134
|
+
'#c678dd', // purple
|
|
135
|
+
'#e06c75', // coral
|
|
136
|
+
'#56b6c2', // teal
|
|
137
|
+
'#d19a66', // gold
|
|
138
|
+
'#98c379', // lime
|
|
139
|
+
'#e5c07b', // yellow
|
|
140
|
+
'#ff5e72', // red
|
|
141
|
+
'#61afef', // light blue
|
|
142
|
+
'#be5046', // rust
|
|
143
|
+
];
|
|
144
|
+
let _ga4ColorIdx = 0;
|
|
145
|
+
function _ga4EventColor(name) {
|
|
146
|
+
if (!getGA4ColorsEnabled()) return ''; // empty = inherit default text color
|
|
147
|
+
if (!_ga4EventColors[name]) {
|
|
148
|
+
_ga4EventColors[name] = _ga4ColorPalette[_ga4ColorIdx % _ga4ColorPalette.length];
|
|
149
|
+
_ga4ColorIdx++;
|
|
150
|
+
}
|
|
151
|
+
return _ga4EventColors[name];
|
|
152
|
+
}
|
|
153
|
+
function getGA4ColorsEnabled() {
|
|
154
|
+
try { return localStorage.getItem('rn-debug-ga4-colors') === 'true'; } catch { return false; }
|
|
155
|
+
}
|
|
156
|
+
function setGA4ColorsEnabled(v) {
|
|
157
|
+
try { localStorage.setItem('rn-debug-ga4-colors', v ? 'true' : 'false'); } catch {}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function renderGA4List() {
|
|
161
|
+
const list = $('ga4List');
|
|
162
|
+
const empty = $('ga4Empty');
|
|
163
|
+
if (!list) return;
|
|
164
|
+
|
|
165
|
+
const { searchFilter, sortDir } = ga4State;
|
|
166
|
+
let visible = ga4State.events.filter(e =>
|
|
167
|
+
!searchFilter || e.name.toLowerCase().includes(searchFilter)
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
// Sort: newest first (desc) or oldest first (asc)
|
|
171
|
+
if (sortDir === 'desc') {
|
|
172
|
+
visible = [...visible].reverse();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
empty.style.display = visible.length ? 'none' : 'flex';
|
|
176
|
+
list.querySelectorAll('.ga4-row').forEach(e => e.remove());
|
|
177
|
+
|
|
178
|
+
// Cap at 500 rows
|
|
179
|
+
const MAX = 500;
|
|
180
|
+
const toRender = visible.length > MAX ? visible.slice(0, MAX) : visible;
|
|
181
|
+
|
|
182
|
+
const frag = document.createDocumentFragment();
|
|
183
|
+
toRender.forEach(e => {
|
|
184
|
+
const row = document.createElement('div');
|
|
185
|
+
row.className = 'ga4-row' + (e.index === ga4State.selected ? ' selected' : '');
|
|
186
|
+
|
|
187
|
+
const time = new Date(e.ts).toLocaleTimeString('en', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
188
|
+
|
|
189
|
+
const evtColor = _ga4EventColor(e.name);
|
|
190
|
+
const colorStyle = evtColor ? `color:${evtColor}` : '';
|
|
191
|
+
row.innerHTML = `
|
|
192
|
+
<span class="ga4-cell ga4-time">${time}</span>
|
|
193
|
+
<span class="ga4-cell ga4-name" style="${colorStyle}">${esc(e.name)}</span>`;
|
|
194
|
+
|
|
195
|
+
row.addEventListener('click', () => {
|
|
196
|
+
ga4State.selected = e.index;
|
|
197
|
+
list.querySelectorAll('.ga4-row').forEach(r => r.classList.remove('selected'));
|
|
198
|
+
row.classList.add('selected');
|
|
199
|
+
renderGA4Detail(e);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Right-click to copy
|
|
203
|
+
row.addEventListener('contextmenu', (ev) => {
|
|
204
|
+
ev.preventDefault();
|
|
205
|
+
showContextMenu(ev, [
|
|
206
|
+
{ label: 'Copy Event Name', action: () => navigator.clipboard.writeText(e.name) },
|
|
207
|
+
{ label: 'Copy as JSON', action: () => navigator.clipboard.writeText(JSON.stringify({ event: e.name, params: e.params }, null, 2)) },
|
|
208
|
+
]);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
frag.appendChild(row);
|
|
212
|
+
});
|
|
213
|
+
list.appendChild(frag);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function renderGA4Detail(e) {
|
|
217
|
+
let detail = $('ga4Detail');
|
|
218
|
+
if (!detail) return;
|
|
219
|
+
|
|
220
|
+
const time = new Date(e.ts).toLocaleTimeString('en', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
221
|
+
|
|
222
|
+
// Clone-replace to remove stale event listeners
|
|
223
|
+
const fresh = detail.cloneNode(false);
|
|
224
|
+
detail.parentNode.replaceChild(fresh, detail);
|
|
225
|
+
detail = fresh;
|
|
226
|
+
|
|
227
|
+
// Header info
|
|
228
|
+
const header = document.createElement('div');
|
|
229
|
+
header.className = 'ga4-detail-info';
|
|
230
|
+
header.innerHTML = `
|
|
231
|
+
<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>
|
|
232
|
+
<div class="ga4-detail-row"><span class="ga4-detail-key">Timestamp</span><span class="ga4-detail-val">${time}</span></div>
|
|
233
|
+
`;
|
|
234
|
+
detail.appendChild(header);
|
|
235
|
+
|
|
236
|
+
// Separator
|
|
237
|
+
const sep = document.createElement('div');
|
|
238
|
+
sep.className = 'ga4-detail-sep';
|
|
239
|
+
detail.appendChild(sep);
|
|
240
|
+
|
|
241
|
+
// Parameters as key-value list with collapsible objects
|
|
242
|
+
if (e.params && typeof e.params === 'object') {
|
|
243
|
+
const keys = Object.keys(e.params).sort();
|
|
244
|
+
keys.forEach(key => {
|
|
245
|
+
const val = e.params[key];
|
|
246
|
+
const row = document.createElement('div');
|
|
247
|
+
row.className = 'ga4-param-row';
|
|
248
|
+
|
|
249
|
+
const keyEl = document.createElement('span');
|
|
250
|
+
keyEl.className = 'ga4-param-key';
|
|
251
|
+
keyEl.textContent = key;
|
|
252
|
+
row.appendChild(keyEl);
|
|
253
|
+
|
|
254
|
+
if (val && typeof val === 'object') {
|
|
255
|
+
// Collapsible object tree
|
|
256
|
+
const treeWrap = document.createElement('span');
|
|
257
|
+
treeWrap.className = 'ga4-param-val';
|
|
258
|
+
treeWrap.appendChild(createTreeNode(null, val, true));
|
|
259
|
+
row.appendChild(treeWrap);
|
|
260
|
+
} else {
|
|
261
|
+
const valEl = document.createElement('span');
|
|
262
|
+
valEl.className = 'ga4-param-val';
|
|
263
|
+
valEl.textContent = val === null ? 'null' : val === undefined ? 'undefined' : JSON.stringify(val);
|
|
264
|
+
if (typeof val === 'string') valEl.style.color = 'var(--green)';
|
|
265
|
+
else if (typeof val === 'number') valEl.style.color = 'var(--orange)';
|
|
266
|
+
else if (typeof val === 'boolean') valEl.style.color = 'var(--accent2)';
|
|
267
|
+
row.appendChild(valEl);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
detail.appendChild(row);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Right-click on detail
|
|
275
|
+
detail.addEventListener('contextmenu', (ev) => {
|
|
276
|
+
ev.preventDefault();
|
|
277
|
+
showContextMenu(ev, [
|
|
278
|
+
{ label: 'Copy All Parameters', action: () => navigator.clipboard.writeText(JSON.stringify(e.params, null, 2)) },
|
|
279
|
+
{ label: 'Copy Event JSON', action: () => navigator.clipboard.writeText(JSON.stringify({ event: e.name, params: e.params, timestamp: e.ts }, null, 2)) },
|
|
280
|
+
]);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function renderGA4Summary() {
|
|
285
|
+
const summary = $('ga4Summary');
|
|
286
|
+
if (!summary) return;
|
|
287
|
+
|
|
288
|
+
const counts = {};
|
|
289
|
+
ga4State.events.forEach(e => {
|
|
290
|
+
counts[e.name] = (counts[e.name] || 0) + 1;
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]);
|
|
294
|
+
|
|
295
|
+
summary.innerHTML = '';
|
|
296
|
+
|
|
297
|
+
const totalLabel = document.createElement('span');
|
|
298
|
+
totalLabel.className = 'ga4-summary-label';
|
|
299
|
+
totalLabel.textContent = `Total: ${ga4State.events.length}`;
|
|
300
|
+
summary.appendChild(totalLabel);
|
|
301
|
+
|
|
302
|
+
sorted.forEach(([name, count]) => {
|
|
303
|
+
const chip = document.createElement('span');
|
|
304
|
+
const isActive = ga4State.searchFilter === name.toLowerCase();
|
|
305
|
+
const chipColor = _ga4EventColor(name);
|
|
306
|
+
chip.className = 'ga4-summary-chip' + (isActive ? ' active' : '');
|
|
307
|
+
if (chipColor) {
|
|
308
|
+
chip.style.borderColor = chipColor;
|
|
309
|
+
if (isActive) chip.style.background = chipColor + '22';
|
|
310
|
+
chip.innerHTML = `<b style="color:${chipColor}">${esc(name)}</b><span class="chip-count">${count}</span>`;
|
|
311
|
+
} else {
|
|
312
|
+
chip.innerHTML = `<b>${esc(name)}</b><span class="chip-count">${count}</span>`;
|
|
313
|
+
}
|
|
314
|
+
chip.addEventListener('click', () => {
|
|
315
|
+
const search = $('ga4Search');
|
|
316
|
+
if (isActive) {
|
|
317
|
+
// Clear filter
|
|
318
|
+
ga4State.searchFilter = '';
|
|
319
|
+
if (search) search.value = '';
|
|
320
|
+
} else {
|
|
321
|
+
// Set filter to this event name
|
|
322
|
+
ga4State.searchFilter = name.toLowerCase();
|
|
323
|
+
if (search) search.value = name;
|
|
324
|
+
}
|
|
325
|
+
renderGA4List();
|
|
326
|
+
renderGA4Summary();
|
|
327
|
+
});
|
|
328
|
+
summary.appendChild(chip);
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
package/panels/native.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// ─── Native Logs Panel ─────────────────────────────────────────────────────
|
|
2
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
3
|
+
// NATIVE LOGS PANEL
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
const _nativeState = { logs: [], connected: false, platform: null, levelFilter: 'all', searchFilter: '' };
|
|
6
|
+
const MAX_NATIVE_LOGS = 2000;
|
|
7
|
+
|
|
8
|
+
function initNativeLogsPanel() {
|
|
9
|
+
const panel = $('panel-native');
|
|
10
|
+
if (!panel) return;
|
|
11
|
+
panel.innerHTML = `
|
|
12
|
+
<div class="panel-toolbar">
|
|
13
|
+
<span class="panel-label">Native Logs</span>
|
|
14
|
+
<span class="badge" id="nativeBadge">0</span>
|
|
15
|
+
<div class="ml-auto" style="display:flex;align-items:center;gap:6px">
|
|
16
|
+
<span class="native-status" id="nativeStatus">Detecting...</span>
|
|
17
|
+
<button class="panel-clear-btn" id="nativeClear">Clear</button>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="native-connect-panel" id="nativeConnectPanel">
|
|
21
|
+
<div class="native-hero">
|
|
22
|
+
<div style="font-size:36px;opacity:0.15;margin-bottom:12px">📱</div>
|
|
23
|
+
<div style="font-size:14px;font-weight:600;color:var(--text);margin-bottom:6px">Native Logs</div>
|
|
24
|
+
<div style="font-size:11px;color:var(--text-dim);max-width:420px;line-height:1.7;margin-bottom:20px">
|
|
25
|
+
Stream native crash logs, errors, and warnings directly in ReactoRadar.<br/>
|
|
26
|
+
No need to open Android Studio or Xcode.
|
|
27
|
+
</div>
|
|
28
|
+
<div class="native-platform-cards">
|
|
29
|
+
<div class="native-card" id="nativeCardAndroid">
|
|
30
|
+
<div class="native-card-icon">🤖</div>
|
|
31
|
+
<div class="native-card-title">Android</div>
|
|
32
|
+
<div class="native-card-hint">Requires: <code>adb</code> in PATH (Android SDK)</div>
|
|
33
|
+
<div class="native-card-prereq">
|
|
34
|
+
<div class="native-prereq-step"><b>Prerequisites:</b></div>
|
|
35
|
+
<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>
|
|
36
|
+
<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>
|
|
37
|
+
<div class="native-prereq-step">3. Connect device via USB and accept the prompt</div>
|
|
38
|
+
<div class="native-prereq-step">4. Verify: run <code>adb devices</code> in terminal</div>
|
|
39
|
+
</div>
|
|
40
|
+
<div id="nativeAndroidStatus" class="native-detect-status"></div>
|
|
41
|
+
<button class="native-connect-btn" id="nativeConnectAndroid">Connect Android</button>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="native-card" id="nativeCardIOS">
|
|
44
|
+
<div class="native-card-icon">🍎</div>
|
|
45
|
+
<div class="native-card-title">iOS</div>
|
|
46
|
+
<div class="native-card-hint">Simulator or USB device</div>
|
|
47
|
+
<div class="native-card-prereq">
|
|
48
|
+
<div class="native-prereq-step"><b>Simulator:</b></div>
|
|
49
|
+
<div class="native-prereq-step">Requires Xcode Command Line Tools<br/><code>xcode-select --install</code></div>
|
|
50
|
+
<div class="native-prereq-step" style="margin-top:6px"><b>Real Device (USB):</b></div>
|
|
51
|
+
<div class="native-prereq-step">1. Install: <code>brew install libimobiledevice</code></div>
|
|
52
|
+
<div class="native-prereq-step">2. Connect device, tap <b>Trust</b> on the prompt</div>
|
|
53
|
+
<div class="native-prereq-step">3. Verify: <code>idevice_id -l</code> shows device UDID</div>
|
|
54
|
+
</div>
|
|
55
|
+
<div id="nativeIOSStatus" class="native-detect-status"></div>
|
|
56
|
+
<div style="display:flex;gap:6px;margin-top:8px">
|
|
57
|
+
<button class="native-connect-btn" id="nativeConnectIOSSim">Simulator</button>
|
|
58
|
+
<button class="native-connect-btn" id="nativeConnectIOSDevice">USB Device</button>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
<div class="native-logs-area" id="nativeLogsArea" style="display:none">
|
|
65
|
+
<div class="native-filter-bar">
|
|
66
|
+
<input id="nativeSearch" class="net-search-input" placeholder="Filter logs..." />
|
|
67
|
+
<div class="native-level-filters" id="nativeLevelFilters">
|
|
68
|
+
<button class="net-status-btn active" data-level="all">All</button>
|
|
69
|
+
<button class="net-status-btn" data-level="fatal">Fatal</button>
|
|
70
|
+
<button class="net-status-btn" data-level="error">Error</button>
|
|
71
|
+
<button class="net-status-btn" data-level="warn">Warn</button>
|
|
72
|
+
<button class="net-status-btn" data-level="info">Info</button>
|
|
73
|
+
<button class="net-status-btn" data-level="debug">Debug</button>
|
|
74
|
+
</div>
|
|
75
|
+
<div style="margin-left:auto;display:flex;gap:6px;align-items:center">
|
|
76
|
+
<button class="panel-clear-btn" id="nativeLogsClear">Clear</button>
|
|
77
|
+
<button class="panel-clear-btn" id="nativeDisconnect" style="color:var(--red)">Disconnect</button>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="native-log-list" id="nativeLogList"></div>
|
|
81
|
+
</div>`;
|
|
82
|
+
|
|
83
|
+
// Connect buttons — auto-enable tab when user clicks connect
|
|
84
|
+
function _enableNativeTab() {
|
|
85
|
+
const vis = getTabVisibility();
|
|
86
|
+
if (!vis['native']) {
|
|
87
|
+
vis['native'] = true;
|
|
88
|
+
setTabVisibility(vis);
|
|
89
|
+
applyTabVisibility();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
$('nativeConnectAndroid')?.addEventListener('click', () => { _enableNativeTab(); window.electronAPI?.startNativeLogs('android'); });
|
|
93
|
+
$('nativeConnectIOSSim')?.addEventListener('click', () => { _enableNativeTab(); window.electronAPI?.startNativeLogs('ios-sim'); });
|
|
94
|
+
$('nativeConnectIOSDevice')?.addEventListener('click', () => { _enableNativeTab(); window.electronAPI?.startNativeLogs('ios-device'); });
|
|
95
|
+
$('nativeDisconnect')?.addEventListener('click', () => window.electronAPI?.stopNativeLogs());
|
|
96
|
+
|
|
97
|
+
// Clear buttons (toolbar + logs area)
|
|
98
|
+
$('nativeClear')?.addEventListener('click', _clearNativeLogs);
|
|
99
|
+
$('nativeLogsClear')?.addEventListener('click', _clearNativeLogs);
|
|
100
|
+
|
|
101
|
+
// Level filter
|
|
102
|
+
$('nativeLevelFilters')?.addEventListener('click', (e) => {
|
|
103
|
+
const btn = e.target.closest('.net-status-btn');
|
|
104
|
+
if (!btn) return;
|
|
105
|
+
$('nativeLevelFilters').querySelectorAll('.net-status-btn').forEach(b => b.classList.remove('active'));
|
|
106
|
+
btn.classList.add('active');
|
|
107
|
+
_nativeState.levelFilter = btn.dataset.level;
|
|
108
|
+
_renderNativeLogs();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Search
|
|
112
|
+
$('nativeSearch')?.addEventListener('input', (e) => {
|
|
113
|
+
_nativeState.searchFilter = e.target.value.toLowerCase().trim();
|
|
114
|
+
_renderNativeLogs();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// IPC: receive native logs
|
|
118
|
+
window.electronAPI?.on('native-log', (log) => {
|
|
119
|
+
if (!isTabEnabled('native')) return;
|
|
120
|
+
_nativeState.logs.push(log);
|
|
121
|
+
if (_nativeState.logs.length > MAX_NATIVE_LOGS) {
|
|
122
|
+
_nativeState.logs = _nativeState.logs.slice(-MAX_NATIVE_LOGS);
|
|
123
|
+
}
|
|
124
|
+
$('nativeBadge').textContent = _nativeState.logs.length;
|
|
125
|
+
_appendNativeLog(log);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// IPC: connection status
|
|
129
|
+
window.electronAPI?.on('native-status', (status) => {
|
|
130
|
+
_nativeState.connected = status.connected;
|
|
131
|
+
_nativeState.platform = status.platform || null;
|
|
132
|
+
const statusEl = $('nativeStatus');
|
|
133
|
+
const connectPanel = $('nativeConnectPanel');
|
|
134
|
+
const logsArea = $('nativeLogsArea');
|
|
135
|
+
|
|
136
|
+
if (status.connected) {
|
|
137
|
+
if (statusEl) { statusEl.textContent = `Connected (${status.platform})`; statusEl.style.color = 'var(--green)'; }
|
|
138
|
+
if (connectPanel) connectPanel.style.display = 'none';
|
|
139
|
+
if (logsArea) logsArea.style.display = 'flex';
|
|
140
|
+
} else {
|
|
141
|
+
if (statusEl) {
|
|
142
|
+
statusEl.textContent = status.error || 'Not connected';
|
|
143
|
+
statusEl.style.color = status.error ? 'var(--red)' : 'var(--text-dim)';
|
|
144
|
+
}
|
|
145
|
+
if (connectPanel) connectPanel.style.display = 'flex';
|
|
146
|
+
if (logsArea) logsArea.style.display = 'none';
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Auto-detect platform and auto-connect
|
|
151
|
+
_autoDetectNative();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function _clearNativeLogs() {
|
|
155
|
+
_nativeState.logs = [];
|
|
156
|
+
if ($('nativeBadge')) $('nativeBadge').textContent = '0';
|
|
157
|
+
const list = $('nativeLogList');
|
|
158
|
+
if (list) list.innerHTML = '';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function _autoDetectNative() {
|
|
162
|
+
const statusEl = $('nativeStatus');
|
|
163
|
+
try {
|
|
164
|
+
const result = await window.electronAPI?.detectNativePlatform();
|
|
165
|
+
if (!result) { if (statusEl) { statusEl.textContent = 'Detection unavailable'; statusEl.style.color = 'var(--text-dim)'; } return; }
|
|
166
|
+
|
|
167
|
+
// Update card statuses
|
|
168
|
+
const androidStatus = $('nativeAndroidStatus');
|
|
169
|
+
const iosStatus = $('nativeIOSStatus');
|
|
170
|
+
if (androidStatus) {
|
|
171
|
+
if (result.android) { androidStatus.innerHTML = '<span style="color:var(--green)">Device detected</span>'; }
|
|
172
|
+
else if (result.adbPath) { androidStatus.innerHTML = '<span style="color:var(--orange)">adb found — no device connected</span>'; }
|
|
173
|
+
else { androidStatus.innerHTML = '<span style="color:var(--text-dim)">adb not found</span>'; }
|
|
174
|
+
}
|
|
175
|
+
if (iosStatus) {
|
|
176
|
+
const parts = [];
|
|
177
|
+
if (result.iosSim) parts.push('<span style="color:var(--green)">Simulator running</span>');
|
|
178
|
+
if (result.iosDevice) parts.push('<span style="color:var(--green)">USB device detected</span>');
|
|
179
|
+
if (!parts.length) parts.push('<span style="color:var(--text-dim)">No device detected</span>');
|
|
180
|
+
iosStatus.innerHTML = parts.join(' · ');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Show detection result — user clicks Connect to start
|
|
184
|
+
if (result.android || result.iosSim || result.iosDevice) {
|
|
185
|
+
const detected = [result.android ? 'Android' : '', result.iosSim ? 'iOS Sim' : '', result.iosDevice ? 'iOS Device' : ''].filter(Boolean).join(', ');
|
|
186
|
+
if (statusEl) { statusEl.textContent = `Detected: ${detected} — click Connect to start`; statusEl.style.color = 'var(--accent)'; }
|
|
187
|
+
} else {
|
|
188
|
+
if (statusEl) { statusEl.textContent = 'No device detected'; statusEl.style.color = 'var(--text-dim)'; }
|
|
189
|
+
}
|
|
190
|
+
} catch {
|
|
191
|
+
if (statusEl) { statusEl.textContent = 'Detection failed'; statusEl.style.color = 'var(--text-dim)'; }
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function _appendNativeLog(log) {
|
|
196
|
+
const list = $('nativeLogList');
|
|
197
|
+
if (!list) return;
|
|
198
|
+
|
|
199
|
+
// Check filters
|
|
200
|
+
if (_nativeState.levelFilter !== 'all' && log.level !== _nativeState.levelFilter) return;
|
|
201
|
+
if (_nativeState.searchFilter && !log.message?.toLowerCase().includes(_nativeState.searchFilter) && !log.tag?.toLowerCase().includes(_nativeState.searchFilter)) return;
|
|
202
|
+
|
|
203
|
+
const isExpandable = log.level === 'error' || log.level === 'fatal' || (log.message || '').length > 200;
|
|
204
|
+
const row = document.createElement('div');
|
|
205
|
+
row.className = `native-log-row native-${log.level || 'info'}`;
|
|
206
|
+
|
|
207
|
+
const time = log.time || new Date(log.ts).toLocaleTimeString('en', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
208
|
+
|
|
209
|
+
// Header line (always visible)
|
|
210
|
+
const header = document.createElement('div');
|
|
211
|
+
header.className = 'native-log-header';
|
|
212
|
+
header.innerHTML = `<span class="native-log-time">${esc(time)}</span>`
|
|
213
|
+
+ `<span class="native-log-level">${esc((log.level || 'info').toUpperCase())}</span>`
|
|
214
|
+
+ (log.tag ? `<span class="native-log-tag">${esc(log.tag)}</span>` : '')
|
|
215
|
+
+ `<span class="native-log-preview">${esc((log.message || '').split('\\n')[0].slice(0, 200))}</span>`;
|
|
216
|
+
row.appendChild(header);
|
|
217
|
+
|
|
218
|
+
// Expandable full message (for errors and long messages)
|
|
219
|
+
if (isExpandable) {
|
|
220
|
+
const fullMsg = document.createElement('div');
|
|
221
|
+
fullMsg.className = 'native-log-full';
|
|
222
|
+
fullMsg.style.display = 'none';
|
|
223
|
+
fullMsg.textContent = log.message || '';
|
|
224
|
+
row.appendChild(fullMsg);
|
|
225
|
+
|
|
226
|
+
header.style.cursor = 'pointer';
|
|
227
|
+
header.addEventListener('click', () => {
|
|
228
|
+
const open = fullMsg.style.display !== 'none';
|
|
229
|
+
fullMsg.style.display = open ? 'none' : 'block';
|
|
230
|
+
row.classList.toggle('expanded', !open);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Right-click to copy
|
|
235
|
+
row.addEventListener('contextmenu', (e) => {
|
|
236
|
+
e.preventDefault();
|
|
237
|
+
showContextMenu(e, [
|
|
238
|
+
{ label: 'Copy Message', action: () => navigator.clipboard.writeText(log.message || '') },
|
|
239
|
+
{ label: 'Copy Raw Line', action: () => navigator.clipboard.writeText(log.raw || log.message || '') },
|
|
240
|
+
...(log.tag ? [{ label: `Copy Tag (${log.tag})`, action: () => navigator.clipboard.writeText(log.tag) }] : []),
|
|
241
|
+
]);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
list.appendChild(row);
|
|
245
|
+
|
|
246
|
+
// Cap DOM rows
|
|
247
|
+
while (list.children.length > 1000) list.firstChild.remove();
|
|
248
|
+
|
|
249
|
+
// Auto-scroll if near bottom
|
|
250
|
+
const atBottom = (list.scrollHeight - list.scrollTop - list.clientHeight) < 150;
|
|
251
|
+
if (atBottom) list.scrollTop = list.scrollHeight;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function _renderNativeLogs() {
|
|
255
|
+
const list = $('nativeLogList');
|
|
256
|
+
if (!list) return;
|
|
257
|
+
list.innerHTML = '';
|
|
258
|
+
_nativeState.logs.forEach(log => _appendNativeLog(log));
|
|
259
|
+
}
|
|
260
|
+
|