sidepanel.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723
  1. // sidepanel/sidepanel.js — Side Panel logic
  2. const STATUS_ICONS = {
  3. pending: '',
  4. running: '',
  5. completed: '\u2713', // ✓
  6. failed: '\u2717', // ✗
  7. stopped: '\u25A0', // ■
  8. };
  9. const logArea = document.getElementById('log-area');
  10. const displayOauthUrl = document.getElementById('display-oauth-url');
  11. const displayLocalhostUrl = document.getElementById('display-localhost-url');
  12. const displayStatus = document.getElementById('display-status');
  13. const statusBar = document.getElementById('status-bar');
  14. const inputEmail = document.getElementById('input-email');
  15. const inputPassword = document.getElementById('input-password');
  16. const btnFetchEmail = document.getElementById('btn-fetch-email');
  17. const btnTogglePassword = document.getElementById('btn-toggle-password');
  18. const btnStop = document.getElementById('btn-stop');
  19. const btnReset = document.getElementById('btn-reset');
  20. const stepsProgress = document.getElementById('steps-progress');
  21. const btnAutoRun = document.getElementById('btn-auto-run');
  22. const btnAutoContinue = document.getElementById('btn-auto-continue');
  23. const autoContinueBar = document.getElementById('auto-continue-bar');
  24. const btnClearLog = document.getElementById('btn-clear-log');
  25. const inputVpsUrl = document.getElementById('input-vps-url');
  26. const selectMailProvider = document.getElementById('select-mail-provider');
  27. const rowGmailBaseEmail = document.getElementById('row-gmail-base-email');
  28. const inputGmailBaseEmail = document.getElementById('input-gmail-base-email');
  29. const rowGmailClientId = document.getElementById('row-gmail-client-id');
  30. const inputGmailClientId = document.getElementById('input-gmail-client-id');
  31. const rowGmailClientSecret = document.getElementById('row-gmail-client-secret');
  32. const inputGmailClientSecret = document.getElementById('input-gmail-client-secret');
  33. const rowGmailRefreshToken = document.getElementById('row-gmail-refresh-token');
  34. const inputGmailRefreshToken = document.getElementById('input-gmail-refresh-token');
  35. const rowGmailApiToken = document.getElementById('row-gmail-api-token');
  36. const inputGmailApiToken = document.getElementById('input-gmail-api-token');
  37. const rowTempApiUrl = document.getElementById('row-temp-api-url');
  38. const inputTempApiUrl = document.getElementById('input-temp-api-url');
  39. const rowInbucketHost = document.getElementById('row-inbucket-host');
  40. const inputInbucketHost = document.getElementById('input-inbucket-host');
  41. const rowInbucketMailbox = document.getElementById('row-inbucket-mailbox');
  42. const inputInbucketMailbox = document.getElementById('input-inbucket-mailbox');
  43. const rowTempApiMails = document.getElementById('row-temp-api-mails');
  44. const selectTempApiMailsPath = document.getElementById('select-temp-api-mails-path');
  45. const rowTempApiAdminAuth = document.getElementById('row-temp-api-admin-auth');
  46. const rowTempApiCustomAuth = document.getElementById('row-temp-api-custom-auth');
  47. const rowTempApiFingerprint = document.getElementById('row-temp-api-fingerprint');
  48. const rowTempApiUserToken = document.getElementById('row-temp-api-user-token');
  49. const inputTempApiAdminAuth = document.getElementById('input-temp-api-admin-auth');
  50. const inputTempApiCustomAuth = document.getElementById('input-temp-api-custom-auth');
  51. const inputTempApiFingerprint = document.getElementById('input-temp-api-fingerprint');
  52. const inputTempApiUserToken = document.getElementById('input-temp-api-user-token');
  53. const inputRunCount = document.getElementById('input-run-count');
  54. // ============================================================
  55. // Toast Notifications
  56. // ============================================================
  57. const toastContainer = document.getElementById('toast-container');
  58. const TOAST_ICONS = {
  59. error: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>',
  60. warn: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
  61. success: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>',
  62. info: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>',
  63. };
  64. function showToast(message, type = 'error', duration = 4000) {
  65. const toast = document.createElement('div');
  66. toast.className = `toast toast-${type}`;
  67. toast.innerHTML = `${TOAST_ICONS[type] || ''}<span class="toast-msg">${escapeHtml(message)}</span><button class="toast-close">&times;</button>`;
  68. toast.querySelector('.toast-close').addEventListener('click', () => dismissToast(toast));
  69. toastContainer.appendChild(toast);
  70. if (duration > 0) {
  71. setTimeout(() => dismissToast(toast), duration);
  72. }
  73. }
  74. function dismissToast(toast) {
  75. if (!toast.parentNode) return;
  76. toast.classList.add('toast-exit');
  77. toast.addEventListener('animationend', () => toast.remove());
  78. }
  79. // ============================================================
  80. // State Restore on load
  81. // ============================================================
  82. async function restoreState() {
  83. try {
  84. const state = await chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' });
  85. if (state.oauthUrl) {
  86. displayOauthUrl.textContent = state.oauthUrl;
  87. displayOauthUrl.classList.add('has-value');
  88. }
  89. if (state.localhostUrl) {
  90. displayLocalhostUrl.textContent = state.localhostUrl;
  91. displayLocalhostUrl.classList.add('has-value');
  92. }
  93. if (state.email) {
  94. inputEmail.value = state.email;
  95. }
  96. syncPasswordField(state);
  97. if (state.vpsUrl) {
  98. inputVpsUrl.value = state.vpsUrl;
  99. }
  100. if (state.mailProvider) {
  101. selectMailProvider.value = state.mailProvider;
  102. }
  103. if (state.gmailBaseEmail) {
  104. inputGmailBaseEmail.value = state.gmailBaseEmail;
  105. }
  106. if (state.gmailApiClientId) {
  107. inputGmailClientId.value = state.gmailApiClientId;
  108. }
  109. if (state.gmailApiClientSecret) {
  110. inputGmailClientSecret.value = state.gmailApiClientSecret;
  111. }
  112. if (state.gmailApiRefreshToken) {
  113. inputGmailRefreshToken.value = state.gmailApiRefreshToken;
  114. }
  115. if (state.gmailApiAccessToken) {
  116. inputGmailApiToken.value = state.gmailApiAccessToken;
  117. }
  118. if (state.tempApiUrl) {
  119. inputTempApiUrl.value = state.tempApiUrl;
  120. }
  121. if (state.tempApiMailsPath) {
  122. selectTempApiMailsPath.value = state.tempApiMailsPath;
  123. }
  124. if (state.tempApiAdminAuth) {
  125. inputTempApiAdminAuth.value = state.tempApiAdminAuth;
  126. }
  127. if (state.tempApiCustomAuth) {
  128. inputTempApiCustomAuth.value = state.tempApiCustomAuth;
  129. }
  130. if (state.tempApiFingerprint) {
  131. inputTempApiFingerprint.value = state.tempApiFingerprint;
  132. }
  133. if (state.tempApiUserToken) {
  134. inputTempApiUserToken.value = state.tempApiUserToken;
  135. }
  136. if (state.inbucketHost) {
  137. inputInbucketHost.value = state.inbucketHost;
  138. }
  139. if (state.inbucketMailbox) {
  140. inputInbucketMailbox.value = state.inbucketMailbox;
  141. }
  142. if (state.stepStatuses) {
  143. for (const [step, status] of Object.entries(state.stepStatuses)) {
  144. updateStepUI(Number(step), status);
  145. }
  146. }
  147. if (state.logs) {
  148. for (const entry of state.logs) {
  149. appendLog(entry);
  150. }
  151. }
  152. updateStatusDisplay(state);
  153. updateProgressCounter();
  154. updateMailProviderUI();
  155. // Sync temp API URL from UI to state (handles browser autocomplete / form restore)
  156. const currentTempApiUrl = inputTempApiUrl.value.trim();
  157. if (currentTempApiUrl && currentTempApiUrl !== (state.tempApiUrl || '')) {
  158. await chrome.runtime.sendMessage({
  159. type: 'SAVE_SETTING',
  160. source: 'sidepanel',
  161. payload: { tempApiUrl: currentTempApiUrl },
  162. });
  163. }
  164. } catch (err) {
  165. console.error('Failed to restore state:', err);
  166. }
  167. }
  168. function syncPasswordField(state) {
  169. inputPassword.value = state.customPassword || state.password || '';
  170. }
  171. function updateMailProviderUI() {
  172. const val = selectMailProvider.value;
  173. const isTemp = val === 'temp-api';
  174. const isGmail = val === 'gmail';
  175. rowGmailBaseEmail.style.display = isGmail ? '' : 'none';
  176. rowGmailClientId.style.display = isGmail ? '' : 'none';
  177. rowGmailClientSecret.style.display = isGmail ? '' : 'none';
  178. rowGmailRefreshToken.style.display = isGmail ? '' : 'none';
  179. rowGmailApiToken.style.display = isGmail ? '' : 'none';
  180. rowTempApiUrl.style.display = isTemp ? '' : 'none';
  181. rowTempApiMails.style.display = isTemp ? '' : 'none';
  182. const adminPath = selectTempApiMailsPath.value === 'admin';
  183. rowTempApiAdminAuth.style.display = isTemp && adminPath ? '' : 'none';
  184. rowTempApiCustomAuth.style.display = isTemp && adminPath ? '' : 'none';
  185. rowTempApiFingerprint.style.display = isTemp && adminPath ? '' : 'none';
  186. rowTempApiUserToken.style.display = isTemp && adminPath ? '' : 'none';
  187. rowInbucketHost.style.display = val === 'inbucket' ? '' : 'none';
  188. rowInbucketMailbox.style.display = val === 'inbucket' ? '' : 'none';
  189. }
  190. // ============================================================
  191. // UI Updates
  192. // ============================================================
  193. function updateStepUI(step, status) {
  194. const statusEl = document.querySelector(`.step-status[data-step="${step}"]`);
  195. const row = document.querySelector(`.step-row[data-step="${step}"]`);
  196. if (statusEl) statusEl.textContent = STATUS_ICONS[status] || '';
  197. if (row) {
  198. row.className = `step-row ${status}`;
  199. }
  200. updateButtonStates();
  201. updateProgressCounter();
  202. }
  203. function updateProgressCounter() {
  204. let completed = 0;
  205. document.querySelectorAll('.step-row').forEach(row => {
  206. if (row.classList.contains('completed')) completed++;
  207. });
  208. stepsProgress.textContent = `${completed} / 9`;
  209. }
  210. function updateButtonStates() {
  211. const statuses = {};
  212. document.querySelectorAll('.step-row').forEach(row => {
  213. const step = Number(row.dataset.step);
  214. if (row.classList.contains('completed')) statuses[step] = 'completed';
  215. else if (row.classList.contains('running')) statuses[step] = 'running';
  216. else if (row.classList.contains('failed')) statuses[step] = 'failed';
  217. else if (row.classList.contains('stopped')) statuses[step] = 'stopped';
  218. else statuses[step] = 'pending';
  219. });
  220. const anyRunning = Object.values(statuses).some(s => s === 'running');
  221. for (let step = 1; step <= 9; step++) {
  222. const btn = document.querySelector(`.step-btn[data-step="${step}"]`);
  223. if (!btn) continue;
  224. if (anyRunning) {
  225. btn.disabled = true;
  226. } else if (step === 1) {
  227. btn.disabled = false;
  228. } else {
  229. const prevStatus = statuses[step - 1];
  230. const currentStatus = statuses[step];
  231. btn.disabled = !(prevStatus === 'completed' || currentStatus === 'failed' || currentStatus === 'completed' || currentStatus === 'stopped');
  232. }
  233. }
  234. updateStopButtonState(anyRunning || autoContinueBar.style.display !== 'none');
  235. }
  236. function updateStopButtonState(active) {
  237. btnStop.disabled = !active;
  238. }
  239. function updateStatusDisplay(state) {
  240. if (!state || !state.stepStatuses) return;
  241. statusBar.className = 'status-bar';
  242. const running = Object.entries(state.stepStatuses).find(([, s]) => s === 'running');
  243. if (running) {
  244. displayStatus.textContent = `Step ${running[0]} running...`;
  245. statusBar.classList.add('running');
  246. return;
  247. }
  248. const failed = Object.entries(state.stepStatuses).find(([, s]) => s === 'failed');
  249. if (failed) {
  250. displayStatus.textContent = `Step ${failed[0]} failed`;
  251. statusBar.classList.add('failed');
  252. return;
  253. }
  254. const stopped = Object.entries(state.stepStatuses).find(([, s]) => s === 'stopped');
  255. if (stopped) {
  256. displayStatus.textContent = `Step ${stopped[0]} stopped`;
  257. statusBar.classList.add('stopped');
  258. return;
  259. }
  260. const lastCompleted = Object.entries(state.stepStatuses)
  261. .filter(([, s]) => s === 'completed')
  262. .map(([k]) => Number(k))
  263. .sort((a, b) => b - a)[0];
  264. if (lastCompleted === 9) {
  265. displayStatus.textContent = 'All steps completed!';
  266. statusBar.classList.add('completed');
  267. } else if (lastCompleted) {
  268. displayStatus.textContent = `Step ${lastCompleted} done`;
  269. } else {
  270. displayStatus.textContent = 'Ready';
  271. }
  272. }
  273. function appendLog(entry) {
  274. const time = new Date(entry.timestamp).toLocaleTimeString('en-US', { hour12: false });
  275. const levelLabel = entry.level.toUpperCase();
  276. const line = document.createElement('div');
  277. line.className = `log-line log-${entry.level}`;
  278. const stepMatch = entry.message.match(/Step (\d)/);
  279. const stepNum = stepMatch ? stepMatch[1] : null;
  280. let html = `<span class="log-time">${time}</span> `;
  281. html += `<span class="log-level log-level-${entry.level}">${levelLabel}</span> `;
  282. if (stepNum) {
  283. html += `<span class="log-step-tag step-${stepNum}">S${stepNum}</span>`;
  284. }
  285. html += `<span class="log-msg">${escapeHtml(entry.message)}</span>`;
  286. line.innerHTML = html;
  287. logArea.appendChild(line);
  288. logArea.scrollTop = logArea.scrollHeight;
  289. }
  290. function escapeHtml(text) {
  291. const div = document.createElement('div');
  292. div.textContent = text;
  293. return div.innerHTML;
  294. }
  295. async function generateYibasiEmail() {
  296. const defaultLabel = '随机';
  297. btnFetchEmail.disabled = true;
  298. btnFetchEmail.textContent = '...';
  299. try {
  300. const response = await chrome.runtime.sendMessage({
  301. type: 'GENERATE_YIBASI_EMAIL',
  302. source: 'sidepanel',
  303. payload: {},
  304. });
  305. if (response?.error) {
  306. throw new Error(response.error);
  307. }
  308. if (!response?.email) {
  309. throw new Error('未生成邮箱。');
  310. }
  311. inputEmail.value = response.email;
  312. showToast(`已生成 ${response.email}`, 'success', 2500);
  313. return response.email;
  314. } catch (err) {
  315. showToast(`生成失败: ${err.message}`, 'error');
  316. throw err;
  317. } finally {
  318. btnFetchEmail.disabled = false;
  319. btnFetchEmail.textContent = defaultLabel;
  320. }
  321. }
  322. function syncPasswordToggleLabel() {
  323. btnTogglePassword.textContent = inputPassword.type === 'password' ? 'Show' : 'Hide';
  324. }
  325. // ============================================================
  326. // Button Handlers
  327. // ============================================================
  328. document.querySelectorAll('.step-btn').forEach(btn => {
  329. btn.addEventListener('click', async () => {
  330. const step = Number(btn.dataset.step);
  331. if (step === 3) {
  332. const email = inputEmail.value.trim();
  333. if (!email) {
  334. showToast('请先点击「随机」生成当前来源邮箱或手动粘贴', 'warn');
  335. return;
  336. }
  337. await chrome.runtime.sendMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step, email } });
  338. } else {
  339. await chrome.runtime.sendMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step } });
  340. }
  341. });
  342. });
  343. btnFetchEmail.addEventListener('click', async () => {
  344. await generateYibasiEmail().catch(() => {});
  345. });
  346. btnTogglePassword.addEventListener('click', () => {
  347. inputPassword.type = inputPassword.type === 'password' ? 'text' : 'password';
  348. syncPasswordToggleLabel();
  349. });
  350. btnStop.addEventListener('click', async () => {
  351. btnStop.disabled = true;
  352. await chrome.runtime.sendMessage({ type: 'STOP_FLOW', source: 'sidepanel', payload: {} });
  353. showToast('Stopping current flow...', 'warn', 2000);
  354. });
  355. // Auto Run
  356. btnAutoRun.addEventListener('click', async () => {
  357. const totalRuns = parseInt(inputRunCount.value) || 1;
  358. btnAutoRun.disabled = true;
  359. inputRunCount.disabled = true;
  360. btnAutoRun.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg> Running...';
  361. await chrome.runtime.sendMessage({ type: 'AUTO_RUN', source: 'sidepanel', payload: { totalRuns } });
  362. });
  363. btnAutoContinue.addEventListener('click', async () => {
  364. const email = inputEmail.value.trim();
  365. if (!email) {
  366. showToast('请先生成或粘贴邮箱', 'warn');
  367. return;
  368. }
  369. autoContinueBar.style.display = 'none';
  370. await chrome.runtime.sendMessage({ type: 'RESUME_AUTO_RUN', source: 'sidepanel', payload: { email } });
  371. });
  372. // Reset
  373. btnReset.addEventListener('click', async () => {
  374. if (confirm('Reset all steps and data?')) {
  375. await chrome.runtime.sendMessage({ type: 'RESET', source: 'sidepanel' });
  376. displayOauthUrl.textContent = 'Waiting...';
  377. displayOauthUrl.classList.remove('has-value');
  378. displayLocalhostUrl.textContent = 'Waiting...';
  379. displayLocalhostUrl.classList.remove('has-value');
  380. inputEmail.value = '';
  381. displayStatus.textContent = 'Ready';
  382. statusBar.className = 'status-bar';
  383. logArea.innerHTML = '';
  384. document.querySelectorAll('.step-row').forEach(row => row.className = 'step-row');
  385. document.querySelectorAll('.step-status').forEach(el => el.textContent = '');
  386. btnAutoRun.disabled = false;
  387. inputRunCount.disabled = false;
  388. btnAutoRun.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg> Auto';
  389. autoContinueBar.style.display = 'none';
  390. updateStopButtonState(false);
  391. updateButtonStates();
  392. updateProgressCounter();
  393. }
  394. });
  395. // Clear log
  396. btnClearLog.addEventListener('click', () => {
  397. logArea.innerHTML = '';
  398. });
  399. // Save settings on change
  400. inputEmail.addEventListener('change', async () => {
  401. const email = inputEmail.value.trim();
  402. if (email) {
  403. await chrome.runtime.sendMessage({ type: 'SAVE_EMAIL', source: 'sidepanel', payload: { email } });
  404. }
  405. });
  406. inputVpsUrl.addEventListener('change', async () => {
  407. const vpsUrl = inputVpsUrl.value.trim();
  408. if (vpsUrl) {
  409. await chrome.runtime.sendMessage({ type: 'SAVE_SETTING', source: 'sidepanel', payload: { vpsUrl } });
  410. }
  411. });
  412. inputPassword.addEventListener('change', async () => {
  413. await chrome.runtime.sendMessage({
  414. type: 'SAVE_SETTING',
  415. source: 'sidepanel',
  416. payload: { customPassword: inputPassword.value },
  417. });
  418. });
  419. selectMailProvider.addEventListener('change', async () => {
  420. updateMailProviderUI();
  421. await chrome.runtime.sendMessage({
  422. type: 'SAVE_SETTING', source: 'sidepanel',
  423. payload: { mailProvider: selectMailProvider.value },
  424. });
  425. });
  426. inputGmailBaseEmail.addEventListener('change', async () => {
  427. await chrome.runtime.sendMessage({
  428. type: 'SAVE_SETTING',
  429. source: 'sidepanel',
  430. payload: { gmailBaseEmail: inputGmailBaseEmail.value.trim() },
  431. });
  432. });
  433. for (const evt of ['change', 'input']) {
  434. inputGmailClientId.addEventListener(evt, async () => {
  435. await chrome.runtime.sendMessage({
  436. type: 'SAVE_SETTING',
  437. source: 'sidepanel',
  438. payload: { gmailApiClientId: inputGmailClientId.value.trim() },
  439. });
  440. });
  441. inputGmailClientSecret.addEventListener(evt, async () => {
  442. await chrome.runtime.sendMessage({
  443. type: 'SAVE_SETTING',
  444. source: 'sidepanel',
  445. payload: { gmailApiClientSecret: inputGmailClientSecret.value.trim() },
  446. });
  447. });
  448. inputGmailRefreshToken.addEventListener(evt, async () => {
  449. await chrome.runtime.sendMessage({
  450. type: 'SAVE_SETTING',
  451. source: 'sidepanel',
  452. payload: { gmailApiRefreshToken: inputGmailRefreshToken.value.trim() },
  453. });
  454. });
  455. inputGmailApiToken.addEventListener(evt, async () => {
  456. await chrome.runtime.sendMessage({
  457. type: 'SAVE_SETTING',
  458. source: 'sidepanel',
  459. payload: { gmailApiAccessToken: inputGmailApiToken.value.trim() },
  460. });
  461. });
  462. }
  463. inputInbucketMailbox.addEventListener('change', async () => {
  464. await chrome.runtime.sendMessage({
  465. type: 'SAVE_SETTING',
  466. source: 'sidepanel',
  467. payload: { inbucketMailbox: inputInbucketMailbox.value.trim() },
  468. });
  469. });
  470. // Save on both 'change' (blur) and 'input' (immediate, e.g. paste)
  471. for (const evt of ['change', 'input']) {
  472. inputTempApiUrl.addEventListener(evt, async () => {
  473. await chrome.runtime.sendMessage({
  474. type: 'SAVE_SETTING',
  475. source: 'sidepanel',
  476. payload: { tempApiUrl: inputTempApiUrl.value.trim() },
  477. });
  478. });
  479. inputTempApiAdminAuth.addEventListener(evt, async () => {
  480. await chrome.runtime.sendMessage({
  481. type: 'SAVE_SETTING',
  482. source: 'sidepanel',
  483. payload: { tempApiAdminAuth: inputTempApiAdminAuth.value.trim() },
  484. });
  485. });
  486. inputTempApiCustomAuth.addEventListener(evt, async () => {
  487. await chrome.runtime.sendMessage({
  488. type: 'SAVE_SETTING',
  489. source: 'sidepanel',
  490. payload: { tempApiCustomAuth: inputTempApiCustomAuth.value.trim() },
  491. });
  492. });
  493. inputTempApiFingerprint.addEventListener(evt, async () => {
  494. await chrome.runtime.sendMessage({
  495. type: 'SAVE_SETTING',
  496. source: 'sidepanel',
  497. payload: { tempApiFingerprint: inputTempApiFingerprint.value.trim() },
  498. });
  499. });
  500. inputTempApiUserToken.addEventListener(evt, async () => {
  501. await chrome.runtime.sendMessage({
  502. type: 'SAVE_SETTING',
  503. source: 'sidepanel',
  504. payload: { tempApiUserToken: inputTempApiUserToken.value.trim() },
  505. });
  506. });
  507. }
  508. selectTempApiMailsPath.addEventListener('change', async () => {
  509. updateMailProviderUI();
  510. await chrome.runtime.sendMessage({
  511. type: 'SAVE_SETTING',
  512. source: 'sidepanel',
  513. payload: { tempApiMailsPath: selectTempApiMailsPath.value },
  514. });
  515. });
  516. inputInbucketHost.addEventListener('change', async () => {
  517. await chrome.runtime.sendMessage({
  518. type: 'SAVE_SETTING',
  519. source: 'sidepanel',
  520. payload: { inbucketHost: inputInbucketHost.value.trim() },
  521. });
  522. });
  523. // ============================================================
  524. // Listen for Background broadcasts
  525. // ============================================================
  526. chrome.runtime.onMessage.addListener((message) => {
  527. switch (message.type) {
  528. case 'LOG_ENTRY':
  529. appendLog(message.payload);
  530. if (message.payload.level === 'error') {
  531. showToast(message.payload.message, 'error');
  532. }
  533. break;
  534. case 'STEP_STATUS_CHANGED': {
  535. const { step, status } = message.payload;
  536. updateStepUI(step, status);
  537. chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' }).then(updateStatusDisplay);
  538. if (status === 'completed') {
  539. chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' }).then(state => {
  540. syncPasswordField(state);
  541. if (state.oauthUrl) {
  542. displayOauthUrl.textContent = state.oauthUrl;
  543. displayOauthUrl.classList.add('has-value');
  544. }
  545. if (state.localhostUrl) {
  546. displayLocalhostUrl.textContent = state.localhostUrl;
  547. displayLocalhostUrl.classList.add('has-value');
  548. }
  549. });
  550. }
  551. break;
  552. }
  553. case 'AUTO_RUN_RESET': {
  554. // Full UI reset for next run
  555. displayOauthUrl.textContent = 'Waiting...';
  556. displayOauthUrl.classList.remove('has-value');
  557. displayLocalhostUrl.textContent = 'Waiting...';
  558. displayLocalhostUrl.classList.remove('has-value');
  559. inputEmail.value = '';
  560. displayStatus.textContent = 'Ready';
  561. statusBar.className = 'status-bar';
  562. logArea.innerHTML = '';
  563. document.querySelectorAll('.step-row').forEach(row => row.className = 'step-row');
  564. document.querySelectorAll('.step-status').forEach(el => el.textContent = '');
  565. updateStopButtonState(false);
  566. updateProgressCounter();
  567. break;
  568. }
  569. case 'DATA_UPDATED': {
  570. if (message.payload.email) {
  571. inputEmail.value = message.payload.email;
  572. }
  573. if (message.payload.password !== undefined) {
  574. inputPassword.value = message.payload.password || '';
  575. }
  576. if (message.payload.oauthUrl) {
  577. displayOauthUrl.textContent = message.payload.oauthUrl;
  578. displayOauthUrl.classList.add('has-value');
  579. }
  580. if (message.payload.localhostUrl) {
  581. displayLocalhostUrl.textContent = message.payload.localhostUrl;
  582. displayLocalhostUrl.classList.add('has-value');
  583. }
  584. break;
  585. }
  586. case 'AUTO_RUN_STATUS': {
  587. const { phase, currentRun, totalRuns, successCount } = message.payload;
  588. const successLabel = successCount > 0 ? ` ok:${successCount}` : '';
  589. switch (phase) {
  590. case 'waiting_email':
  591. autoContinueBar.style.display = 'flex';
  592. btnAutoRun.innerHTML = `Paused${successLabel}`;
  593. updateStopButtonState(true);
  594. break;
  595. case 'running':
  596. btnAutoRun.innerHTML = `Running${successLabel}`;
  597. updateStopButtonState(true);
  598. break;
  599. case 'complete':
  600. btnAutoRun.disabled = false;
  601. inputRunCount.disabled = false;
  602. btnAutoRun.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg> Auto';
  603. autoContinueBar.style.display = 'none';
  604. updateStopButtonState(false);
  605. break;
  606. case 'stopped':
  607. btnAutoRun.disabled = false;
  608. inputRunCount.disabled = false;
  609. btnAutoRun.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg> Auto';
  610. autoContinueBar.style.display = 'none';
  611. updateStopButtonState(false);
  612. break;
  613. }
  614. break;
  615. }
  616. }
  617. });
  618. // ============================================================
  619. // Theme Toggle
  620. // ============================================================
  621. const btnTheme = document.getElementById('btn-theme');
  622. function setTheme(theme) {
  623. document.documentElement.setAttribute('data-theme', theme);
  624. localStorage.setItem('multipage-theme', theme);
  625. }
  626. function initTheme() {
  627. const saved = localStorage.getItem('multipage-theme');
  628. if (saved) {
  629. setTheme(saved);
  630. } else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
  631. setTheme('dark');
  632. }
  633. }
  634. btnTheme.addEventListener('click', () => {
  635. const current = document.documentElement.getAttribute('data-theme');
  636. setTheme(current === 'dark' ? 'light' : 'dark');
  637. });
  638. // ============================================================
  639. // Init
  640. // ============================================================
  641. initTheme();
  642. restoreState().then(() => {
  643. syncPasswordToggleLabel();
  644. updateButtonStates();
  645. });