atlas-browser 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/.github/FUNDING.yml +1 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.md +26 -0
  3. package/.github/ISSUE_TEMPLATE/feature_request.md +16 -0
  4. package/CONTRIBUTING.md +63 -0
  5. package/LICENSE +21 -0
  6. package/PRIVACY.md +37 -0
  7. package/README.md +163 -0
  8. package/SECURITY.md +29 -0
  9. package/assets/logo.png +0 -0
  10. package/bin/cli.js +142 -0
  11. package/dist-electron/main/blocker.js +109 -0
  12. package/dist-electron/main/blocker.js.map +1 -0
  13. package/dist-electron/main/bookmark-manager.js +121 -0
  14. package/dist-electron/main/bookmark-manager.js.map +1 -0
  15. package/dist-electron/main/download-manager.js +118 -0
  16. package/dist-electron/main/download-manager.js.map +1 -0
  17. package/dist-electron/main/index.js +116 -0
  18. package/dist-electron/main/index.js.map +1 -0
  19. package/dist-electron/main/ipc-handlers.js +303 -0
  20. package/dist-electron/main/ipc-handlers.js.map +1 -0
  21. package/dist-electron/main/menu.js +229 -0
  22. package/dist-electron/main/menu.js.map +1 -0
  23. package/dist-electron/main/security-analyzer.js +71 -0
  24. package/dist-electron/main/security-analyzer.js.map +1 -0
  25. package/dist-electron/main/settings-manager.js +105 -0
  26. package/dist-electron/main/settings-manager.js.map +1 -0
  27. package/dist-electron/main/tab-manager.js +205 -0
  28. package/dist-electron/main/tab-manager.js.map +1 -0
  29. package/dist-electron/main/tor-manager.js +59 -0
  30. package/dist-electron/main/tor-manager.js.map +1 -0
  31. package/dist-electron/preload/preload.js +73 -0
  32. package/dist-electron/preload/preload.js.map +1 -0
  33. package/install.sh +120 -0
  34. package/package.json +67 -0
  35. package/src/main/blocker.ts +121 -0
  36. package/src/main/bookmark-manager.ts +99 -0
  37. package/src/main/download-manager.ts +103 -0
  38. package/src/main/index.ts +93 -0
  39. package/src/main/ipc-handlers.ts +283 -0
  40. package/src/main/menu.ts +192 -0
  41. package/src/main/security-analyzer.ts +97 -0
  42. package/src/main/settings-manager.ts +84 -0
  43. package/src/main/tab-manager.ts +249 -0
  44. package/src/main/tor-manager.ts +59 -0
  45. package/src/preload/preload.ts +85 -0
  46. package/src/renderer/bookmarks.html +84 -0
  47. package/src/renderer/browser-ui.js +427 -0
  48. package/src/renderer/downloads.html +94 -0
  49. package/src/renderer/history.html +111 -0
  50. package/src/renderer/index.html +152 -0
  51. package/src/renderer/internet-map.html +313 -0
  52. package/src/renderer/network-map.js +131 -0
  53. package/src/renderer/security-panel.js +13 -0
  54. package/src/renderer/settings.html +138 -0
  55. package/src/renderer/styles.css +688 -0
  56. package/tsconfig.json +18 -0
@@ -0,0 +1,85 @@
1
+ import { contextBridge, ipcRenderer } from 'electron';
2
+
3
+ contextBridge.exposeInMainWorld('browserAPI', {
4
+ // ── Tab management ────────────────────────────────────────
5
+ newTab: (url?: string) => ipcRenderer.send('new-tab', url),
6
+ closeTab: (tabId: number) => ipcRenderer.send('close-tab', tabId),
7
+ switchTab: (tabId: number) => ipcRenderer.send('switch-tab', tabId),
8
+ getTabs: () => ipcRenderer.invoke('get-tabs'),
9
+
10
+ // ── Navigation ────────────────────────────────────────────
11
+ navigate: (url: string) => ipcRenderer.send('navigate', url),
12
+ goBack: () => ipcRenderer.send('go-back'),
13
+ goForward: () => ipcRenderer.send('go-forward'),
14
+ reload: () => ipcRenderer.send('reload'),
15
+ goHome: () => ipcRenderer.send('go-home'),
16
+ openPage: (page: string) => ipcRenderer.send('open-page', page),
17
+
18
+ // ── Blocker ───────────────────────────────────────────────
19
+ toggleBlocker: () => ipcRenderer.invoke('toggle-blocker'),
20
+ getBlockerStats: () => ipcRenderer.invoke('get-blocker-stats'),
21
+
22
+ // ── Tor ───────────────────────────────────────────────────
23
+ toggleTor: () => ipcRenderer.invoke('toggle-tor'),
24
+ getTorStatus: () => ipcRenderer.invoke('get-tor-status'),
25
+ getTorCircuit: () => ipcRenderer.invoke('get-tor-circuit'),
26
+
27
+ // ── Sidebar ───────────────────────────────────────────────
28
+ toggleSidebar: () => ipcRenderer.send('toggle-sidebar'),
29
+ getSecurityInfo: () => ipcRenderer.invoke('get-security-info'),
30
+
31
+ // ── Bookmarks ─────────────────────────────────────────────
32
+ getBookmarks: () => ipcRenderer.invoke('get-bookmarks'),
33
+ getBarBookmarks: () => ipcRenderer.invoke('get-bar-bookmarks'),
34
+ addBookmark: (title: string, url: string, favicon: string) =>
35
+ ipcRenderer.invoke('add-bookmark', title, url, favicon),
36
+ removeBookmark: (id: string) => ipcRenderer.invoke('remove-bookmark', id),
37
+ isBookmarked: (url: string) => ipcRenderer.invoke('is-bookmarked', url),
38
+ toggleBookmark: (title: string, url: string) =>
39
+ ipcRenderer.invoke('toggle-bookmark', title, url),
40
+
41
+ // ── Downloads ─────────────────────────────────────────────
42
+ getDownloads: () => ipcRenderer.invoke('get-downloads'),
43
+ clearDownloads: () => ipcRenderer.invoke('clear-downloads'),
44
+ openDownload: (path: string) => ipcRenderer.send('open-download', path),
45
+ showDownload: (path: string) => ipcRenderer.send('show-download', path),
46
+
47
+ // ── Settings ──────────────────────────────────────────────
48
+ getSettings: () => ipcRenderer.invoke('get-settings'),
49
+ setSetting: (key: string, value: any) => ipcRenderer.invoke('set-setting', key, value),
50
+ resetSettings: () => ipcRenderer.invoke('reset-settings'),
51
+
52
+ // ── Zoom ──────────────────────────────────────────────────
53
+ zoomIn: () => ipcRenderer.invoke('zoom-in'),
54
+ zoomOut: () => ipcRenderer.invoke('zoom-out'),
55
+ zoomReset: () => ipcRenderer.invoke('zoom-reset'),
56
+ getZoom: () => ipcRenderer.invoke('get-zoom'),
57
+
58
+ // ── History ────────────────────────────────────────────────
59
+ getHistory: () => ipcRenderer.invoke('get-history'),
60
+ clearHistory: () => ipcRenderer.invoke('clear-history'),
61
+ deleteHistoryItem: (id: string) => ipcRenderer.invoke('delete-history-item', id),
62
+
63
+ // ── Phantom Mode ──────────────────────────────────────────
64
+ phantomStart: () => ipcRenderer.invoke('phantom-start'),
65
+ phantomEnd: (sessionId: string) => ipcRenderer.invoke('phantom-end', sessionId),
66
+ phantomStatus: (sessionId: string) => ipcRenderer.invoke('phantom-status', sessionId),
67
+
68
+ // ── DNS / Internet Map ────────────────────────────────────
69
+ dnsLookup: (domain: string) => ipcRenderer.invoke('dns-lookup', domain),
70
+
71
+ // ── Events ────────────────────────────────────────────────
72
+ onTabCreated: (cb: Function) => ipcRenderer.on('tab-created', (_e, d) => cb(d)),
73
+ onTabUpdated: (cb: Function) => ipcRenderer.on('tab-updated', (_e, d) => cb(d)),
74
+ onTabActivated: (cb: Function) => ipcRenderer.on('tab-activated', (_e, id) => cb(id)),
75
+ onTabClosed: (cb: Function) => ipcRenderer.on('tab-closed', (_e, id) => cb(id)),
76
+ onUrlChanged: (cb: Function) => ipcRenderer.on('url-changed', (_e, d) => cb(d)),
77
+ onBlockedCountUpdate: (cb: Function) => ipcRenderer.on('blocked-count-update', (_e, d) => cb(d)),
78
+ onSecurityUpdate: (cb: Function) => ipcRenderer.on('security-update', (_e, d) => cb(d)),
79
+ onFocusAddressBar: (cb: Function) => ipcRenderer.on('focus-address-bar', () => cb()),
80
+ onSidebarToggled: (cb: Function) => ipcRenderer.on('sidebar-toggled', (_e, v) => cb(v)),
81
+ onTorStatusChanged: (cb: Function) => ipcRenderer.on('tor-status-changed', (_e, v) => cb(v)),
82
+ onDownloadStarted: (cb: Function) => ipcRenderer.on('download-started', (_e, d) => cb(d)),
83
+ onDownloadProgress: (cb: Function) => ipcRenderer.on('download-progress', (_e, d) => cb(d)),
84
+ onDownloadDone: (cb: Function) => ipcRenderer.on('download-done', (_e, d) => cb(d)),
85
+ });
@@ -0,0 +1,84 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Bookmarks - Atlas Browser</title>
6
+ <style>
7
+ * { margin: 0; padding: 0; box-sizing: border-box; }
8
+ body { background: #0a0a0f; color: #e2e2e8; font-family: -apple-system, sans-serif; padding: 40px; max-width: 800px; margin: 0 auto; }
9
+ h1 { font-size: 24px; margin-bottom: 24px; }
10
+ .bookmark-grid { display: grid; gap: 8px; }
11
+ .bm-card { display: flex; align-items: center; gap: 12px; padding: 14px 16px; border: 1px solid #1e1e2e; border-radius: 10px; background: #12121a; cursor: pointer; transition: all 0.15s; }
12
+ .bm-card:hover { border-color: #00d4ff33; background: #1a1a28; }
13
+ .bm-title { font-size: 14px; font-weight: 500; flex: 1; }
14
+ .bm-url { font-size: 11px; color: #8888a0; font-family: 'SF Mono', monospace; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 300px; }
15
+ .bm-date { font-size: 10px; color: #8888a033; font-family: 'SF Mono', monospace; }
16
+ .bm-delete { background: none; border: none; color: #8888a0; cursor: pointer; font-size: 16px; padding: 4px 8px; border-radius: 4px; }
17
+ .bm-delete:hover { color: #ef4444; background: rgba(239,68,68,0.1); }
18
+ .empty { text-align: center; padding: 60px; color: #8888a0; }
19
+ .empty-icon { font-size: 48px; margin-bottom: 12px; }
20
+ .search { width: 100%; padding: 10px 16px; background: #12121a; border: 1px solid #1e1e2e; border-radius: 10px; color: #e2e2e8; font-size: 14px; outline: none; margin-bottom: 16px; }
21
+ .search:focus { border-color: #00d4ff; }
22
+ </style>
23
+ </head>
24
+ <body>
25
+ <h1>&#x1f4d1; Bookmarks</h1>
26
+ <input class="search" id="search" placeholder="Search bookmarks..." autofocus>
27
+ <div id="bookmark-grid" class="bookmark-grid"></div>
28
+ <script>
29
+ const api = window.browserAPI;
30
+ const grid = document.getElementById('bookmark-grid');
31
+ const search = document.getElementById('search');
32
+ let allBookmarks = [];
33
+
34
+ async function load() {
35
+ if (!api) return;
36
+ allBookmarks = await api.getBookmarks();
37
+ render(allBookmarks);
38
+ }
39
+
40
+ function render(bms) {
41
+ if (!bms || bms.length === 0) {
42
+ grid.innerHTML = '<div class="empty"><div class="empty-icon">&#x1f516;</div><p>No bookmarks yet. Press &#9734; in the address bar to add one.</p></div>';
43
+ return;
44
+ }
45
+ grid.innerHTML = bms.map(b =>
46
+ '<div class="bm-card" data-url="' + b.url + '">' +
47
+ '<div style="flex:1">' +
48
+ '<div class="bm-title">' + b.title + '</div>' +
49
+ '<div class="bm-url">' + b.url + '</div>' +
50
+ '</div>' +
51
+ '<div class="bm-date">' + new Date(b.createdAt).toLocaleDateString() + '</div>' +
52
+ '<button class="bm-delete" data-id="' + b.id + '" title="Delete">&times;</button>' +
53
+ '</div>'
54
+ ).join('');
55
+
56
+ grid.querySelectorAll('.bm-card').forEach(card => {
57
+ card.addEventListener('click', (e) => {
58
+ if (e.target.classList.contains('bm-delete')) return;
59
+ // Navigate in browser
60
+ window.location.href = card.dataset.url;
61
+ });
62
+ });
63
+
64
+ grid.querySelectorAll('.bm-delete').forEach(btn => {
65
+ btn.addEventListener('click', async (e) => {
66
+ e.stopPropagation();
67
+ await api.removeBookmark(btn.dataset.id);
68
+ load();
69
+ });
70
+ });
71
+ }
72
+
73
+ search.addEventListener('input', () => {
74
+ const q = search.value.toLowerCase();
75
+ const filtered = allBookmarks.filter(b =>
76
+ b.title.toLowerCase().includes(q) || b.url.toLowerCase().includes(q)
77
+ );
78
+ render(filtered);
79
+ });
80
+
81
+ load();
82
+ </script>
83
+ </body>
84
+ </html>
@@ -0,0 +1,427 @@
1
+ // Atlas Browser - Browser Chrome UI
2
+ const api = window.browserAPI;
3
+
4
+ // ── State ──────────────────────────────────────────────────
5
+ let tabs = [];
6
+ let activeTabId = null;
7
+ let blockedCounts = {};
8
+ let torEnabled = false;
9
+ let currentZoom = 100;
10
+ let phantomSession = null;
11
+ let phantomTimer = null;
12
+
13
+ // ── DOM Elements ───────────────────────────────────────────
14
+ const tabsContainer = document.getElementById('tabs-container');
15
+ const newTabBtn = document.getElementById('new-tab-btn');
16
+ const addressBar = document.getElementById('address-bar');
17
+ const backBtn = document.getElementById('back-btn');
18
+ const forwardBtn = document.getElementById('forward-btn');
19
+ const reloadBtn = document.getElementById('reload-btn');
20
+ const homeBtn = document.getElementById('home-btn');
21
+ const bookmarkStar = document.getElementById('bookmark-star');
22
+ const shieldBtn = document.getElementById('shield-btn');
23
+ const torBtn = document.getElementById('tor-btn');
24
+ const securityBtn = document.getElementById('security-btn');
25
+ const downloadsBtn = document.getElementById('downloads-btn');
26
+ const mapBtn = document.getElementById('map-btn');
27
+ const settingsBtn = document.getElementById('settings-btn');
28
+ const blockedCount = document.getElementById('blocked-count');
29
+ const sslIndicator = document.getElementById('ssl-indicator');
30
+ const sidebar = document.getElementById('security-sidebar');
31
+ const closeSidebar = document.getElementById('close-sidebar');
32
+ const zoomIn = document.getElementById('zoom-in');
33
+ const zoomOut = document.getElementById('zoom-out');
34
+ const zoomLevel = document.getElementById('zoom-level');
35
+ const bookmarksContainer = document.getElementById('bookmarks-container');
36
+ const allBookmarksBtn = document.getElementById('all-bookmarks-btn');
37
+ const progressBar = document.getElementById('progress-bar');
38
+ const progressFill = document.getElementById('progress-fill');
39
+ const contextMenu = document.getElementById('context-menu');
40
+
41
+ // ── Tab Rendering ──────────────────────────────────────────
42
+ function renderTabs() {
43
+ tabsContainer.innerHTML = '';
44
+ tabs.forEach(tab => {
45
+ const el = document.createElement('div');
46
+ el.className = `tab ${tab.id === activeTabId ? 'active' : ''}`;
47
+ el.innerHTML = `
48
+ ${tab.isLoading ? '<div class="tab-loading"></div>' : ''}
49
+ <span class="tab-title">${esc(tab.title || 'New Tab')}</span>
50
+ <span class="tab-close" data-id="${tab.id}">&times;</span>
51
+ `;
52
+ el.addEventListener('click', (e) => {
53
+ if (e.target.classList.contains('tab-close')) {
54
+ api?.closeTab(parseInt(e.target.dataset.id));
55
+ } else {
56
+ api?.switchTab(tab.id);
57
+ }
58
+ });
59
+ tabsContainer.appendChild(el);
60
+ });
61
+ }
62
+
63
+ function esc(str) {
64
+ const d = document.createElement('div');
65
+ d.textContent = str;
66
+ return d.innerHTML;
67
+ }
68
+
69
+ // ── Navigation ─────────────────────────────────────────────
70
+ addressBar.addEventListener('keydown', (e) => {
71
+ if (e.key === 'Enter') {
72
+ const v = addressBar.value.trim();
73
+ if (v) api?.navigate(v);
74
+ }
75
+ });
76
+
77
+ newTabBtn.addEventListener('click', () => api?.newTab());
78
+ backBtn.addEventListener('click', () => api?.goBack());
79
+ forwardBtn.addEventListener('click', () => api?.goForward());
80
+ reloadBtn.addEventListener('click', () => api?.reload());
81
+ homeBtn.addEventListener('click', () => api?.goHome());
82
+
83
+ // ── Bookmark Star ──────────────────────────────────────────
84
+ bookmarkStar.addEventListener('click', async () => {
85
+ if (!api) return;
86
+ const tab = tabs.find(t => t.id === activeTabId);
87
+ if (!tab) return;
88
+ const isNow = await api.toggleBookmark(tab.title || 'Bookmark', tab.url || '');
89
+ bookmarkStar.innerHTML = isNow ? '&#9733;' : '&#9734;';
90
+ bookmarkStar.classList.toggle('active', isNow);
91
+ loadBookmarksBar();
92
+ });
93
+
94
+ async function checkBookmark(url) {
95
+ if (!api || !url) return;
96
+ const is = await api.isBookmarked(url);
97
+ bookmarkStar.innerHTML = is ? '&#9733;' : '&#9734;';
98
+ bookmarkStar.classList.toggle('active', is);
99
+ }
100
+
101
+ // ── Bookmarks Bar ──────────────────────────────────────────
102
+ async function loadBookmarksBar() {
103
+ if (!api) return;
104
+ const bms = await api.getBarBookmarks();
105
+ if (!bms || bms.length === 0) {
106
+ bookmarksContainer.innerHTML = '<span class="bookmarks-empty">Bookmark pages with &#9734; to see them here</span>';
107
+ return;
108
+ }
109
+ bookmarksContainer.innerHTML = '';
110
+ bms.forEach(bm => {
111
+ const el = document.createElement('div');
112
+ el.className = 'bm-item';
113
+ el.innerHTML = `<span>${esc(bm.title.slice(0, 20))}</span>`;
114
+ el.title = bm.url;
115
+ el.addEventListener('click', () => api.navigate(bm.url));
116
+ el.addEventListener('contextmenu', (e) => {
117
+ e.preventDefault();
118
+ if (confirm('Remove bookmark?')) {
119
+ api.removeBookmark(bm.id).then(() => loadBookmarksBar());
120
+ }
121
+ });
122
+ bookmarksContainer.appendChild(el);
123
+ });
124
+ }
125
+
126
+ allBookmarksBtn.addEventListener('click', () => api?.openPage('bookmarks'));
127
+
128
+ // ── Shield (blocker) ───────────────────────────────────────
129
+ shieldBtn.addEventListener('click', async () => {
130
+ if (!api) return;
131
+ const enabled = await api.toggleBlocker();
132
+ shieldBtn.classList.toggle('shield-active', enabled);
133
+ });
134
+
135
+ // ── Tor Toggle ─────────────────────────────────────────────
136
+ torBtn.addEventListener('click', async () => {
137
+ if (!api) return;
138
+ torEnabled = await api.toggleTor();
139
+ torBtn.classList.toggle('tor-active', torEnabled);
140
+ updateTorCircuit();
141
+ });
142
+
143
+ async function updateTorCircuit() {
144
+ if (!api) return;
145
+ const circuit = await api.getTorCircuit();
146
+ const el = document.getElementById('tor-circuit');
147
+ if (!circuit || !circuit.isActive) {
148
+ el.innerHTML = '<span class="tor-disabled">Tor is disabled</span>';
149
+ } else {
150
+ el.innerHTML = circuit.nodes.map(n => `<div class="tor-node">${esc(n)}</div>`).join('');
151
+ }
152
+ }
153
+
154
+ // ── Zoom ───────────────────────────────────────────────────
155
+ zoomIn.addEventListener('click', async () => {
156
+ if (!api) return;
157
+ currentZoom = await api.zoomIn();
158
+ zoomLevel.textContent = currentZoom + '%';
159
+ });
160
+ zoomOut.addEventListener('click', async () => {
161
+ if (!api) return;
162
+ currentZoom = await api.zoomOut();
163
+ zoomLevel.textContent = currentZoom + '%';
164
+ });
165
+ zoomLevel.addEventListener('click', async () => {
166
+ if (!api) return;
167
+ currentZoom = await api.zoomReset();
168
+ zoomLevel.textContent = currentZoom + '%';
169
+ });
170
+
171
+ // ── Toolbar buttons ────────────────────────────────────────
172
+ downloadsBtn.addEventListener('click', () => api?.openPage('downloads'));
173
+ mapBtn.addEventListener('click', () => api?.openPage('internet-map'));
174
+ settingsBtn.addEventListener('click', () => api?.openPage('settings'));
175
+
176
+ // ── Security Sidebar ───────────────────────────────────────
177
+ securityBtn.addEventListener('click', () => api?.toggleSidebar());
178
+ closeSidebar.addEventListener('click', () => api?.toggleSidebar());
179
+
180
+ async function updateSecurityInfo() {
181
+ if (!api) return;
182
+ const info = await api.getSecurityInfo();
183
+ if (!info) return;
184
+
185
+ const scoreEl = document.getElementById('privacy-score');
186
+ scoreEl.textContent = info.privacyScore;
187
+ scoreEl.style.color = info.privacyScore >= 80 ? 'var(--green)' :
188
+ info.privacyScore >= 50 ? 'var(--amber)' : 'var(--red)';
189
+
190
+ document.getElementById('proto-val').textContent = info.isSecure ? 'HTTPS (Secure)' : 'HTTP (Insecure)';
191
+ document.getElementById('proto-val').className = `value ${info.isSecure ? 'badge-green' : 'badge-red'}`;
192
+ document.getElementById('cert-val').textContent = info.certificate ? info.certificate.issuer : 'None';
193
+ document.getElementById('cookies-count').textContent = info.cookieCount;
194
+
195
+ const stats = await api.getBlockerStats();
196
+ document.getElementById('trackers-total').textContent = stats.totalBlocked;
197
+ }
198
+
199
+ // ── SSL Indicator ──────────────────────────────────────────
200
+ function updateSslIndicator(url) {
201
+ try {
202
+ const isSecure = url.startsWith('https://');
203
+ sslIndicator.textContent = isSecure ? '\u{1f512}' : '\u{1f513}';
204
+ sslIndicator.className = `ssl-indicator ${isSecure ? 'ssl-secure' : 'ssl-insecure'}`;
205
+ } catch {}
206
+ }
207
+
208
+ // ── Notification Toast ─────────────────────────────────────
209
+ function showToast(msg, color = 'var(--accent)') {
210
+ const t = document.createElement('div');
211
+ t.textContent = msg;
212
+ t.style.cssText = `
213
+ position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
214
+ background: rgba(0,0,0,0.8); border: 1px solid ${color};
215
+ color: ${color}; padding: 8px 20px; border-radius: 10px;
216
+ font-size: 12px; z-index: 9999; backdrop-filter: blur(8px);
217
+ `;
218
+ document.body.appendChild(t);
219
+ setTimeout(() => t.remove(), 3000);
220
+ }
221
+
222
+ // ── IPC Event Listeners ────────────────────────────────────
223
+ if (api) {
224
+ api.onTabCreated((data) => {
225
+ tabs.push({ ...data, isLoading: true });
226
+ renderTabs();
227
+ });
228
+
229
+ api.onTabUpdated((data) => {
230
+ const idx = tabs.findIndex(t => t.id === data.id);
231
+ if (idx >= 0) { tabs[idx] = { ...tabs[idx], ...data }; renderTabs(); }
232
+ });
233
+
234
+ api.onTabActivated((tabId) => {
235
+ activeTabId = tabId;
236
+ renderTabs();
237
+ const tab = tabs.find(t => t.id === tabId);
238
+ if (tab) {
239
+ addressBar.value = tab.url || '';
240
+ updateSslIndicator(tab.url || '');
241
+ checkBookmark(tab.url);
242
+ }
243
+ });
244
+
245
+ api.onTabClosed((tabId) => {
246
+ tabs = tabs.filter(t => t.id !== tabId);
247
+ delete blockedCounts[tabId];
248
+ renderTabs();
249
+ });
250
+
251
+ api.onUrlChanged((data) => {
252
+ const tab = tabs.find(t => t.id === data.tabId);
253
+ if (tab) {
254
+ tab.url = data.url;
255
+ if (data.tabId === activeTabId) {
256
+ addressBar.value = data.url;
257
+ updateSslIndicator(data.url);
258
+ backBtn.disabled = !data.canGoBack;
259
+ forwardBtn.disabled = !data.canGoForward;
260
+ checkBookmark(data.url);
261
+ }
262
+ }
263
+ });
264
+
265
+ api.onBlockedCountUpdate((data) => {
266
+ blockedCounts[data.tabId] = data.count;
267
+ if (data.tabId === activeTabId) {
268
+ blockedCount.textContent = data.count;
269
+ // Pop animation
270
+ blockedCount.classList.remove('pop');
271
+ void blockedCount.offsetWidth; // force reflow
272
+ blockedCount.classList.add('pop');
273
+ }
274
+ });
275
+
276
+ api.onFocusAddressBar(() => { addressBar.focus(); addressBar.select(); });
277
+
278
+ api.onSidebarToggled((isOpen) => {
279
+ if (isOpen) { sidebar.classList.remove('hidden'); updateSecurityInfo(); }
280
+ else { sidebar.classList.add('hidden'); }
281
+ });
282
+
283
+ api.onTorStatusChanged((enabled) => {
284
+ torEnabled = enabled;
285
+ torBtn.classList.toggle('tor-active', enabled);
286
+ updateTorCircuit();
287
+ showToast(
288
+ enabled ? 'Tor enabled - traffic routed through Tor network' : 'Tor disabled - direct connection',
289
+ enabled ? 'var(--purple)' : 'var(--muted)'
290
+ );
291
+ });
292
+
293
+ api.onDownloadStarted((d) => showToast(`Downloading: ${d.filename}`, 'var(--green)'));
294
+ api.onDownloadDone((d) => showToast(
295
+ d.state === 'completed' ? `Downloaded: ${d.filename}` : `Download failed: ${d.filename}`,
296
+ d.state === 'completed' ? 'var(--green)' : 'var(--red)'
297
+ ));
298
+
299
+ // Load initial data
300
+ loadBookmarksBar();
301
+ }
302
+
303
+ // ── Progress Bar (loading indicator) ───────────────────────
304
+ if (api) {
305
+ api.onTabUpdated((data) => {
306
+ if (data.id !== activeTabId) return;
307
+ if (data.isLoading) {
308
+ progressBar.classList.remove('hidden');
309
+ progressFill.style.width = '70%';
310
+ } else {
311
+ progressFill.style.width = '100%';
312
+ setTimeout(() => {
313
+ progressBar.classList.add('hidden');
314
+ progressFill.style.width = '0%';
315
+ }, 300);
316
+ }
317
+ });
318
+ }
319
+
320
+ // ── Context Menu ───────────────────────────────────────────
321
+ document.addEventListener('contextmenu', (e) => {
322
+ e.preventDefault();
323
+ contextMenu.classList.remove('hidden');
324
+ contextMenu.style.left = Math.min(e.clientX, window.innerWidth - 200) + 'px';
325
+ contextMenu.style.top = Math.min(e.clientY, window.innerHeight - 200) + 'px';
326
+ });
327
+
328
+ document.addEventListener('click', () => {
329
+ contextMenu.classList.add('hidden');
330
+ });
331
+
332
+ contextMenu.querySelectorAll('.ctx-item').forEach(item => {
333
+ item.addEventListener('click', () => {
334
+ const action = item.dataset.action;
335
+ if (action === 'back') api?.goBack();
336
+ if (action === 'forward') api?.goForward();
337
+ if (action === 'reload') api?.reload();
338
+ if (action === 'newtab') api?.newTab();
339
+ if (action === 'copyurl') {
340
+ const tab = tabs.find(t => t.id === activeTabId);
341
+ if (tab) navigator.clipboard.writeText(tab.url);
342
+ }
343
+ if (action === 'devtools') {
344
+ // DevTools handled by menu Cmd+Option+I
345
+ }
346
+ contextMenu.classList.add('hidden');
347
+ });
348
+ });
349
+
350
+ // ── Phantom Mode ───────────────────────────────────────────
351
+ const phantomBtn = document.getElementById('phantom-btn');
352
+ const phantomBar = document.getElementById('phantom-bar');
353
+ const phantomInfo = document.getElementById('phantom-info');
354
+ const phantomTimerEl = document.getElementById('phantom-timer');
355
+ const phantomDestroyBtn = document.getElementById('phantom-destroy');
356
+
357
+ function formatPhantomTime(seconds) {
358
+ const m = Math.floor(seconds / 60);
359
+ const s = seconds % 60;
360
+ return `${m}:${s.toString().padStart(2, '0')}`;
361
+ }
362
+
363
+ async function startPhantom() {
364
+ if (!api || phantomSession) return;
365
+ phantomBtn.classList.add('phantom-active');
366
+ showToast('Creating phantom container...', 'var(--purple)');
367
+
368
+ try {
369
+ const result = await api.phantomStart();
370
+ if (result && result.session_id) {
371
+ phantomSession = result;
372
+ phantomBar.classList.remove('hidden');
373
+ phantomInfo.textContent = `Container phantom_${result.session_id.slice(0, 8)} | Port ${result.port}`;
374
+
375
+ // Start countdown
376
+ let remaining = 1800;
377
+ phantomTimer = setInterval(() => {
378
+ remaining--;
379
+ phantomTimerEl.textContent = formatPhantomTime(remaining);
380
+ if (remaining <= 0) {
381
+ endPhantom();
382
+ }
383
+ }, 1000);
384
+
385
+ showToast('Phantom Mode active - all searches isolated in Docker container', 'var(--purple)');
386
+ }
387
+ } catch (err) {
388
+ showToast('Failed to start phantom: ' + (err.message || err), 'var(--red)');
389
+ phantomBtn.classList.remove('phantom-active');
390
+ }
391
+ }
392
+
393
+ async function endPhantom() {
394
+ if (!api || !phantomSession) return;
395
+
396
+ try {
397
+ await api.phantomEnd(phantomSession.session_id);
398
+ } catch {}
399
+
400
+ phantomSession = null;
401
+ phantomBar.classList.add('hidden');
402
+ phantomBtn.classList.remove('phantom-active');
403
+ if (phantomTimer) { clearInterval(phantomTimer); phantomTimer = null; }
404
+ showToast('Phantom container destroyed. Zero trace remains.', 'var(--green)');
405
+ }
406
+
407
+ if (phantomBtn) {
408
+ phantomBtn.addEventListener('click', () => {
409
+ if (phantomSession) {
410
+ endPhantom();
411
+ } else {
412
+ startPhantom();
413
+ }
414
+ });
415
+ }
416
+
417
+ if (phantomDestroyBtn) {
418
+ phantomDestroyBtn.addEventListener('click', endPhantom);
419
+ }
420
+
421
+ // ── Keyboard Shortcuts ─────────────────────────────────────
422
+ document.addEventListener('keydown', (e) => {
423
+ if (e.metaKey && e.key === 'l') { e.preventDefault(); addressBar.focus(); addressBar.select(); }
424
+ if (e.metaKey && e.key === 'd') { e.preventDefault(); bookmarkStar.click(); }
425
+ if (e.metaKey && e.key === 'j') { e.preventDefault(); api?.openPage('downloads'); }
426
+ if (e.metaKey && e.key === 'y') { e.preventDefault(); api?.openPage('history'); }
427
+ });