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
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// ─── Sources Panel ─────────────────────────────────────────────────────────
|
|
2
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
3
|
+
// SOURCES PANEL (placeholder — use JS Debugger button for breakpoints)
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
function initSourcesPanel() {
|
|
6
|
+
const panel = $('panel-sources');
|
|
7
|
+
if (!panel) return;
|
|
8
|
+
panel.innerHTML = `
|
|
9
|
+
<div class="panel-toolbar">
|
|
10
|
+
<span class="panel-label">Sources</span>
|
|
11
|
+
<div class="ml-auto" style="display:flex;gap:6px">
|
|
12
|
+
<button class="tb-btn" id="btnOpenSourcesExt" title="Open in separate DevTools window">Breakpoints ↗</button>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="sources-layout">
|
|
16
|
+
<div class="sources-sidebar" id="sourcesSidebar">
|
|
17
|
+
<div class="panel-toolbar" style="height:32px">
|
|
18
|
+
<input id="sourcesSearch" class="net-search-input" style="width:100%" placeholder="Search files..." />
|
|
19
|
+
</div>
|
|
20
|
+
<div class="scroll-area sources-file-list" id="sourcesFileList">
|
|
21
|
+
<div class="empty-state" id="sourcesEmpty">
|
|
22
|
+
<div class="icon" style="font-size:28px;opacity:.2"></></div>
|
|
23
|
+
<div class="label">Waiting for Metro...</div>
|
|
24
|
+
<div class="hint">Source files will load when Metro is running</div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="sources-editor" id="sourcesEditor">
|
|
29
|
+
<div class="panel-toolbar" style="height:32px">
|
|
30
|
+
<span id="sourcesFileName" style="font-size:10px;color:var(--accent)"></span>
|
|
31
|
+
<span id="sourcesLineInfo" style="font-size:10px;color:var(--text-dim);margin-left:auto"></span>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="scroll-area sources-code" id="sourcesCode">
|
|
34
|
+
<span style="color:var(--text-dim);padding:20px;display:block">Select a file to view its source</span>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>`;
|
|
38
|
+
|
|
39
|
+
// Open JS Debugger for breakpoints
|
|
40
|
+
$('btnOpenSourcesExt').addEventListener('click', () => {
|
|
41
|
+
window.electronAPI?.openCDPTarget(null);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Search filter for file tree
|
|
45
|
+
$('sourcesSearch').addEventListener('input', (e) => {
|
|
46
|
+
const term = e.target.value.toLowerCase().trim();
|
|
47
|
+
document.querySelectorAll('#sourcesFileList .src-tree-file').forEach(row => {
|
|
48
|
+
const filepath = row.dataset.file || '';
|
|
49
|
+
const match = !term || filepath.toLowerCase().includes(term);
|
|
50
|
+
row.style.display = match ? '' : 'none';
|
|
51
|
+
});
|
|
52
|
+
// Show/hide folder nodes based on whether they have visible children
|
|
53
|
+
document.querySelectorAll('#sourcesFileList .src-tree-folder').forEach(folder => {
|
|
54
|
+
const visibleFiles = folder.querySelectorAll('.src-tree-file:not([style*="display: none"])');
|
|
55
|
+
folder.style.display = (!term || visibleFiles.length > 0) ? '' : 'none';
|
|
56
|
+
// Auto-expand folders when searching
|
|
57
|
+
if (term && visibleFiles.length > 0) {
|
|
58
|
+
const children = folder.querySelector('.src-tree-children');
|
|
59
|
+
const arrow = folder.querySelector('.src-tree-arrow');
|
|
60
|
+
if (children) children.style.display = 'block';
|
|
61
|
+
if (arrow) { arrow.textContent = '\u25BC'; arrow.classList.add('expanded'); }
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Fetch the source map / bundle modules list from Metro
|
|
67
|
+
fetchSourceFileList();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function fetchSourceFileList() {
|
|
71
|
+
if (!window.electronAPI?.getSourceFileList) {
|
|
72
|
+
console.log('[Sources] electronAPI.getSourceFileList not available, retrying...');
|
|
73
|
+
setTimeout(fetchSourceFileList, 5000);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
console.log('[Sources] Fetching file list from Metro...');
|
|
78
|
+
const result = await window.electronAPI.getSourceFileList();
|
|
79
|
+
console.log('[Sources] Got result:', result?.files?.length, 'files, root:', result?.root?.slice(-30));
|
|
80
|
+
if (result?.files && result.files.length > 0) {
|
|
81
|
+
state._sourcesRoot = result.root;
|
|
82
|
+
// Limit to 500 files max to avoid DOM overload
|
|
83
|
+
const files = result.files.length > 500 ? result.files.slice(0, 500) : result.files;
|
|
84
|
+
renderSourceFileList(files);
|
|
85
|
+
console.log('[Sources] Rendered', files.length, 'files');
|
|
86
|
+
} else {
|
|
87
|
+
console.log('[Sources] No files, retrying in 5s...');
|
|
88
|
+
setTimeout(fetchSourceFileList, 5000);
|
|
89
|
+
}
|
|
90
|
+
} catch (e) {
|
|
91
|
+
console.log('[Sources] Error:', e?.message || e);
|
|
92
|
+
setTimeout(fetchSourceFileList, 5000);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function renderSourceFileList(files) {
|
|
97
|
+
const list = $('sourcesFileList');
|
|
98
|
+
const empty = $('sourcesEmpty');
|
|
99
|
+
if (!list) return;
|
|
100
|
+
if (!files.length) return;
|
|
101
|
+
if (empty) empty.style.display = 'none';
|
|
102
|
+
list.querySelectorAll('.src-tree-node').forEach(e => e.remove());
|
|
103
|
+
|
|
104
|
+
// Build folder tree from file paths
|
|
105
|
+
const tree = {};
|
|
106
|
+
files.forEach(filepath => {
|
|
107
|
+
const parts = filepath.split('/').filter(Boolean);
|
|
108
|
+
let node = tree;
|
|
109
|
+
parts.forEach((part, i) => {
|
|
110
|
+
if (i === parts.length - 1) {
|
|
111
|
+
// File leaf
|
|
112
|
+
node[part] = filepath; // string = file
|
|
113
|
+
} else {
|
|
114
|
+
// Folder
|
|
115
|
+
if (!node[part] || typeof node[part] === 'string') node[part] = {};
|
|
116
|
+
node = node[part];
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Render tree recursively
|
|
122
|
+
const frag = document.createDocumentFragment();
|
|
123
|
+
|
|
124
|
+
// Project folders first, node_modules last
|
|
125
|
+
const topKeys = Object.keys(tree).sort((a, b) => {
|
|
126
|
+
if (a === 'node_modules') return 1;
|
|
127
|
+
if (b === 'node_modules') return -1;
|
|
128
|
+
return a.localeCompare(b);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
topKeys.forEach(key => {
|
|
132
|
+
frag.appendChild(buildSourceTreeNode(key, tree[key], 0));
|
|
133
|
+
});
|
|
134
|
+
list.appendChild(frag);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function buildSourceTreeNode(name, value, depth) {
|
|
138
|
+
if (typeof value === 'string') {
|
|
139
|
+
// File leaf
|
|
140
|
+
const row = document.createElement('div');
|
|
141
|
+
row.className = 'src-tree-node src-tree-file';
|
|
142
|
+
row.dataset.file = value;
|
|
143
|
+
row.style.paddingLeft = (12 + depth * 16) + 'px';
|
|
144
|
+
const isNM = value.includes('node_modules');
|
|
145
|
+
const ext = name.split('.').pop();
|
|
146
|
+
const iconColor = ext === 'tsx' || ext === 'ts' ? '#3178c6'
|
|
147
|
+
: ext === 'jsx' || ext === 'js' ? '#f0db4f'
|
|
148
|
+
: ext === 'json' ? '#a0a0a0'
|
|
149
|
+
: ext === 'css' ? '#264de4'
|
|
150
|
+
: 'var(--text-dim)';
|
|
151
|
+
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>`;
|
|
152
|
+
row.addEventListener('click', () => {
|
|
153
|
+
const fileList = $('sourcesFileList');
|
|
154
|
+
fileList.querySelectorAll('.src-tree-file').forEach(el => el.classList.remove('selected'));
|
|
155
|
+
row.classList.add('selected');
|
|
156
|
+
loadSourceFile(value);
|
|
157
|
+
});
|
|
158
|
+
// Search filter support
|
|
159
|
+
const searchInput = $('sourcesSearch');
|
|
160
|
+
if (searchInput && searchInput.value) {
|
|
161
|
+
const term = searchInput.value.toLowerCase();
|
|
162
|
+
if (!name.toLowerCase().includes(term) && !value.toLowerCase().includes(term)) {
|
|
163
|
+
row.style.display = 'none';
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return row;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Folder node
|
|
170
|
+
const container = document.createElement('div');
|
|
171
|
+
container.className = 'src-tree-node src-tree-folder';
|
|
172
|
+
|
|
173
|
+
const header = document.createElement('div');
|
|
174
|
+
header.className = 'src-tree-folder-header';
|
|
175
|
+
header.style.paddingLeft = (8 + depth * 16) + 'px';
|
|
176
|
+
|
|
177
|
+
const arrow = document.createElement('span');
|
|
178
|
+
arrow.className = 'src-tree-arrow';
|
|
179
|
+
arrow.textContent = '\u25B6';
|
|
180
|
+
|
|
181
|
+
const folderName = document.createElement('span');
|
|
182
|
+
folderName.className = 'src-folder-name';
|
|
183
|
+
const isNM = name === 'node_modules';
|
|
184
|
+
folderName.style.color = isNM ? 'var(--text-dim)' : 'var(--text)';
|
|
185
|
+
folderName.textContent = name;
|
|
186
|
+
|
|
187
|
+
header.appendChild(arrow);
|
|
188
|
+
header.appendChild(folderName);
|
|
189
|
+
container.appendChild(header);
|
|
190
|
+
|
|
191
|
+
const children = document.createElement('div');
|
|
192
|
+
children.className = 'src-tree-children';
|
|
193
|
+
// Start all folders collapsed
|
|
194
|
+
children.style.display = 'none';
|
|
195
|
+
|
|
196
|
+
// Sort: folders first, then files
|
|
197
|
+
const entries = Object.entries(value).sort((a, b) => {
|
|
198
|
+
const aIsFolder = typeof a[1] === 'object';
|
|
199
|
+
const bIsFolder = typeof b[1] === 'object';
|
|
200
|
+
if (aIsFolder !== bIsFolder) return aIsFolder ? -1 : 1;
|
|
201
|
+
return a[0].localeCompare(b[0]);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
let populated = false;
|
|
205
|
+
function populate() {
|
|
206
|
+
if (populated) return;
|
|
207
|
+
populated = true;
|
|
208
|
+
entries.forEach(([childName, childValue]) => {
|
|
209
|
+
children.appendChild(buildSourceTreeNode(childName, childValue, depth + 1));
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Folders start collapsed — populate lazily on first expand
|
|
214
|
+
header.addEventListener('click', () => {
|
|
215
|
+
const isOpen = children.style.display !== 'none';
|
|
216
|
+
if (!isOpen) {
|
|
217
|
+
populate();
|
|
218
|
+
children.style.display = 'block';
|
|
219
|
+
arrow.textContent = '\u25BC';
|
|
220
|
+
arrow.classList.add('expanded');
|
|
221
|
+
} else {
|
|
222
|
+
children.style.display = 'none';
|
|
223
|
+
arrow.textContent = '\u25B6';
|
|
224
|
+
arrow.classList.remove('expanded');
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
container.appendChild(children);
|
|
229
|
+
return container;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function loadSourceFile(filepath) {
|
|
233
|
+
const codeEl = $('sourcesCode');
|
|
234
|
+
const nameEl = $('sourcesFileName');
|
|
235
|
+
const lineEl = $('sourcesLineInfo');
|
|
236
|
+
if (!codeEl) return;
|
|
237
|
+
if (nameEl) nameEl.textContent = filepath.split('/').pop();
|
|
238
|
+
if (lineEl) lineEl.textContent = filepath;
|
|
239
|
+
codeEl.innerHTML = '<span style="color:var(--text-dim)">Loading...</span>';
|
|
240
|
+
|
|
241
|
+
let source = null;
|
|
242
|
+
const root = state._sourcesRoot || '';
|
|
243
|
+
const fullPath = root ? `${root}/${filepath}` : filepath;
|
|
244
|
+
|
|
245
|
+
// Strategy 1: Read from disk via IPC (most reliable)
|
|
246
|
+
if (window.electronAPI?.readSourceFile) {
|
|
247
|
+
source = await window.electronAPI.readSourceFile(fullPath);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Strategy 2: Fetch from Metro
|
|
251
|
+
if (!source) {
|
|
252
|
+
try {
|
|
253
|
+
const port = getStoredMetroPort();
|
|
254
|
+
const resp = await fetch(`http://localhost:${port}/${filepath}?platform=ios&dev=true`);
|
|
255
|
+
if (resp.ok) source = await resp.text();
|
|
256
|
+
} catch {}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (!source) {
|
|
260
|
+
codeEl.innerHTML = `<span style="color:var(--text-dim);padding:20px;display:block">Could not load: ${esc(filepath)}</span>`;
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Render with line numbers
|
|
265
|
+
const lines = source.split('\n');
|
|
266
|
+
if (lineEl) lineEl.textContent = `${filepath} (${lines.length} lines)`;
|
|
267
|
+
codeEl.innerHTML = '';
|
|
268
|
+
const pre = document.createElement('pre');
|
|
269
|
+
pre.className = 'source-pre';
|
|
270
|
+
lines.forEach((line, i) => {
|
|
271
|
+
const lineDiv = document.createElement('div');
|
|
272
|
+
lineDiv.className = 'source-line';
|
|
273
|
+
lineDiv.innerHTML = `<span class="source-line-num">${i + 1}</span><span class="source-line-code">${syntaxHighlight(esc(line))}</span>`;
|
|
274
|
+
pre.appendChild(lineDiv);
|
|
275
|
+
});
|
|
276
|
+
codeEl.appendChild(pre);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Called from cdp-targets IPC handler (no longer opens external window)
|
|
280
|
+
|
|
281
|
+
// Called from cdp-targets IPC handler (shared, no duplicate registration)
|
|
282
|
+
// Sources panel uses Metro source map for file tree — CDP targets are only
|
|
283
|
+
// used for the "Breakpoints" button, not for the file list.
|
|
284
|
+
function updateSourcesPanel(targets) {
|
|
285
|
+
// No-op: file list is populated by fetchSourceFileList from Metro source map
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
289
|
+
// PERFORMANCE PANEL — FPS, render timing, JS thread
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// ─── AsyncStorage Panel ────────────────────────────────────────────────────
|
|
2
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
3
|
+
// ASYNC STORAGE PANEL
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
function initStoragePanel() {
|
|
6
|
+
const panel = $('panel-storage');
|
|
7
|
+
if (!panel) return;
|
|
8
|
+
panel.innerHTML = `
|
|
9
|
+
<div class="panel-toolbar">
|
|
10
|
+
<span class="panel-label">AsyncStorage</span>
|
|
11
|
+
<span class="badge" id="sBadge">0</span>
|
|
12
|
+
<div class="ml-auto">
|
|
13
|
+
<input id="storageSearch" class="net-search-input" placeholder="Filter keys..." />
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="storage-layout" id="storageLayout">
|
|
17
|
+
<div class="storage-keys" id="storageKeysPane">
|
|
18
|
+
<div class="panel-toolbar" style="height:32px">
|
|
19
|
+
<span style="font-size:10px;color:var(--text-dim);text-transform:uppercase;letter-spacing:1px">Keys</span>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="scroll-area storage-keys-list" id="storageKeyList">
|
|
22
|
+
<div class="empty-state" id="storageEmpty">
|
|
23
|
+
<div class="icon">💾</div>
|
|
24
|
+
<div class="label">No storage data</div>
|
|
25
|
+
<div class="hint">AsyncStorage data will appear here</div>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="storage-resize-handle" id="storageResizeHandle"></div>
|
|
30
|
+
<div class="storage-value-view">
|
|
31
|
+
<div class="storage-value-toolbar">
|
|
32
|
+
<span style="font-size:10px;color:var(--text-dim);text-transform:uppercase;letter-spacing:1px">Value</span>
|
|
33
|
+
<span id="storageSelectedKey" style="font-size:11px;color:var(--accent);margin-left:8px"></span>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="storage-value-body" id="storageValueBody">
|
|
36
|
+
<span style="color:var(--text-dim)">Select a key to view its value</span>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>`;
|
|
40
|
+
|
|
41
|
+
$('storageSearch').addEventListener('input', (e) => {
|
|
42
|
+
state.storage.searchFilter = e.target.value.toLowerCase().trim();
|
|
43
|
+
renderStorage();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Drag resize handle for key list width
|
|
47
|
+
const handle = $('storageResizeHandle');
|
|
48
|
+
const layout = $('storageLayout');
|
|
49
|
+
const keysPane = $('storageKeysPane');
|
|
50
|
+
if (handle && layout && keysPane) {
|
|
51
|
+
let dragging = false;
|
|
52
|
+
let startX = 0;
|
|
53
|
+
let startW = 0;
|
|
54
|
+
handle.addEventListener('mousedown', (e) => {
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
dragging = true;
|
|
57
|
+
startX = e.clientX;
|
|
58
|
+
startW = keysPane.offsetWidth;
|
|
59
|
+
document.body.style.cursor = 'col-resize';
|
|
60
|
+
document.body.style.userSelect = 'none';
|
|
61
|
+
});
|
|
62
|
+
document.addEventListener('mousemove', (e) => {
|
|
63
|
+
if (!dragging) return;
|
|
64
|
+
const newW = Math.max(120, Math.min(600, startW + (e.clientX - startX)));
|
|
65
|
+
layout.style.gridTemplateColumns = `${newW}px 4px 1fr`;
|
|
66
|
+
});
|
|
67
|
+
document.addEventListener('mouseup', () => {
|
|
68
|
+
if (!dragging) return;
|
|
69
|
+
dragging = false;
|
|
70
|
+
document.body.style.cursor = '';
|
|
71
|
+
document.body.style.userSelect = '';
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let _storageRAF = null;
|
|
77
|
+
|
|
78
|
+
function handleStorageEvent(event) {
|
|
79
|
+
if (event.type !== 'storage') return;
|
|
80
|
+
if (!isTabEnabled('storage')) return;
|
|
81
|
+
const { key, value, action } = event;
|
|
82
|
+
if (action === 'set' || action === 'snapshot') {
|
|
83
|
+
if (action === 'snapshot' && typeof key === 'object') {
|
|
84
|
+
// Skip if data hasn't changed
|
|
85
|
+
const newKeys = Object.keys(key).slice().sort().join(',');
|
|
86
|
+
const oldKeys = state.storage.keys.slice().sort().join(',');
|
|
87
|
+
if (newKeys === oldKeys) {
|
|
88
|
+
// Check if values changed
|
|
89
|
+
let same = true;
|
|
90
|
+
for (const [k, v] of Object.entries(key)) {
|
|
91
|
+
if (state.storage.entries[k] !== v) { same = false; break; }
|
|
92
|
+
}
|
|
93
|
+
if (same) return; // No changes, skip re-render
|
|
94
|
+
}
|
|
95
|
+
Object.entries(key).forEach(([k, v]) => {
|
|
96
|
+
state.storage.entries[k] = v;
|
|
97
|
+
if (!state.storage.keys.includes(k)) state.storage.keys.push(k);
|
|
98
|
+
});
|
|
99
|
+
} else {
|
|
100
|
+
if (state.storage.entries[key] === value) return; // No change
|
|
101
|
+
state.storage.entries[key] = value;
|
|
102
|
+
if (!state.storage.keys.includes(key)) state.storage.keys.push(key);
|
|
103
|
+
}
|
|
104
|
+
} else if (action === 'remove') {
|
|
105
|
+
if (!(key in state.storage.entries)) return; // Already removed
|
|
106
|
+
delete state.storage.entries[key];
|
|
107
|
+
state.storage.keys = state.storage.keys.filter(k => k !== key);
|
|
108
|
+
if (state.storage.selected === key) state.storage.selected = null;
|
|
109
|
+
}
|
|
110
|
+
$('sBadge').textContent = state.storage.keys.length;
|
|
111
|
+
// Debounce render via rAF
|
|
112
|
+
if (!_storageRAF) {
|
|
113
|
+
_storageRAF = requestAnimationFrame(() => {
|
|
114
|
+
_storageRAF = null;
|
|
115
|
+
renderStorage();
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function renderStorage() {
|
|
121
|
+
const list = $('storageKeyList');
|
|
122
|
+
const empty = $('storageEmpty');
|
|
123
|
+
if (!list) return;
|
|
124
|
+
|
|
125
|
+
const { searchFilter } = state.storage;
|
|
126
|
+
const visible = state.storage.keys.filter(k =>
|
|
127
|
+
!searchFilter || k.toLowerCase().includes(searchFilter)
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
empty.style.display = visible.length ? 'none' : 'flex';
|
|
131
|
+
list.querySelectorAll('.storage-key-row').forEach(e => e.remove());
|
|
132
|
+
|
|
133
|
+
const frag = document.createDocumentFragment();
|
|
134
|
+
visible.forEach(k => {
|
|
135
|
+
const div = document.createElement('div');
|
|
136
|
+
const val = state.storage.entries[k] || '';
|
|
137
|
+
div.className = 'storage-key-row entry' + (k === state.storage.selected ? ' selected' : '');
|
|
138
|
+
div.innerHTML = `
|
|
139
|
+
<span class="key-name">${highlight(esc(k), searchFilter)}</span>
|
|
140
|
+
<span class="key-size">${formatSize(val.length)}</span>`;
|
|
141
|
+
div.onclick = () => { state.storage.selected = k; renderStorage(); renderStorageValue(); };
|
|
142
|
+
frag.appendChild(div);
|
|
143
|
+
});
|
|
144
|
+
list.appendChild(frag);
|
|
145
|
+
renderStorageValue();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function renderStorageValue() {
|
|
149
|
+
let body = $('storageValueBody');
|
|
150
|
+
const keyLabel = $('storageSelectedKey');
|
|
151
|
+
if (!body) return;
|
|
152
|
+
const { selected, entries } = state.storage;
|
|
153
|
+
if (!selected) {
|
|
154
|
+
body.innerHTML = '<span style="color:var(--text-dim)">Select a key</span>';
|
|
155
|
+
if (keyLabel) keyLabel.textContent = '';
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (keyLabel) keyLabel.textContent = selected;
|
|
159
|
+
// Clone-replace to remove stale event listeners
|
|
160
|
+
const fresh = body.cloneNode(false);
|
|
161
|
+
body.parentNode.replaceChild(fresh, body);
|
|
162
|
+
body = fresh;
|
|
163
|
+
|
|
164
|
+
let val = entries[selected];
|
|
165
|
+
// Try to parse JSON strings into objects for tree display
|
|
166
|
+
if (typeof val === 'string') {
|
|
167
|
+
try { val = JSON.parse(val); } catch {}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (val && typeof val === 'object') {
|
|
171
|
+
body.appendChild(createTreeNode(null, val, false));
|
|
172
|
+
body.addEventListener('contextmenu', (e) => {
|
|
173
|
+
e.preventDefault();
|
|
174
|
+
showContextMenu(e, [
|
|
175
|
+
{ label: 'Copy Value', action: () => navigator.clipboard.writeText(JSON.stringify(val, null, 2)) },
|
|
176
|
+
{ label: 'Copy Key', action: () => navigator.clipboard.writeText(selected) },
|
|
177
|
+
]);
|
|
178
|
+
});
|
|
179
|
+
} else {
|
|
180
|
+
body.innerHTML = renderJSON(val);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function formatSize(bytes) {
|
|
185
|
+
if (bytes < 1024) return `${bytes}b`;
|
|
186
|
+
return `${(bytes/1024).toFixed(1)}kb`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
190
|
+
// REACT TREE PANEL
|
|
191
|
+
// ─────────────────────────────────────────────────────────────────────────────
|