// sidepanel/sidepanel.js — Side Panel logic const STATUS_ICONS = { pending: '', running: '', completed: '\u2713', // ✓ failed: '\u2717', // ✗ stopped: '\u25A0', // ■ }; const logArea = document.getElementById('log-area'); const displayOauthUrl = document.getElementById('display-oauth-url'); const displayLocalhostUrl = document.getElementById('display-localhost-url'); const displayStatus = document.getElementById('display-status'); const statusBar = document.getElementById('status-bar'); const inputEmail = document.getElementById('input-email'); const inputPassword = document.getElementById('input-password'); const btnFetchEmail = document.getElementById('btn-fetch-email'); const btnTogglePassword = document.getElementById('btn-toggle-password'); const btnStop = document.getElementById('btn-stop'); const btnReset = document.getElementById('btn-reset'); const stepsProgress = document.getElementById('steps-progress'); const btnAutoRun = document.getElementById('btn-auto-run'); const btnAutoContinue = document.getElementById('btn-auto-continue'); const autoContinueBar = document.getElementById('auto-continue-bar'); const btnClearLog = document.getElementById('btn-clear-log'); const inputVpsUrl = document.getElementById('input-vps-url'); const selectMailProvider = document.getElementById('select-mail-provider'); const rowInbucketMailbox = document.getElementById('row-inbucket-mailbox'); const inputInbucketMailbox = document.getElementById('input-inbucket-mailbox'); const inputRunCount = document.getElementById('input-run-count'); // ============================================================ // Toast Notifications // ============================================================ const toastContainer = document.getElementById('toast-container'); const TOAST_ICONS = { error: '', warn: '', success: '', info: '', }; function showToast(message, type = 'error', duration = 4000) { const toast = document.createElement('div'); toast.className = `toast toast-${type}`; toast.innerHTML = `${TOAST_ICONS[type] || ''}${escapeHtml(message)}`; toast.querySelector('.toast-close').addEventListener('click', () => dismissToast(toast)); toastContainer.appendChild(toast); if (duration > 0) { setTimeout(() => dismissToast(toast), duration); } } function dismissToast(toast) { if (!toast.parentNode) return; toast.classList.add('toast-exit'); toast.addEventListener('animationend', () => toast.remove()); } // ============================================================ // State Restore on load // ============================================================ async function restoreState() { try { const state = await chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' }); if (state.oauthUrl) { displayOauthUrl.textContent = state.oauthUrl; displayOauthUrl.classList.add('has-value'); } if (state.localhostUrl) { displayLocalhostUrl.textContent = state.localhostUrl; displayLocalhostUrl.classList.add('has-value'); } if (state.email) { inputEmail.value = state.email; } syncPasswordField(state); if (state.vpsUrl) { inputVpsUrl.value = state.vpsUrl; } if (state.mailProvider) { selectMailProvider.value = state.mailProvider; } if (state.inbucketMailbox) { inputInbucketMailbox.value = state.inbucketMailbox; } if (state.stepStatuses) { for (const [step, status] of Object.entries(state.stepStatuses)) { updateStepUI(Number(step), status); } } if (state.logs) { for (const entry of state.logs) { appendLog(entry); } } updateStatusDisplay(state); updateProgressCounter(); updateMailProviderUI(); } catch (err) { console.error('Failed to restore state:', err); } } function syncPasswordField(state) { inputPassword.value = state.customPassword || state.password || ''; } function updateMailProviderUI() { const useInbucket = selectMailProvider.value === 'inbucket'; rowInbucketMailbox.style.display = useInbucket ? '' : 'none'; } // ============================================================ // UI Updates // ============================================================ function updateStepUI(step, status) { const statusEl = document.querySelector(`.step-status[data-step="${step}"]`); const row = document.querySelector(`.step-row[data-step="${step}"]`); if (statusEl) statusEl.textContent = STATUS_ICONS[status] || ''; if (row) { row.className = `step-row ${status}`; } updateButtonStates(); updateProgressCounter(); } function updateProgressCounter() { let completed = 0; document.querySelectorAll('.step-row').forEach(row => { if (row.classList.contains('completed')) completed++; }); stepsProgress.textContent = `${completed} / 9`; } function updateButtonStates() { const statuses = {}; document.querySelectorAll('.step-row').forEach(row => { const step = Number(row.dataset.step); if (row.classList.contains('completed')) statuses[step] = 'completed'; else if (row.classList.contains('running')) statuses[step] = 'running'; else if (row.classList.contains('failed')) statuses[step] = 'failed'; else if (row.classList.contains('stopped')) statuses[step] = 'stopped'; else statuses[step] = 'pending'; }); const anyRunning = Object.values(statuses).some(s => s === 'running'); for (let step = 1; step <= 9; step++) { const btn = document.querySelector(`.step-btn[data-step="${step}"]`); if (!btn) continue; if (anyRunning) { btn.disabled = true; } else if (step === 1) { btn.disabled = false; } else { const prevStatus = statuses[step - 1]; const currentStatus = statuses[step]; btn.disabled = !(prevStatus === 'completed' || currentStatus === 'failed' || currentStatus === 'completed' || currentStatus === 'stopped'); } } updateStopButtonState(anyRunning || autoContinueBar.style.display !== 'none'); } function updateStopButtonState(active) { btnStop.disabled = !active; } function updateStatusDisplay(state) { if (!state || !state.stepStatuses) return; statusBar.className = 'status-bar'; const running = Object.entries(state.stepStatuses).find(([, s]) => s === 'running'); if (running) { displayStatus.textContent = `Step ${running[0]} running...`; statusBar.classList.add('running'); return; } const failed = Object.entries(state.stepStatuses).find(([, s]) => s === 'failed'); if (failed) { displayStatus.textContent = `Step ${failed[0]} failed`; statusBar.classList.add('failed'); return; } const stopped = Object.entries(state.stepStatuses).find(([, s]) => s === 'stopped'); if (stopped) { displayStatus.textContent = `Step ${stopped[0]} stopped`; statusBar.classList.add('stopped'); return; } const lastCompleted = Object.entries(state.stepStatuses) .filter(([, s]) => s === 'completed') .map(([k]) => Number(k)) .sort((a, b) => b - a)[0]; if (lastCompleted === 9) { displayStatus.textContent = 'All steps completed!'; statusBar.classList.add('completed'); } else if (lastCompleted) { displayStatus.textContent = `Step ${lastCompleted} done`; } else { displayStatus.textContent = 'Ready'; } } function appendLog(entry) { const time = new Date(entry.timestamp).toLocaleTimeString('en-US', { hour12: false }); const levelLabel = entry.level.toUpperCase(); const line = document.createElement('div'); line.className = `log-line log-${entry.level}`; const stepMatch = entry.message.match(/Step (\d)/); const stepNum = stepMatch ? stepMatch[1] : null; let html = `${time} `; html += `${levelLabel} `; if (stepNum) { html += `S${stepNum}`; } html += `${escapeHtml(entry.message)}`; line.innerHTML = html; logArea.appendChild(line); logArea.scrollTop = logArea.scrollHeight; } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } async function fetchDuckEmail() { const defaultLabel = 'Auto'; btnFetchEmail.disabled = true; btnFetchEmail.textContent = '...'; try { const response = await chrome.runtime.sendMessage({ type: 'FETCH_DUCK_EMAIL', source: 'sidepanel', payload: { generateNew: true }, }); if (response?.error) { throw new Error(response.error); } if (!response?.email) { throw new Error('Duck email was not returned.'); } inputEmail.value = response.email; showToast(`Fetched ${response.email}`, 'success', 2500); return response.email; } catch (err) { showToast(`Auto fetch failed: ${err.message}`, 'error'); throw err; } finally { btnFetchEmail.disabled = false; btnFetchEmail.textContent = defaultLabel; } } function syncPasswordToggleLabel() { btnTogglePassword.textContent = inputPassword.type === 'password' ? 'Show' : 'Hide'; } // ============================================================ // Button Handlers // ============================================================ document.querySelectorAll('.step-btn').forEach(btn => { btn.addEventListener('click', async () => { const step = Number(btn.dataset.step); if (step === 3) { const email = inputEmail.value.trim(); if (!email) { showToast('Please paste email address or use Auto first', 'warn'); return; } await chrome.runtime.sendMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step, email } }); } else { await chrome.runtime.sendMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step } }); } }); }); btnFetchEmail.addEventListener('click', async () => { await fetchDuckEmail().catch(() => {}); }); btnTogglePassword.addEventListener('click', () => { inputPassword.type = inputPassword.type === 'password' ? 'text' : 'password'; syncPasswordToggleLabel(); }); btnStop.addEventListener('click', async () => { btnStop.disabled = true; await chrome.runtime.sendMessage({ type: 'STOP_FLOW', source: 'sidepanel', payload: {} }); showToast('Stopping current flow...', 'warn', 2000); }); // Auto Run btnAutoRun.addEventListener('click', async () => { const totalRuns = parseInt(inputRunCount.value) || 1; btnAutoRun.disabled = true; inputRunCount.disabled = true; btnAutoRun.innerHTML = ' Running...'; await chrome.runtime.sendMessage({ type: 'AUTO_RUN', source: 'sidepanel', payload: { totalRuns } }); }); btnAutoContinue.addEventListener('click', async () => { const email = inputEmail.value.trim(); if (!email) { showToast('Please fetch or paste DuckDuckGo email first!', 'warn'); return; } autoContinueBar.style.display = 'none'; await chrome.runtime.sendMessage({ type: 'RESUME_AUTO_RUN', source: 'sidepanel', payload: { email } }); }); // Reset btnReset.addEventListener('click', async () => { if (confirm('Reset all steps and data?')) { await chrome.runtime.sendMessage({ type: 'RESET', source: 'sidepanel' }); displayOauthUrl.textContent = 'Waiting...'; displayOauthUrl.classList.remove('has-value'); displayLocalhostUrl.textContent = 'Waiting...'; displayLocalhostUrl.classList.remove('has-value'); inputEmail.value = ''; displayStatus.textContent = 'Ready'; statusBar.className = 'status-bar'; logArea.innerHTML = ''; document.querySelectorAll('.step-row').forEach(row => row.className = 'step-row'); document.querySelectorAll('.step-status').forEach(el => el.textContent = ''); btnAutoRun.disabled = false; inputRunCount.disabled = false; btnAutoRun.innerHTML = ' Auto'; autoContinueBar.style.display = 'none'; updateStopButtonState(false); updateButtonStates(); updateProgressCounter(); } }); // Clear log btnClearLog.addEventListener('click', () => { logArea.innerHTML = ''; }); // Save settings on change inputEmail.addEventListener('change', async () => { const email = inputEmail.value.trim(); if (email) { await chrome.runtime.sendMessage({ type: 'SAVE_EMAIL', source: 'sidepanel', payload: { email } }); } }); inputVpsUrl.addEventListener('change', async () => { const vpsUrl = inputVpsUrl.value.trim(); if (vpsUrl) { await chrome.runtime.sendMessage({ type: 'SAVE_SETTING', source: 'sidepanel', payload: { vpsUrl } }); } }); inputPassword.addEventListener('change', async () => { await chrome.runtime.sendMessage({ type: 'SAVE_SETTING', source: 'sidepanel', payload: { customPassword: inputPassword.value }, }); }); selectMailProvider.addEventListener('change', async () => { updateMailProviderUI(); await chrome.runtime.sendMessage({ type: 'SAVE_SETTING', source: 'sidepanel', payload: { mailProvider: selectMailProvider.value }, }); }); inputInbucketMailbox.addEventListener('change', async () => { await chrome.runtime.sendMessage({ type: 'SAVE_SETTING', source: 'sidepanel', payload: { inbucketMailbox: inputInbucketMailbox.value.trim() }, }); }); // ============================================================ // Listen for Background broadcasts // ============================================================ chrome.runtime.onMessage.addListener((message) => { switch (message.type) { case 'LOG_ENTRY': appendLog(message.payload); if (message.payload.level === 'error') { showToast(message.payload.message, 'error'); } break; case 'STEP_STATUS_CHANGED': { const { step, status } = message.payload; updateStepUI(step, status); chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' }).then(updateStatusDisplay); if (status === 'completed') { chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' }).then(state => { syncPasswordField(state); if (state.oauthUrl) { displayOauthUrl.textContent = state.oauthUrl; displayOauthUrl.classList.add('has-value'); } if (state.localhostUrl) { displayLocalhostUrl.textContent = state.localhostUrl; displayLocalhostUrl.classList.add('has-value'); } }); } break; } case 'AUTO_RUN_RESET': { // Full UI reset for next run displayOauthUrl.textContent = 'Waiting...'; displayOauthUrl.classList.remove('has-value'); displayLocalhostUrl.textContent = 'Waiting...'; displayLocalhostUrl.classList.remove('has-value'); inputEmail.value = ''; displayStatus.textContent = 'Ready'; statusBar.className = 'status-bar'; logArea.innerHTML = ''; document.querySelectorAll('.step-row').forEach(row => row.className = 'step-row'); document.querySelectorAll('.step-status').forEach(el => el.textContent = ''); updateStopButtonState(false); updateProgressCounter(); break; } case 'DATA_UPDATED': { if (message.payload.email) { inputEmail.value = message.payload.email; } if (message.payload.password !== undefined) { inputPassword.value = message.payload.password || ''; } if (message.payload.oauthUrl) { displayOauthUrl.textContent = message.payload.oauthUrl; displayOauthUrl.classList.add('has-value'); } if (message.payload.localhostUrl) { displayLocalhostUrl.textContent = message.payload.localhostUrl; displayLocalhostUrl.classList.add('has-value'); } break; } case 'AUTO_RUN_STATUS': { const { phase, currentRun, totalRuns } = message.payload; const runLabel = totalRuns > 1 ? ` (${currentRun}/${totalRuns})` : ''; switch (phase) { case 'waiting_email': autoContinueBar.style.display = 'flex'; btnAutoRun.innerHTML = `Paused${runLabel}`; updateStopButtonState(true); break; case 'running': btnAutoRun.innerHTML = `Running${runLabel}`; updateStopButtonState(true); break; case 'complete': btnAutoRun.disabled = false; inputRunCount.disabled = false; btnAutoRun.innerHTML = ' Auto'; autoContinueBar.style.display = 'none'; updateStopButtonState(false); break; case 'stopped': btnAutoRun.disabled = false; inputRunCount.disabled = false; btnAutoRun.innerHTML = ' Auto'; autoContinueBar.style.display = 'none'; updateStopButtonState(false); break; } break; } } }); // ============================================================ // Theme Toggle // ============================================================ const btnTheme = document.getElementById('btn-theme'); function setTheme(theme) { document.documentElement.setAttribute('data-theme', theme); localStorage.setItem('multipage-theme', theme); } function initTheme() { const saved = localStorage.getItem('multipage-theme'); if (saved) { setTheme(saved); } else if (window.matchMedia('(prefers-color-scheme: dark)').matches) { setTheme('dark'); } } btnTheme.addEventListener('click', () => { const current = document.documentElement.getAttribute('data-theme'); setTheme(current === 'dark' ? 'light' : 'dark'); }); // ============================================================ // Init // ============================================================ initTheme(); restoreState().then(() => { syncPasswordToggleLabel(); updateButtonStates(); });