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
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
// ─── Settings Panel ────────────────────────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
// ─── Theme helpers ───────────────────────────────────────────────────────────
|
|
4
|
+
function getStoredTheme() {
|
|
5
|
+
try { return localStorage.getItem('rn-debug-theme') || 'dark'; } catch { return 'dark'; }
|
|
6
|
+
}
|
|
7
|
+
function setStoredTheme(t) {
|
|
8
|
+
try { localStorage.setItem('rn-debug-theme', t); } catch {}
|
|
9
|
+
}
|
|
10
|
+
function getStoredFontSize() {
|
|
11
|
+
try { return parseInt(localStorage.getItem('rn-debug-fontsize')) || 12; } catch { return 12; }
|
|
12
|
+
}
|
|
13
|
+
function setStoredFontSize(s) {
|
|
14
|
+
try { localStorage.setItem('rn-debug-fontsize', String(s)); } catch {}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const FONT_FAMILIES = [
|
|
18
|
+
{ label: 'SF Mono', value: "'SFMono-Regular', 'SF Mono', monospace" },
|
|
19
|
+
{ label: 'Menlo', value: "Menlo, monospace" },
|
|
20
|
+
{ label: 'Monaco', value: "Monaco, monospace" },
|
|
21
|
+
{ label: 'Courier New', value: "'Courier New', Courier, monospace" },
|
|
22
|
+
{ label: 'System Mono', value: "monospace" },
|
|
23
|
+
];
|
|
24
|
+
function getStoredFontFamily() {
|
|
25
|
+
try {
|
|
26
|
+
const saved = localStorage.getItem('rn-debug-fontfamily');
|
|
27
|
+
// Reset if saved value was a removed font
|
|
28
|
+
if (saved && !FONT_FAMILIES.some(f => f.value === saved)) return FONT_FAMILIES[0].value;
|
|
29
|
+
return saved || FONT_FAMILIES[0].value;
|
|
30
|
+
} catch { return FONT_FAMILIES[0].value; }
|
|
31
|
+
}
|
|
32
|
+
function setStoredFontFamily(f) {
|
|
33
|
+
try { localStorage.setItem('rn-debug-fontfamily', f); } catch {}
|
|
34
|
+
}
|
|
35
|
+
function applyFontFamily(family) {
|
|
36
|
+
document.body.style.fontFamily = family;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ─── Hidden URLs (Network tab) ───────────────────────────────────────────────
|
|
40
|
+
function getHiddenURLs() {
|
|
41
|
+
try { return JSON.parse(localStorage.getItem('rn-debug-hidden-urls') || '[]'); } catch { return []; }
|
|
42
|
+
}
|
|
43
|
+
function setHiddenURLs(list) {
|
|
44
|
+
try { localStorage.setItem('rn-debug-hidden-urls', JSON.stringify(list)); } catch {}
|
|
45
|
+
}
|
|
46
|
+
function addHiddenURL(url) {
|
|
47
|
+
// Extract the base URL (without query params) as the pattern
|
|
48
|
+
const pattern = url.split('?')[0];
|
|
49
|
+
const list = getHiddenURLs();
|
|
50
|
+
if (!list.includes(pattern)) {
|
|
51
|
+
list.push(pattern);
|
|
52
|
+
setHiddenURLs(list);
|
|
53
|
+
}
|
|
54
|
+
_updateHiddenBadge();
|
|
55
|
+
}
|
|
56
|
+
function removeHiddenURL(pattern) {
|
|
57
|
+
const list = getHiddenURLs().filter(u => u !== pattern);
|
|
58
|
+
setHiddenURLs(list);
|
|
59
|
+
_updateHiddenBadge();
|
|
60
|
+
}
|
|
61
|
+
function isURLHidden(url) {
|
|
62
|
+
const hidden = getHiddenURLs();
|
|
63
|
+
if (!hidden.length) return false;
|
|
64
|
+
const base = url.split('?')[0];
|
|
65
|
+
return hidden.some(pattern => base === pattern || base.startsWith(pattern));
|
|
66
|
+
}
|
|
67
|
+
function _updateHiddenBadge() {
|
|
68
|
+
const btn = $('netHiddenBtn');
|
|
69
|
+
if (!btn) return;
|
|
70
|
+
const count = getHiddenURLs().length;
|
|
71
|
+
btn.textContent = count > 0 ? `Hidden (${count})` : 'Hidden';
|
|
72
|
+
btn.style.display = count > 0 ? '' : 'none';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─── Tab Visibility ──────────────────────────────────────────────────────────
|
|
76
|
+
const TAB_CONFIG = [
|
|
77
|
+
{ id: 'console', label: 'Console', icon: '🖥', essential: true },
|
|
78
|
+
{ id: 'network', label: 'Network', icon: '📡', essential: true },
|
|
79
|
+
{ id: 'redux', label: 'Redux', icon: '🔲', essential: false },
|
|
80
|
+
{ id: 'ga4', label: 'GA4 Events', icon: '📊', essential: false },
|
|
81
|
+
{ id: 'storage', label: 'AsyncStorage', icon: '💾', essential: false },
|
|
82
|
+
{ id: 'memory', label: 'Memory', icon: '🧠', essential: false, defaultHidden: true },
|
|
83
|
+
{ id: 'performance', label: 'Performance', icon: '⚡', essential: false, defaultHidden: true },
|
|
84
|
+
{ id: 'react', label: 'React Tree', icon: '⚛️', essential: false },
|
|
85
|
+
{ id: 'native', label: 'Native Logs', icon: '📱', essential: false, defaultHidden: true },
|
|
86
|
+
];
|
|
87
|
+
function getTabVisibility() {
|
|
88
|
+
try {
|
|
89
|
+
const saved = JSON.parse(localStorage.getItem('rn-debug-tab-visibility') || '{}');
|
|
90
|
+
const result = {};
|
|
91
|
+
TAB_CONFIG.forEach(t => { result[t.id] = saved[t.id] !== undefined ? saved[t.id] : !t.defaultHidden; });
|
|
92
|
+
return result;
|
|
93
|
+
} catch {
|
|
94
|
+
const result = {};
|
|
95
|
+
TAB_CONFIG.forEach(t => { result[t.id] = !t.defaultHidden; });
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function setTabVisibility(vis) {
|
|
100
|
+
try { localStorage.setItem('rn-debug-tab-visibility', JSON.stringify(vis)); } catch {}
|
|
101
|
+
}
|
|
102
|
+
function getTabOrder() {
|
|
103
|
+
try {
|
|
104
|
+
const saved = JSON.parse(localStorage.getItem('rn-debug-tab-order') || '[]');
|
|
105
|
+
if (saved.length) {
|
|
106
|
+
// Merge: keep saved order, append any new tabs not in saved list
|
|
107
|
+
const allIds = TAB_CONFIG.map(t => t.id);
|
|
108
|
+
const merged = saved.filter(id => allIds.includes(id));
|
|
109
|
+
allIds.forEach(id => { if (!merged.includes(id)) merged.push(id); });
|
|
110
|
+
return merged;
|
|
111
|
+
}
|
|
112
|
+
} catch {}
|
|
113
|
+
return TAB_CONFIG.map(t => t.id);
|
|
114
|
+
}
|
|
115
|
+
function setTabOrder(order) {
|
|
116
|
+
try { localStorage.setItem('rn-debug-tab-order', JSON.stringify(order)); } catch {}
|
|
117
|
+
}
|
|
118
|
+
function applyTabVisibility() {
|
|
119
|
+
const vis = getTabVisibility();
|
|
120
|
+
const order = getTabOrder();
|
|
121
|
+
const nav = $('sidebar');
|
|
122
|
+
if (!nav) return;
|
|
123
|
+
// Reorder nav buttons according to saved order + hide disabled ones
|
|
124
|
+
// Settings button always stays last
|
|
125
|
+
const settingsBtn = nav.querySelector('.nav-btn[data-panel="settings"]');
|
|
126
|
+
const spacer = nav.querySelector('.nav-spacer');
|
|
127
|
+
const anchor = spacer || settingsBtn; // insert before spacer or settings
|
|
128
|
+
order.forEach(tabId => {
|
|
129
|
+
const btn = nav.querySelector(`.nav-btn[data-panel="${tabId}"]`);
|
|
130
|
+
if (btn) {
|
|
131
|
+
btn.style.display = vis[tabId] ? '' : 'none';
|
|
132
|
+
nav.insertBefore(btn, anchor);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
// If active panel is now hidden, switch to first visible
|
|
136
|
+
if (!vis[state.activePanel]) {
|
|
137
|
+
const first = order.find(id => vis[id]);
|
|
138
|
+
if (first) switchPanel(first);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function isTabEnabled(tabId) {
|
|
142
|
+
return getTabVisibility()[tabId] !== false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function _buildTabVisGrid() {
|
|
146
|
+
const container = $('tabVisibilityGrid');
|
|
147
|
+
if (!container) return;
|
|
148
|
+
container.innerHTML = '';
|
|
149
|
+
const vis = getTabVisibility();
|
|
150
|
+
const order = getTabOrder();
|
|
151
|
+
let dragSrc = null;
|
|
152
|
+
|
|
153
|
+
order.forEach(tabId => {
|
|
154
|
+
const t = TAB_CONFIG.find(c => c.id === tabId);
|
|
155
|
+
if (!t) return;
|
|
156
|
+
|
|
157
|
+
const item = document.createElement('div');
|
|
158
|
+
item.className = `tab-vis-item ${vis[t.id] ? 'active' : 'inactive'}`;
|
|
159
|
+
item.dataset.tab = t.id;
|
|
160
|
+
item.draggable = true;
|
|
161
|
+
|
|
162
|
+
// Drag handle
|
|
163
|
+
const drag = document.createElement('span');
|
|
164
|
+
drag.className = 'tab-vis-drag';
|
|
165
|
+
drag.textContent = '⠿';
|
|
166
|
+
item.appendChild(drag);
|
|
167
|
+
|
|
168
|
+
// Checkbox
|
|
169
|
+
const check = document.createElement('input');
|
|
170
|
+
check.type = 'checkbox';
|
|
171
|
+
check.className = 'tab-vis-check';
|
|
172
|
+
check.checked = vis[t.id];
|
|
173
|
+
if (t.essential) check.disabled = true;
|
|
174
|
+
check.addEventListener('change', () => {
|
|
175
|
+
const v = getTabVisibility();
|
|
176
|
+
v[t.id] = check.checked;
|
|
177
|
+
setTabVisibility(v);
|
|
178
|
+
applyTabVisibility();
|
|
179
|
+
item.classList.toggle('active', check.checked);
|
|
180
|
+
item.classList.toggle('inactive', !check.checked);
|
|
181
|
+
});
|
|
182
|
+
item.appendChild(check);
|
|
183
|
+
|
|
184
|
+
// Icon + label
|
|
185
|
+
const icon = document.createElement('span');
|
|
186
|
+
icon.className = 'tab-vis-icon';
|
|
187
|
+
icon.textContent = t.icon;
|
|
188
|
+
item.appendChild(icon);
|
|
189
|
+
|
|
190
|
+
const label = document.createElement('span');
|
|
191
|
+
label.className = 'tab-vis-label';
|
|
192
|
+
label.textContent = t.label;
|
|
193
|
+
item.appendChild(label);
|
|
194
|
+
|
|
195
|
+
if (t.essential) {
|
|
196
|
+
const req = document.createElement('span');
|
|
197
|
+
req.className = 'tab-vis-required';
|
|
198
|
+
req.textContent = 'Required';
|
|
199
|
+
item.appendChild(req);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Drag events
|
|
203
|
+
item.addEventListener('dragstart', (e) => {
|
|
204
|
+
dragSrc = item;
|
|
205
|
+
item.classList.add('dragging');
|
|
206
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
207
|
+
});
|
|
208
|
+
item.addEventListener('dragend', () => {
|
|
209
|
+
item.classList.remove('dragging');
|
|
210
|
+
container.querySelectorAll('.tab-vis-item').forEach(el => el.classList.remove('drag-over'));
|
|
211
|
+
dragSrc = null;
|
|
212
|
+
});
|
|
213
|
+
item.addEventListener('dragover', (e) => {
|
|
214
|
+
e.preventDefault();
|
|
215
|
+
e.dataTransfer.dropEffect = 'move';
|
|
216
|
+
if (dragSrc && dragSrc !== item) item.classList.add('drag-over');
|
|
217
|
+
});
|
|
218
|
+
item.addEventListener('dragleave', () => {
|
|
219
|
+
item.classList.remove('drag-over');
|
|
220
|
+
});
|
|
221
|
+
item.addEventListener('drop', (e) => {
|
|
222
|
+
e.preventDefault();
|
|
223
|
+
item.classList.remove('drag-over');
|
|
224
|
+
if (!dragSrc || dragSrc === item) return;
|
|
225
|
+
// Reorder: move dragSrc before or after this item
|
|
226
|
+
const items = [...container.querySelectorAll('.tab-vis-item')];
|
|
227
|
+
const fromIdx = items.indexOf(dragSrc);
|
|
228
|
+
const toIdx = items.indexOf(item);
|
|
229
|
+
if (fromIdx < toIdx) {
|
|
230
|
+
container.insertBefore(dragSrc, item.nextSibling);
|
|
231
|
+
} else {
|
|
232
|
+
container.insertBefore(dragSrc, item);
|
|
233
|
+
}
|
|
234
|
+
// Save new order
|
|
235
|
+
const newOrder = [...container.querySelectorAll('.tab-vis-item')].map(el => el.dataset.tab);
|
|
236
|
+
setTabOrder(newOrder);
|
|
237
|
+
applyTabVisibility();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
container.appendChild(item);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function getStoredAppName() {
|
|
245
|
+
try { return localStorage.getItem('rn-debug-appname') || 'ReactoRadar'; } catch { return 'ReactoRadar'; }
|
|
246
|
+
}
|
|
247
|
+
function setStoredAppName(n) {
|
|
248
|
+
try { localStorage.setItem('rn-debug-appname', n); } catch {}
|
|
249
|
+
}
|
|
250
|
+
function getStoredMetroPort() {
|
|
251
|
+
try { return parseInt(localStorage.getItem('rn-debug-metro-port')) || 8081; } catch { return 8081; }
|
|
252
|
+
}
|
|
253
|
+
function setStoredMetroPort(p) {
|
|
254
|
+
try { localStorage.setItem('rn-debug-metro-port', String(p)); } catch {}
|
|
255
|
+
}
|
|
256
|
+
function applyAppName(name) {
|
|
257
|
+
const logo = document.querySelector('.logo');
|
|
258
|
+
if (logo) {
|
|
259
|
+
// Split name — first part normal, last word in accent span
|
|
260
|
+
const words = name.split(/(?=[A-Z])/);
|
|
261
|
+
if (words.length >= 2) {
|
|
262
|
+
logo.innerHTML = words.slice(0, -1).join('') + '<span>' + words[words.length - 1] + '</span>';
|
|
263
|
+
} else {
|
|
264
|
+
logo.textContent = name;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
document.title = name;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function applyTheme(theme) {
|
|
271
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
272
|
+
// Tell main process (light themes need light nativeTheme for window chrome)
|
|
273
|
+
const isLight = ['light', 'solarized-light'].includes(theme);
|
|
274
|
+
window.electronAPI?.setTheme(isLight ? 'light' : 'dark');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function applyFontSize(size) {
|
|
278
|
+
document.documentElement.style.setProperty('--app-font-size', size + 'px');
|
|
279
|
+
document.body.style.fontSize = size + 'px';
|
|
280
|
+
// Inject/update a <style> tag so ALL current and future elements get the size
|
|
281
|
+
let styleEl = document.getElementById('dynamic-font-size');
|
|
282
|
+
if (!styleEl) {
|
|
283
|
+
styleEl = document.createElement('style');
|
|
284
|
+
styleEl.id = 'dynamic-font-size';
|
|
285
|
+
document.head.appendChild(styleEl);
|
|
286
|
+
}
|
|
287
|
+
styleEl.textContent = `
|
|
288
|
+
.log-preview, .log-body, .log-text, .log-caller-inline,
|
|
289
|
+
.net-cell, .net-cell-name, .net-type, .net-initiator, .net-size, .net-time, .net-status,
|
|
290
|
+
.detail-content, .kv-val, .kv-key,
|
|
291
|
+
.rdx-type, .rdx-entry-detail, .rdx-store-key-label,
|
|
292
|
+
.storage-value-body, .storage-key-row,
|
|
293
|
+
.sources-code, .source-line-code,
|
|
294
|
+
.ov-leaf, .ov-key, .ov-preview, .ov-str, .ov-num, .ov-bool, .ov-null, .ov-undef,
|
|
295
|
+
.perf-meter-label,
|
|
296
|
+
.settings-label, .settings-hint {
|
|
297
|
+
font-size: ${size}px !important;
|
|
298
|
+
}
|
|
299
|
+
`;
|
|
300
|
+
const display = $('fontSizeDisplay');
|
|
301
|
+
if (display) display.textContent = size + 'px';
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function initSettingsPanel() {
|
|
305
|
+
const panel = $('panel-settings');
|
|
306
|
+
const current = getStoredTheme();
|
|
307
|
+
const currentSize = getStoredFontSize();
|
|
308
|
+
panel.innerHTML = `
|
|
309
|
+
<div class="panel-toolbar">
|
|
310
|
+
<span class="panel-label">Settings</span>
|
|
311
|
+
</div>
|
|
312
|
+
<div class="scroll-area">
|
|
313
|
+
<div class="settings-two-col">
|
|
314
|
+
<div class="settings-col-left">
|
|
315
|
+
<div class="settings-section">
|
|
316
|
+
<div class="settings-section-title">Appearance</div>
|
|
317
|
+
<div class="settings-row" style="flex-direction:column;align-items:flex-start;gap:8px">
|
|
318
|
+
<div>
|
|
319
|
+
<div class="settings-label">Theme</div>
|
|
320
|
+
<div class="settings-hint">Choose a color theme</div>
|
|
321
|
+
</div>
|
|
322
|
+
<div class="theme-grid" id="themeSwitcher"></div>
|
|
323
|
+
</div>
|
|
324
|
+
<div class="settings-row">
|
|
325
|
+
<div>
|
|
326
|
+
<div class="settings-label">Font Size</div>
|
|
327
|
+
<div class="settings-hint">Adjust text size</div>
|
|
328
|
+
</div>
|
|
329
|
+
<div class="font-size-control">
|
|
330
|
+
<button class="font-size-btn" id="fontSizeDown">A-</button>
|
|
331
|
+
<span class="font-size-display" id="fontSizeDisplay">${currentSize}px</span>
|
|
332
|
+
<button class="font-size-btn" id="fontSizeUp">A+</button>
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
<div class="settings-row">
|
|
336
|
+
<div>
|
|
337
|
+
<div class="settings-label">Font Family</div>
|
|
338
|
+
</div>
|
|
339
|
+
<select id="fontFamilySelect" class="net-throttle-select" style="width:150px">
|
|
340
|
+
${FONT_FAMILIES.map(f => `<option value="${esc(f.value)}" ${f.value === getStoredFontFamily() ? 'selected' : ''}>${esc(f.label)}</option>`).join('')}
|
|
341
|
+
</select>
|
|
342
|
+
</div>
|
|
343
|
+
<div class="settings-row">
|
|
344
|
+
<div>
|
|
345
|
+
<div class="settings-label">App Name</div>
|
|
346
|
+
</div>
|
|
347
|
+
<div style="display:flex;align-items:center;gap:6px">
|
|
348
|
+
<input id="appNameInput" class="net-search-input" style="width:120px;text-align:center" value="${getStoredAppName()}" />
|
|
349
|
+
<button class="font-size-btn" id="appNameReset" title="Reset">Reset</button>
|
|
350
|
+
</div>
|
|
351
|
+
</div>
|
|
352
|
+
<div class="settings-row">
|
|
353
|
+
<div>
|
|
354
|
+
<div class="settings-label">Toast Notifications</div>
|
|
355
|
+
<div class="settings-hint">Show alerts for API errors and slow requests</div>
|
|
356
|
+
</div>
|
|
357
|
+
<label class="toggle-label" for="toastToggle">
|
|
358
|
+
<input type="checkbox" id="toastToggle" class="toggle-input" ${getToastsEnabled() ? 'checked' : ''} />
|
|
359
|
+
<span class="toggle-slider"></span>
|
|
360
|
+
</label>
|
|
361
|
+
</div>
|
|
362
|
+
</div>
|
|
363
|
+
<div class="settings-section">
|
|
364
|
+
<div class="settings-section-title">Connection</div>
|
|
365
|
+
<div class="settings-row">
|
|
366
|
+
<div>
|
|
367
|
+
<div class="settings-label">Bridge Ports</div>
|
|
368
|
+
<div class="settings-hint">Redux :9090 · Storage :9091 · Network :9092</div>
|
|
369
|
+
</div>
|
|
370
|
+
</div>
|
|
371
|
+
<div class="settings-row">
|
|
372
|
+
<div>
|
|
373
|
+
<div class="settings-label">Metro Port</div>
|
|
374
|
+
</div>
|
|
375
|
+
<input id="metroPortInput" type="number" class="net-search-input" style="width:70px;text-align:center" value="${getStoredMetroPort()}" />
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
<div class="settings-section">
|
|
379
|
+
<div class="settings-section-title">About</div>
|
|
380
|
+
<div class="settings-about">
|
|
381
|
+
<div class="about-name" id="aboutAppName">${getStoredAppName()}</div>
|
|
382
|
+
<div class="about-version" id="aboutVersion">v${state._appVersion || '...'}</div>
|
|
383
|
+
<div class="about-desc">Standalone macOS debugger for React Native.<br/>Supports Hermes, New Arch, and RN 0.74+.</div>
|
|
384
|
+
<div class="about-links" style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap">
|
|
385
|
+
<span class="about-link" id="linkGithub">GitHub</span>
|
|
386
|
+
<span class="about-link" id="linkDocs">Docs</span>
|
|
387
|
+
<span class="about-link" id="linkLinkedIn">LinkedIn</span>
|
|
388
|
+
</div>
|
|
389
|
+
<div style="margin-top:12px;text-align:center">
|
|
390
|
+
<button class="support-btn" id="linkSupport" title="Support ReactoRadar development">☕ Support this project</button>
|
|
391
|
+
</div>
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
<div class="settings-col-right">
|
|
396
|
+
<div class="settings-section">
|
|
397
|
+
<div class="settings-section-title">Panels</div>
|
|
398
|
+
<div class="settings-hint" style="margin-bottom:8px">Show/hide tabs and drag to reorder. Disabled tabs save memory.</div>
|
|
399
|
+
<div class="tab-visibility-grid" id="tabVisibilityGrid"></div>
|
|
400
|
+
</div>
|
|
401
|
+
<div class="settings-section">
|
|
402
|
+
<div class="settings-section-title">Keyboard Shortcuts</div>
|
|
403
|
+
<div class="settings-shortcut-grid">
|
|
404
|
+
<span class="sc-key">⌘K</span><span class="sc-label">Clear All</span>
|
|
405
|
+
<span class="sc-key">⌘D</span><span class="sc-label">JS Debugger</span>
|
|
406
|
+
<span class="sc-key">⌘R</span><span class="sc-label">React DevTools</span>
|
|
407
|
+
<span class="sc-key">⌘⇧T</span><span class="sc-label">Toggle Theme</span>
|
|
408
|
+
<span class="sc-key">⌘F</span><span class="sc-label">Find</span>
|
|
409
|
+
<span class="sc-key">⌘1–9</span><span class="sc-label">Switch Panels</span>
|
|
410
|
+
<span class="sc-key">⌘+/−</span><span class="sc-label">Zoom</span>
|
|
411
|
+
</div>
|
|
412
|
+
</div>
|
|
413
|
+
<div class="settings-section">
|
|
414
|
+
<div class="settings-section-title">Quick Start</div>
|
|
415
|
+
<div class="settings-hint" style="line-height:1.8;font-size:11px">
|
|
416
|
+
<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/>
|
|
417
|
+
<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/>
|
|
418
|
+
<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/>
|
|
419
|
+
<b style="color:var(--text)">4.</b> Console, Network, Redux auto-connect<br/>
|
|
420
|
+
<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
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
<div class="settings-section">
|
|
424
|
+
<div class="settings-section-title">Version History</div>
|
|
425
|
+
<div class="settings-hint" style="margin-bottom:4px">Roll back to a previous version if you notice issues.</div>
|
|
426
|
+
<div class="settings-hint rollback-steps" id="rollbackSteps" style="margin-bottom:10px;line-height:1.8;font-size:10px">
|
|
427
|
+
<b style="color:var(--text)">How to roll back:</b><br/>
|
|
428
|
+
<span id="rollbackDmgSteps" style="display:none">
|
|
429
|
+
<b style="color:var(--text)">1.</b> Click <b>Download</b> on the version you want<br/>
|
|
430
|
+
<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/>
|
|
431
|
+
<b style="color:var(--text)">3.</b> Drag the app to Applications (replace existing)<br/>
|
|
432
|
+
<b style="color:var(--text)">4.</b> Relaunch ReactoRadar
|
|
433
|
+
</span>
|
|
434
|
+
<span id="rollbackNpmSteps" style="display:none">
|
|
435
|
+
<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/>
|
|
436
|
+
<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/>
|
|
437
|
+
<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
|
|
438
|
+
</span>
|
|
439
|
+
</div>
|
|
440
|
+
<div id="versionHistoryList" class="version-history-list">
|
|
441
|
+
<div style="color:var(--text-dim);font-size:11px;padding:12px;text-align:center">Loading versions...</div>
|
|
442
|
+
</div>
|
|
443
|
+
</div>
|
|
444
|
+
</div>
|
|
445
|
+
</div>
|
|
446
|
+
</div>`;
|
|
447
|
+
|
|
448
|
+
// Build theme cards
|
|
449
|
+
const themes = [
|
|
450
|
+
{ id: 'dark', name: 'Dark', colors: ['#0d0e11','#4facff','#3dd68c','#ff5e72'] },
|
|
451
|
+
{ id: 'light', name: 'Light', colors: ['#f5f6f8','#0969da','#1a7f37','#cf222e'] },
|
|
452
|
+
{ id: 'monokai', name: 'Monokai', colors: ['#272822','#66d9ef','#a6e22e','#f92672'] },
|
|
453
|
+
{ id: 'dracula', name: 'Dracula', colors: ['#282a36','#8be9fd','#50fa7b','#ff5555'] },
|
|
454
|
+
{ id: 'solarized-dark', name: 'Solarized Dark', colors: ['#002b36','#268bd2','#859900','#dc322f'] },
|
|
455
|
+
{ id: 'solarized-light', name: 'Solarized Light', colors: ['#fdf6e3','#268bd2','#859900','#dc322f'] },
|
|
456
|
+
{ id: 'nord', name: 'Nord', colors: ['#2e3440','#88c0d0','#a3be8c','#bf616a'] },
|
|
457
|
+
{ id: 'github-dark', name: 'GitHub Dark', colors: ['#0d1117','#58a6ff','#3fb950','#f85149'] },
|
|
458
|
+
{ id: 'one-dark', name: 'One Dark', colors: ['#282c34','#61afef','#98c379','#e06c75'] },
|
|
459
|
+
];
|
|
460
|
+
// Tab visibility + drag reorder
|
|
461
|
+
_buildTabVisGrid();
|
|
462
|
+
|
|
463
|
+
const grid = $('themeSwitcher');
|
|
464
|
+
themes.forEach(t => {
|
|
465
|
+
const btn = document.createElement('button');
|
|
466
|
+
btn.className = 'theme-card' + (current === t.id ? ' active' : '');
|
|
467
|
+
btn.dataset.theme = t.id;
|
|
468
|
+
btn.innerHTML = '<div class="theme-preview" style="background:' + t.colors[0] + '">' +
|
|
469
|
+
'<span style="background:' + t.colors[1] + '"></span>' +
|
|
470
|
+
'<span style="background:' + t.colors[2] + '"></span>' +
|
|
471
|
+
'<span style="background:' + t.colors[3] + '"></span>' +
|
|
472
|
+
'</div><div class="theme-name">' + t.name + '</div>';
|
|
473
|
+
grid.appendChild(btn);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
// Theme switcher
|
|
477
|
+
$('themeSwitcher').addEventListener('click', (e) => {
|
|
478
|
+
const btn = e.target.closest('.theme-card');
|
|
479
|
+
if (!btn) return;
|
|
480
|
+
const theme = btn.dataset.theme;
|
|
481
|
+
document.querySelectorAll('#themeSwitcher .theme-card').forEach(b => b.classList.remove('active'));
|
|
482
|
+
btn.classList.add('active');
|
|
483
|
+
setStoredTheme(theme);
|
|
484
|
+
applyTheme(theme);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// About links
|
|
488
|
+
$('linkGithub')?.addEventListener('click', () => {
|
|
489
|
+
window.electronAPI?.openExternal('https://github.com/sharanagouda/reactoradar');
|
|
490
|
+
});
|
|
491
|
+
$('linkDocs')?.addEventListener('click', () => {
|
|
492
|
+
window.electronAPI?.openExternal('https://github.com/sharanagouda/reactoradar#readme');
|
|
493
|
+
});
|
|
494
|
+
$('linkLinkedIn')?.addEventListener('click', () => {
|
|
495
|
+
window.electronAPI?.openExternal('https://www.linkedin.com/in/sharanagoudamk/');
|
|
496
|
+
});
|
|
497
|
+
$('linkSupport')?.addEventListener('click', () => {
|
|
498
|
+
window.electronAPI?.openExternal('https://razorpay.me/@reactoradar');
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
// App name
|
|
502
|
+
$('appNameInput').addEventListener('change', (e) => {
|
|
503
|
+
const name = e.target.value.trim() || 'ReactoRadar';
|
|
504
|
+
setStoredAppName(name);
|
|
505
|
+
applyAppName(name);
|
|
506
|
+
});
|
|
507
|
+
$('appNameReset').addEventListener('click', () => {
|
|
508
|
+
setStoredAppName('ReactoRadar');
|
|
509
|
+
$('appNameInput').value = 'ReactoRadar';
|
|
510
|
+
applyAppName('ReactoRadar');
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
// Metro Port
|
|
514
|
+
$('metroPortInput')?.addEventListener('change', (e) => {
|
|
515
|
+
let port = parseInt(e.target.value.trim());
|
|
516
|
+
if (isNaN(port) || port < 1024 || port > 65535) port = 8081;
|
|
517
|
+
e.target.value = port;
|
|
518
|
+
setStoredMetroPort(port);
|
|
519
|
+
window.electronAPI?.setMetroPort(port);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
// Font size controls
|
|
523
|
+
$('fontSizeDown').addEventListener('click', () => {
|
|
524
|
+
let size = getStoredFontSize();
|
|
525
|
+
size = Math.max(8, size - 1);
|
|
526
|
+
setStoredFontSize(size);
|
|
527
|
+
applyFontSize(size);
|
|
528
|
+
});
|
|
529
|
+
$('fontSizeUp').addEventListener('click', () => {
|
|
530
|
+
let size = getStoredFontSize();
|
|
531
|
+
size = Math.min(20, size + 1);
|
|
532
|
+
setStoredFontSize(size);
|
|
533
|
+
applyFontSize(size);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
// Font family
|
|
537
|
+
$('fontFamilySelect')?.addEventListener('change', (e) => {
|
|
538
|
+
const family = e.target.value;
|
|
539
|
+
setStoredFontFamily(family);
|
|
540
|
+
applyFontFamily(family);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
// Toast toggle
|
|
544
|
+
$('toastToggle')?.addEventListener('change', (e) => {
|
|
545
|
+
setToastsEnabled(e.target.checked);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
// Apply update banner if update info arrived before settings panel was created
|
|
549
|
+
_applyUpdateBanner();
|
|
550
|
+
|
|
551
|
+
// Fetch and render version history for rollback
|
|
552
|
+
_loadVersionHistory();
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function _loadVersionHistory() {
|
|
556
|
+
const container = $('versionHistoryList');
|
|
557
|
+
if (!container) return;
|
|
558
|
+
if (!window.electronAPI || typeof window.electronAPI.fetchReleases !== 'function') {
|
|
559
|
+
container.innerHTML = '<div style="color:var(--text-dim);font-size:11px;padding:12px;text-align:center">Version history not available.</div>';
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Show appropriate rollback steps based on install type
|
|
564
|
+
const isPackaged = !!state._isPackaged;
|
|
565
|
+
const dmgSteps = $('rollbackDmgSteps');
|
|
566
|
+
const npmSteps = $('rollbackNpmSteps');
|
|
567
|
+
if (dmgSteps) dmgSteps.style.display = isPackaged ? '' : 'none';
|
|
568
|
+
if (npmSteps) npmSteps.style.display = isPackaged ? 'none' : '';
|
|
569
|
+
|
|
570
|
+
window.electronAPI.fetchReleases().then(releases => {
|
|
571
|
+
if (!Array.isArray(releases) || releases.length === 0) {
|
|
572
|
+
container.innerHTML = '<div style="color:var(--text-dim);font-size:11px;padding:12px;text-align:center">Could not load versions.</div>';
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const currentVersion = state._appVersion || '';
|
|
577
|
+
container.innerHTML = '';
|
|
578
|
+
|
|
579
|
+
releases.forEach(r => {
|
|
580
|
+
if (!r || !r.version) return; // skip malformed entries
|
|
581
|
+
|
|
582
|
+
const isCurrent = r.version === currentVersion;
|
|
583
|
+
const row = document.createElement('div');
|
|
584
|
+
row.className = 'version-row' + (isCurrent ? ' version-current' : '');
|
|
585
|
+
|
|
586
|
+
// Safe date formatting
|
|
587
|
+
let dateStr = '';
|
|
588
|
+
if (r.date) {
|
|
589
|
+
try {
|
|
590
|
+
const d = new Date(r.date);
|
|
591
|
+
if (!isNaN(d.getTime())) {
|
|
592
|
+
dateStr = d.toLocaleDateString('en', { year: 'numeric', month: 'short', day: 'numeric' });
|
|
593
|
+
}
|
|
594
|
+
} catch { /* skip bad date */ }
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Build action buttons based on install type
|
|
598
|
+
let actionHtml = '';
|
|
599
|
+
if (isCurrent) {
|
|
600
|
+
actionHtml = '<span class="version-installed">Installed</span>';
|
|
601
|
+
} else if (isPackaged) {
|
|
602
|
+
// Show download dropdown with .dmg and .zip links
|
|
603
|
+
actionHtml = '<div class="version-dl-wrap"><button class="version-install-btn" title="Download this version">Download ▾</button></div>';
|
|
604
|
+
} else {
|
|
605
|
+
actionHtml = `<button class="version-npm-btn" title="Copy npm install command">npx @${esc(r.version)}</button>`;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
row.innerHTML = `
|
|
609
|
+
<div class="version-info">
|
|
610
|
+
<span class="version-tag">v${esc(r.version)}${r.prerelease ? ' <span class="version-pre">pre</span>' : ''}${isCurrent ? ' <span class="version-badge">current</span>' : ''}</span>
|
|
611
|
+
<span class="version-date">${esc(dateStr)}</span>
|
|
612
|
+
</div>
|
|
613
|
+
<div class="version-actions">
|
|
614
|
+
${actionHtml}
|
|
615
|
+
<button class="version-notes-btn" title="View release notes">Notes</button>
|
|
616
|
+
</div>`;
|
|
617
|
+
|
|
618
|
+
// DMG/ZIP download dropdown
|
|
619
|
+
const dlWrap = row.querySelector('.version-dl-wrap');
|
|
620
|
+
const installBtn = row.querySelector('.version-install-btn');
|
|
621
|
+
if (installBtn && dlWrap) {
|
|
622
|
+
installBtn.addEventListener('click', (e) => {
|
|
623
|
+
e.stopPropagation();
|
|
624
|
+
// Remove any existing dropdown
|
|
625
|
+
document.querySelectorAll('.version-dl-menu').forEach(m => m.remove());
|
|
626
|
+
|
|
627
|
+
const menu = document.createElement('div');
|
|
628
|
+
menu.className = 'version-dl-menu';
|
|
629
|
+
|
|
630
|
+
// .dmg link
|
|
631
|
+
if (r.dmgUrl) {
|
|
632
|
+
const dmgItem = document.createElement('div');
|
|
633
|
+
dmgItem.className = 'version-dl-item';
|
|
634
|
+
dmgItem.innerHTML = `<span class="version-dl-icon">💿</span><div><div class="version-dl-name">.dmg Installer</div><div class="version-dl-hint">macOS installer (Apple Silicon)</div></div>`;
|
|
635
|
+
dmgItem.addEventListener('click', () => { window.electronAPI.openExternal(r.dmgUrl); menu.remove(); });
|
|
636
|
+
menu.appendChild(dmgItem);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// .zip link
|
|
640
|
+
if (r.zipUrl) {
|
|
641
|
+
const zipItem = document.createElement('div');
|
|
642
|
+
zipItem.className = 'version-dl-item';
|
|
643
|
+
zipItem.innerHTML = `<span class="version-dl-icon">📦</span><div><div class="version-dl-name">.zip Archive</div><div class="version-dl-hint">Portable zip archive</div></div>`;
|
|
644
|
+
zipItem.addEventListener('click', () => { window.electronAPI.openExternal(r.zipUrl); menu.remove(); });
|
|
645
|
+
menu.appendChild(zipItem);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// GitHub release page fallback
|
|
649
|
+
if (r.htmlUrl) {
|
|
650
|
+
const ghItem = document.createElement('div');
|
|
651
|
+
ghItem.className = 'version-dl-item';
|
|
652
|
+
ghItem.innerHTML = `<span class="version-dl-icon">🔗</span><div><div class="version-dl-name">GitHub Release</div><div class="version-dl-hint">View all assets on GitHub</div></div>`;
|
|
653
|
+
ghItem.addEventListener('click', () => { window.electronAPI.openExternal(r.htmlUrl); menu.remove(); });
|
|
654
|
+
menu.appendChild(ghItem);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// No assets fallback
|
|
658
|
+
if (!r.dmgUrl && !r.zipUrl && !r.htmlUrl) {
|
|
659
|
+
menu.innerHTML = '<div style="padding:8px 12px;color:var(--text-dim);font-size:10px">No downloads available</div>';
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
dlWrap.appendChild(menu);
|
|
663
|
+
|
|
664
|
+
// Close on outside click
|
|
665
|
+
setTimeout(() => {
|
|
666
|
+
const close = (ev) => { if (!menu.contains(ev.target) && ev.target !== installBtn) { menu.remove(); document.removeEventListener('click', close); } };
|
|
667
|
+
document.addEventListener('click', close);
|
|
668
|
+
}, 0);
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// NPM copy button — copies the npx command to clipboard
|
|
673
|
+
const npmBtn = row.querySelector('.version-npm-btn');
|
|
674
|
+
if (npmBtn) {
|
|
675
|
+
npmBtn.addEventListener('click', () => {
|
|
676
|
+
const cmd = `npx reactoradar@${r.version}`;
|
|
677
|
+
navigator.clipboard.writeText(cmd).then(() => {
|
|
678
|
+
const orig = npmBtn.textContent;
|
|
679
|
+
npmBtn.textContent = 'Copied!';
|
|
680
|
+
npmBtn.style.color = 'var(--green)';
|
|
681
|
+
setTimeout(() => { npmBtn.textContent = orig; npmBtn.style.color = ''; }, 2000);
|
|
682
|
+
}).catch(() => {});
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Notes button — show changelog in modal
|
|
687
|
+
const notesBtn = row.querySelector('.version-notes-btn');
|
|
688
|
+
if (notesBtn) {
|
|
689
|
+
notesBtn.addEventListener('click', () => {
|
|
690
|
+
if (r.version && typeof _showChangelog === 'function') {
|
|
691
|
+
_showChangelog(r.version);
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
container.appendChild(row);
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
// If no rows were rendered (all entries were malformed)
|
|
700
|
+
if (container.children.length === 0) {
|
|
701
|
+
container.innerHTML = '<div style="color:var(--text-dim);font-size:11px;padding:12px;text-align:center">No versions found.</div>';
|
|
702
|
+
}
|
|
703
|
+
}).catch(() => {
|
|
704
|
+
if (container) {
|
|
705
|
+
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>';
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// ─── Update Banner ───────────────────────────────────────────────────────────
|
|
711
|
+
// Reusable — called from IPC handler AND from initSettingsPanel
|
|
712
|
+
function _applyUpdateBanner() {
|
|
713
|
+
const info = state._updateAvailable;
|
|
714
|
+
if (!info) return;
|
|
715
|
+
const { current, latest, autoUpdate } = info;
|
|
716
|
+
const downloaded = state._updateDownloaded;
|
|
717
|
+
const targetVersion = downloaded || latest;
|
|
718
|
+
|
|
719
|
+
const el = $('aboutVersion');
|
|
720
|
+
if (el) {
|
|
721
|
+
if (downloaded) {
|
|
722
|
+
el.innerHTML = `v${current} <span style="color:var(--green);font-size:10px;margin-left:6px">v${downloaded} ready to install</span>`;
|
|
723
|
+
} else {
|
|
724
|
+
el.innerHTML = `v${current} <span style="color:var(--green);font-size:10px;margin-left:6px">v${latest} available</span>`;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Remove old buttons if state changed
|
|
729
|
+
const oldBtn = $('updateBtn');
|
|
730
|
+
if (oldBtn && downloaded && !oldBtn.dataset.isRestart) oldBtn.parentElement?.remove();
|
|
731
|
+
const oldChangelog = $('changelogBtn');
|
|
732
|
+
if (oldChangelog && downloaded && !oldChangelog.dataset.updated) oldChangelog.remove();
|
|
733
|
+
|
|
734
|
+
const aboutEl = document.querySelector('.settings-about');
|
|
735
|
+
if (!aboutEl) return;
|
|
736
|
+
|
|
737
|
+
// Add "What's new?" link
|
|
738
|
+
if (!$('changelogBtn')) {
|
|
739
|
+
const link = document.createElement('div');
|
|
740
|
+
link.style.cssText = 'margin-top:6px;text-align:center';
|
|
741
|
+
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>`;
|
|
742
|
+
aboutEl.appendChild(link);
|
|
743
|
+
$('changelogBtn')?.addEventListener('click', () => _showChangelog(targetVersion));
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Add update button
|
|
747
|
+
if (!$('updateBtn')) {
|
|
748
|
+
const btn = document.createElement('div');
|
|
749
|
+
btn.style.cssText = 'margin-top:8px;text-align:center';
|
|
750
|
+
if (downloaded) {
|
|
751
|
+
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>';
|
|
752
|
+
aboutEl.appendChild(btn);
|
|
753
|
+
$('updateBtn')?.addEventListener('click', () => window.electronAPI?.installUpdate());
|
|
754
|
+
} else if (autoUpdate) {
|
|
755
|
+
btn.innerHTML = '<button id="updateBtn" class="tb-btn" style="font-size:11px;padding:6px 16px;opacity:0.7" disabled>Downloading v' + latest + '...</button>';
|
|
756
|
+
aboutEl.appendChild(btn);
|
|
757
|
+
} else {
|
|
758
|
+
btn.innerHTML = '<button id="updateBtn" class="tb-btn primary" style="font-size:11px;padding:6px 16px">Download v' + latest + '</button>';
|
|
759
|
+
aboutEl.appendChild(btn);
|
|
760
|
+
$('updateBtn')?.addEventListener('click', () => window.electronAPI?.openExternal('https://github.com/sharanagouda/reactoradar/releases'));
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
async function _showChangelog(version) {
|
|
766
|
+
if (!version || typeof version !== 'string') return;
|
|
767
|
+
|
|
768
|
+
// Remove existing modal
|
|
769
|
+
$('changelogModal')?.remove();
|
|
770
|
+
|
|
771
|
+
const safeVersion = esc(version);
|
|
772
|
+
const modal = document.createElement('div');
|
|
773
|
+
modal.id = 'changelogModal';
|
|
774
|
+
modal.className = 'changelog-modal-overlay';
|
|
775
|
+
modal.innerHTML = `
|
|
776
|
+
<div class="changelog-modal">
|
|
777
|
+
<div class="changelog-header">
|
|
778
|
+
<span class="changelog-title">What's New in v${safeVersion}</span>
|
|
779
|
+
<button class="changelog-close" id="changelogClose">×</button>
|
|
780
|
+
</div>
|
|
781
|
+
<div class="changelog-body" id="changelogBody">
|
|
782
|
+
<div style="color:var(--text-dim);padding:20px;text-align:center">Loading release notes...</div>
|
|
783
|
+
</div>
|
|
784
|
+
</div>`;
|
|
785
|
+
document.body.appendChild(modal);
|
|
786
|
+
|
|
787
|
+
// Close handlers
|
|
788
|
+
$('changelogClose')?.addEventListener('click', () => modal.remove());
|
|
789
|
+
modal.addEventListener('click', (e) => { if (e.target === modal) modal.remove(); });
|
|
790
|
+
|
|
791
|
+
// Fetch changelog
|
|
792
|
+
try {
|
|
793
|
+
const notes = await window.electronAPI?.fetchChangelog(version);
|
|
794
|
+
const body = $('changelogBody');
|
|
795
|
+
if (!body) return;
|
|
796
|
+
if (!notes || typeof notes !== 'string') {
|
|
797
|
+
body.innerHTML = '<div style="color:var(--text-dim);padding:20px;text-align:center">No release notes available.</div>';
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
if (body && notes) {
|
|
801
|
+
// Simple markdown-like rendering
|
|
802
|
+
body.innerHTML = notes
|
|
803
|
+
.replace(/^### (.+)$/gm, '<h3 style="color:var(--accent);font-size:12px;font-weight:700;margin:12px 0 6px">$1</h3>')
|
|
804
|
+
.replace(/^## (.+)$/gm, '<h2 style="color:var(--text);font-size:14px;font-weight:700;margin:16px 0 8px">$1</h2>')
|
|
805
|
+
.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>')
|
|
806
|
+
.replace(/^- (.+)$/gm, '<div style="margin:3px 0;font-size:11px;line-height:1.6;color:var(--text-mid)">• $1</div>')
|
|
807
|
+
.replace(/`([^`]+)`/g, '<code style="background:var(--bg3);padding:1px 4px;border-radius:3px;color:var(--accent);font-size:10px">$1</code>')
|
|
808
|
+
// Convert markdown links [text](url) to clickable links
|
|
809
|
+
.replace(/\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g, '<a class="changelog-link" href="$2">$1</a>')
|
|
810
|
+
// Convert bare URLs to clickable links (skip already wrapped in href="...")
|
|
811
|
+
.replace(/(?<!href="|">)(https?:\/\/[^\s<)"]+)/g, '<a class="changelog-link" href="$1">$1</a>')
|
|
812
|
+
// Make .dmg and .zip filenames clickable — link to GitHub release assets
|
|
813
|
+
.replace(/(ReactoRadar-[\d.]+-arm64\.dmg)(?!\s*<\/a>)/g,
|
|
814
|
+
`<a class="changelog-link" href="https://github.com/sharanagouda/reactoradar/releases/download/v${esc(version)}/$1">$1</a>`)
|
|
815
|
+
.replace(/(ReactoRadar-[\d.]+-arm64-mac\.zip)(?!\s*<\/a>)/g,
|
|
816
|
+
`<a class="changelog-link" href="https://github.com/sharanagouda/reactoradar/releases/download/v${esc(version)}/$1">$1</a>`)
|
|
817
|
+
.replace(/\n\n/g, '<br/>');
|
|
818
|
+
|
|
819
|
+
// Make all links open externally (not inside Electron)
|
|
820
|
+
body.querySelectorAll('a.changelog-link').forEach(a => {
|
|
821
|
+
a.addEventListener('click', (e) => {
|
|
822
|
+
e.preventDefault();
|
|
823
|
+
const url = a.getAttribute('href');
|
|
824
|
+
if (url) window.electronAPI?.openExternal(url);
|
|
825
|
+
});
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
} catch {
|
|
829
|
+
const body = $('changelogBody');
|
|
830
|
+
if (body) body.innerHTML = '<div style="color:var(--red);padding:20px;text-align:center">Could not fetch release notes</div>';
|
|
831
|
+
}
|
|
832
|
+
}
|