// 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) { chrome.runtime.sendMessage( { target: 'holesail-native', action: 'send', payload: { type: 'getState', payload: {} } }, (response) => { 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 || '' }); } } pending++; chrome.runtime.sendMessage( { target: 'holesail-native', action: 'send', payload: { type: 'setSshConnections', payload: { connections: mergedSsh } } }, () => { chrome.runtime.sendMessage( { target: 'holesail-native', action: 'send', payload: { type: 'setRdpConnections', payload: { connections: mergedRdp } } }, () => onDone() ); } ); } ); } if (pending === 0) { showToast('Nothing to import', 'default'); e.target.value = ''; } }; reader.readAsText(file); }); // Peer Lookup function runPeerLookup() { const input = $('peerLookupInput'); const resultEl = $('peerLookupResult'); if (!input || !resultEl) return; const key = input.value.trim(); if (!key) { showToast('Enter an hs:// key', 'default'); return; } resultEl.style.display = 'block'; resultEl.innerHTML = 'Looking up…'; chrome.runtime.sendMessage( { target: 'holesail-native', action: 'send', payload: { type: 'lookup', payload: { hsUrl: key } } }, (response) => { if (chrome.runtime.lastError || !response) { resultEl.innerHTML = 'Lookup failed: ' + (chrome.runtime.lastError?.message || 'No response') + ''; return; } if (!response.ok) { resultEl.innerHTML = 'Error: ' + escapeHtml(response.error || 'Unknown error') + ''; return; } const peers = response.peers || []; if (peers.length === 0) { resultEl.innerHTML = 'No peers found for this key.'; } else { resultEl.innerHTML = 'Found ' + peers.length + ' peer(s): ' + peers.map(p => '' + escapeHtml(String(p)) + '').join(', '); } } ); } $('btnPeerLookup')?.addEventListener('click', runPeerLookup); $('peerLookupInput')?.addEventListener('keydown', (e) => { if (e.key === 'Enter') runPeerLookup(); }); setupVirtualHostEvents(); setupServerEvents(); setupServiceTunnelEvents(); setupLogsEvents(); }