// Global event wiring — toggle switches, settings buttons, and per-page setup calls. // Depends on: all page modules function setupEvents() { // Guard against duplicate listener registration if setupEvents() is called // more than once (e.g. after a hot-reload). Without this, anonymous listeners // accumulate and each toggle fires N times per click after N calls. if (document._holesailEventsSetup) return; document._holesailEventsSetup = true; // Restore saved theme before any toggle listeners fire const savedTheme = localStorage.getItem('holesail-theme'); if (savedTheme === 'light') document.documentElement.setAttribute('data-theme', 'light'); // Toggle switches document.querySelectorAll('.toggle').forEach(toggle => { toggle.addEventListener('click', () => { toggle.classList.toggle('active'); if (toggle.id === 'toggleTheme') { const isLight = toggle.classList.contains('active'); document.documentElement.setAttribute('data-theme', isLight ? 'light' : 'dark'); localStorage.setItem('holesail-theme', isLight ? 'light' : 'dark'); } }); }); // Save settings $('btnSaveSettings')?.addEventListener('click', saveSettings); // Reset settings to defaults $('btnResetSettings')?.addEventListener('click', () => { settings = { ...SETTINGS_DEFAULTS }; chrome.runtime.sendMessage( { target: 'holesail-native', action: 'send', payload: { type: 'updateSettings', payload: { ...settings } } }, (response) => { if (response && response.settings) settings = { ...SETTINGS_DEFAULTS, ...response.settings }; updateSettingsUI(); showToast('Settings reset to defaults', 'success'); } ); }); // Export configuration $('btnExportConfig')?.addEventListener('click', () => { chrome.runtime.sendMessage( { target: 'holesail-native', action: 'send', payload: { type: 'getState', payload: {} } }, (response) => { if (chrome.runtime.lastError || !response || !response.ok) { showToast('Export failed: could not read state', 'error'); return; } const state = response; const exportData = { exportedAt: new Date().toISOString(), version: 1, virtualHosts: (state.virtualHosts || []).map(v => ({ hostname: v.hostname, hsUrl: v.hsUrl })), serviceTunnels: (state.serviceTunnels || []).map(t => ({ label: t.label, hsUrl: t.hsUrl, localPort: t.localPort })), sshConnections: (state.sshConnections || []).map(s => ({ label: s.label, hsUrl: s.hsUrl, username: s.username })), rdpConnections: (state.rdpConnections || []).map(r => ({ label: r.label, hsUrl: r.hsUrl, type: r.type, port: r.port, width: r.width, height: r.height, username: r.username })) }; const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'holesail-config-' + new Date().toISOString().slice(0, 10) + '.json'; a.click(); setTimeout(() => URL.revokeObjectURL(url), 5000); showToast('Configuration exported', 'success'); } ); }); // Import configuration $('importConfigFile')?.addEventListener('change', (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (ev) => { let data; try { data = JSON.parse(ev.target.result); } catch (_) { showToast('Invalid JSON file', 'error'); return; } if (!data || typeof data !== 'object') { showToast('Invalid config file', 'error'); return; } const vhosts = Array.isArray(data.virtualHosts) ? data.virtualHosts : []; const svcTunnels = Array.isArray(data.serviceTunnels) ? data.serviceTunnels : []; const sshConns = Array.isArray(data.sshConnections) ? data.sshConnections : []; const rdpConns = Array.isArray(data.rdpConnections) ? data.rdpConnections : []; let pending = 0; let done = 0; function onDone() { done++; if (done >= pending) { showToast('Import complete', 'success'); e.target.value = ''; if (typeof refreshAll === 'function') refreshAll(); } } for (const v of vhosts) { if (!v.hostname || !v.hsUrl) continue; pending++; chrome.runtime.sendMessage( { target: 'holesail-native', action: 'send', payload: { type: 'setVirtualHost', payload: { hostname: v.hostname, hsUrl: v.hsUrl } } }, () => onDone() ); } for (const t of svcTunnels) { if (!t.hsUrl) continue; pending++; chrome.runtime.sendMessage( { target: 'holesail-native', action: 'send', payload: { type: 'startServiceTunnel', payload: { label: t.label || '', hsUrl: t.hsUrl, localPort: t.localPort } } }, () => onDone() ); } if (sshConns.length || rdpConns.length) { pending++; chrome.runtime.sendMessage( { target: 'holesail-native', action: 'send', payload: { type: 'getState', payload: {} } }, (response) => { void chrome.runtime.lastError; const existing = response || {}; const mergedSsh = [...(existing.sshConnections || [])]; for (const s of sshConns) { if (!s.hsUrl) continue; if (!mergedSsh.find(x => x.hsUrl === s.hsUrl && x.username === s.username)) { mergedSsh.push({ id: 'ssh-' + Date.now() + '-' + Math.random().toString(36).slice(2), label: s.label || '', hsUrl: s.hsUrl, username: s.username || '' }); } } const mergedRdp = [...(existing.rdpConnections || [])]; for (const r of rdpConns) { if (!r.hsUrl) continue; if (!mergedRdp.find(x => x.hsUrl === r.hsUrl)) { mergedRdp.push({ id: 'rdp-' + Date.now() + '-' + Math.random().toString(36).slice(2), label: r.label || '', hsUrl: r.hsUrl, type: r.type || 'vnc', port: r.port || 5900, width: r.width || 1280, height: r.height || 720, username: r.username || '' }); } } chrome.runtime.sendMessage( { target: 'holesail-native', action: 'send', payload: { type: 'setSshConnections', payload: { connections: mergedSsh } } }, () => { void chrome.runtime.lastError; chrome.runtime.sendMessage( { target: 'holesail-native', action: 'send', payload: { type: 'setRdpConnections', payload: { connections: mergedRdp } } }, () => { void chrome.runtime.lastError; onDone(); } ); } ); } ); } if (pending === 0) { showToast('Nothing to import', 'default'); e.target.value = ''; } }; reader.readAsText(file); }); // Holesail Lookup function runPeerLookup() { const input = $('peerLookupInput'); if (!input) return; const key = input.value.trim(); if (!key) { showToast('Enter an hs:// key', 'default'); return; } const body = $('holesailLookupBody'); if (body) body.innerHTML = '