feat: reuse tabs across multi-run — no more tab bloat
- tabRegistry preserved across resetState (like seenCodes/accounts) - New reuseOrCreateTab() helper: navigates existing tab or creates new one - All steps use reuseOrCreateTab: VPS panel, auth page, mail - Mail tabs: only activate if alive, don't re-navigate (preserves session) - Auth tab: reused between step 2→6→8, navigated to new URLs - VPS tab: reused across runs, re-injected on navigation
This commit is contained in:
+85
-79
@@ -39,13 +39,14 @@ async function setState(updates) {
|
|||||||
|
|
||||||
async function resetState() {
|
async function resetState() {
|
||||||
console.log(LOG_PREFIX, 'Resetting all state');
|
console.log(LOG_PREFIX, 'Resetting all state');
|
||||||
// Preserve seenCodes and accounts across resets
|
// Preserve seenCodes, accounts, and tabRegistry across resets
|
||||||
const prev = await chrome.storage.session.get(['seenCodes', 'accounts']);
|
const prev = await chrome.storage.session.get(['seenCodes', 'accounts', 'tabRegistry']);
|
||||||
await chrome.storage.session.clear();
|
await chrome.storage.session.clear();
|
||||||
await chrome.storage.session.set({
|
await chrome.storage.session.set({
|
||||||
...DEFAULT_STATE,
|
...DEFAULT_STATE,
|
||||||
seenCodes: prev.seenCodes || [],
|
seenCodes: prev.seenCodes || [],
|
||||||
accounts: prev.accounts || [],
|
accounts: prev.accounts || [],
|
||||||
|
tabRegistry: prev.tabRegistry || {},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,6 +141,61 @@ function flushCommand(source, tabId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Reuse or create tab
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
async function reuseOrCreateTab(source, url, options = {}) {
|
||||||
|
const alive = await isTabAlive(source);
|
||||||
|
if (alive) {
|
||||||
|
const tabId = await getTabId(source);
|
||||||
|
// Navigate existing tab to new URL (or just activate if same URL)
|
||||||
|
await chrome.tabs.update(tabId, { url, active: true });
|
||||||
|
console.log(LOG_PREFIX, `Reused tab ${source} (${tabId}), navigated to ${url.slice(0, 60)}`);
|
||||||
|
|
||||||
|
// Wait for page load
|
||||||
|
await new Promise(resolve => {
|
||||||
|
const listener = (tid, info) => {
|
||||||
|
if (tid === tabId && info.status === 'complete') {
|
||||||
|
chrome.tabs.onUpdated.removeListener(listener);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
chrome.tabs.onUpdated.addListener(listener);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mark as not ready — content script will re-inject and send READY
|
||||||
|
const registry = await getTabRegistry();
|
||||||
|
if (registry[source]) registry[source].ready = false;
|
||||||
|
await setState({ tabRegistry: registry });
|
||||||
|
|
||||||
|
return tabId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new tab
|
||||||
|
const tab = await chrome.tabs.create({ url, active: true });
|
||||||
|
console.log(LOG_PREFIX, `Created new tab ${source} (${tab.id})`);
|
||||||
|
|
||||||
|
// If dynamic injection needed (VPS panel), inject scripts after load
|
||||||
|
if (options.inject) {
|
||||||
|
await new Promise(resolve => {
|
||||||
|
const listener = (tabId, info) => {
|
||||||
|
if (tabId === tab.id && info.status === 'complete') {
|
||||||
|
chrome.tabs.onUpdated.removeListener(listener);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
chrome.tabs.onUpdated.addListener(listener);
|
||||||
|
});
|
||||||
|
await chrome.scripting.executeScript({
|
||||||
|
target: { tabId: tab.id },
|
||||||
|
files: options.inject,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return tab.id;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Send command to content script (with readiness check)
|
// Send command to content script (with readiness check)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -526,35 +582,11 @@ async function resumeAutoRun() {
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
async function executeStep1(state) {
|
async function executeStep1(state) {
|
||||||
const vpsUrl = state.vpsUrl;
|
if (!state.vpsUrl) {
|
||||||
if (!vpsUrl) {
|
|
||||||
throw new Error('No VPS URL configured. Enter VPS address in Side Panel first.');
|
throw new Error('No VPS URL configured. Enter VPS address in Side Panel first.');
|
||||||
}
|
}
|
||||||
|
await addLog(`Step 1: Opening VPS panel...`);
|
||||||
// Ensure VPS panel tab is open
|
await reuseOrCreateTab('vps-panel', state.vpsUrl, { inject: ['content/utils.js', 'content/vps-panel.js'] });
|
||||||
const alive = await isTabAlive('vps-panel');
|
|
||||||
if (!alive) {
|
|
||||||
await addLog(`Step 1: Opening VPS panel: ${vpsUrl.slice(0, 60)}...`);
|
|
||||||
const tab = await chrome.tabs.create({ url: vpsUrl, active: true });
|
|
||||||
// Dynamically inject content scripts since VPS URL is configurable
|
|
||||||
// Wait for page to load, then inject
|
|
||||||
await new Promise(resolve => {
|
|
||||||
const listener = (tabId, info) => {
|
|
||||||
if (tabId === tab.id && info.status === 'complete') {
|
|
||||||
chrome.tabs.onUpdated.removeListener(listener);
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
chrome.tabs.onUpdated.addListener(listener);
|
|
||||||
});
|
|
||||||
await chrome.scripting.executeScript({
|
|
||||||
target: { tabId: tab.id },
|
|
||||||
files: ['content/utils.js', 'content/vps-panel.js'],
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const tabId = await getTabId('vps-panel');
|
|
||||||
if (tabId) await chrome.tabs.update(tabId, { active: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
await sendToContentScript('vps-panel', {
|
await sendToContentScript('vps-panel', {
|
||||||
type: 'EXECUTE_STEP',
|
type: 'EXECUTE_STEP',
|
||||||
@@ -572,10 +604,9 @@ async function executeStep2(state) {
|
|||||||
if (!state.oauthUrl) {
|
if (!state.oauthUrl) {
|
||||||
throw new Error('No OAuth URL. Complete step 1 first.');
|
throw new Error('No OAuth URL. Complete step 1 first.');
|
||||||
}
|
}
|
||||||
await addLog(`Step 2: Opening auth URL in new tab: ${state.oauthUrl.slice(0, 80)}...`);
|
await addLog(`Step 2: Opening auth URL...`);
|
||||||
const tab = await chrome.tabs.create({ url: state.oauthUrl, active: true });
|
await reuseOrCreateTab('signup-page', state.oauthUrl);
|
||||||
// signup-page.js will auto-inject via manifest content_scripts
|
|
||||||
// Queue the command — it will flush when script sends READY signal
|
|
||||||
await sendToContentScript('signup-page', {
|
await sendToContentScript('signup-page', {
|
||||||
type: 'EXECUTE_STEP',
|
type: 'EXECUTE_STEP',
|
||||||
step: 2,
|
step: 2,
|
||||||
@@ -625,14 +656,15 @@ function getMailConfig(state) {
|
|||||||
|
|
||||||
async function executeStep4(state) {
|
async function executeStep4(state) {
|
||||||
const mail = getMailConfig(state);
|
const mail = getMailConfig(state);
|
||||||
|
await addLog(`Step 4: Opening ${mail.label}...`);
|
||||||
|
|
||||||
|
// For mail tabs, only create if not alive — don't navigate (preserves login session)
|
||||||
const alive = await isTabAlive(mail.source);
|
const alive = await isTabAlive(mail.source);
|
||||||
if (!alive) {
|
if (alive) {
|
||||||
await addLog(`Step 4: Opening ${mail.label}...`);
|
|
||||||
await chrome.tabs.create({ url: mail.url, active: true });
|
|
||||||
} else {
|
|
||||||
const tabId = await getTabId(mail.source);
|
const tabId = await getTabId(mail.source);
|
||||||
if (tabId) await chrome.tabs.update(tabId, { active: true });
|
await chrome.tabs.update(tabId, { active: true });
|
||||||
|
} else {
|
||||||
|
await reuseOrCreateTab(mail.source, mail.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await sendToContentScript(mail.source, {
|
const result = await sendToContentScript(mail.source, {
|
||||||
@@ -702,15 +734,9 @@ async function executeStep6(state) {
|
|||||||
throw new Error('No email. Complete step 3 first.');
|
throw new Error('No email. Complete step 3 first.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open the OAuth URL again in a new tab to start the login flow
|
await addLog(`Step 6: Opening OAuth URL for login...`);
|
||||||
// Close the old signup tab first (it's on add-phone page, not needed)
|
// Reuse the signup-page tab — navigate it to the OAuth URL
|
||||||
const oldSignupTabId = await getTabId('signup-page');
|
await reuseOrCreateTab('signup-page', state.oauthUrl);
|
||||||
if (oldSignupTabId) {
|
|
||||||
try { await chrome.tabs.remove(oldSignupTabId); } catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
await addLog(`Step 6: Opening OAuth URL for login: ${state.oauthUrl.slice(0, 60)}...`);
|
|
||||||
await chrome.tabs.create({ url: state.oauthUrl, active: true });
|
|
||||||
|
|
||||||
// signup-page.js will inject (same auth.openai.com domain) and handle login
|
// signup-page.js will inject (same auth.openai.com domain) and handle login
|
||||||
await sendToContentScript('signup-page', {
|
await sendToContentScript('signup-page', {
|
||||||
@@ -727,14 +753,14 @@ async function executeStep6(state) {
|
|||||||
|
|
||||||
async function executeStep7(state) {
|
async function executeStep7(state) {
|
||||||
const mail = getMailConfig(state);
|
const mail = getMailConfig(state);
|
||||||
|
await addLog(`Step 7: Opening ${mail.label}...`);
|
||||||
|
|
||||||
const alive = await isTabAlive(mail.source);
|
const alive = await isTabAlive(mail.source);
|
||||||
if (!alive) {
|
if (alive) {
|
||||||
await addLog(`Step 7: Opening ${mail.label}...`);
|
|
||||||
await chrome.tabs.create({ url: mail.url, active: true });
|
|
||||||
} else {
|
|
||||||
const tabId = await getTabId(mail.source);
|
const tabId = await getTabId(mail.source);
|
||||||
if (tabId) await chrome.tabs.update(tabId, { active: true });
|
await chrome.tabs.update(tabId, { active: true });
|
||||||
|
} else {
|
||||||
|
await reuseOrCreateTab(mail.source, mail.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await sendToContentScript(mail.source, {
|
const result = await sendToContentScript(mail.source, {
|
||||||
@@ -835,9 +861,8 @@ async function executeStep8(state) {
|
|||||||
payload: {},
|
payload: {},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Auth tab was closed, reopen OAuth URL
|
await reuseOrCreateTab('signup-page', state.oauthUrl);
|
||||||
await chrome.tabs.create({ url: state.oauthUrl, active: true });
|
await addLog('Step 8: Auth tab reopened...');
|
||||||
await addLog('Step 8: Auth tab closed, reopening OAuth URL...');
|
|
||||||
await sendToContentScript('signup-page', {
|
await sendToContentScript('signup-page', {
|
||||||
type: 'EXECUTE_STEP',
|
type: 'EXECUTE_STEP',
|
||||||
step: 8,
|
step: 8,
|
||||||
@@ -865,32 +890,13 @@ async function executeStep9(state) {
|
|||||||
if (!state.localhostUrl) {
|
if (!state.localhostUrl) {
|
||||||
throw new Error('No localhost URL. Complete step 8 first.');
|
throw new Error('No localhost URL. Complete step 8 first.');
|
||||||
}
|
}
|
||||||
|
if (!state.vpsUrl) {
|
||||||
const vpsUrl = state.vpsUrl || 'http://154.26.182.181:8317/management.html#/oauth';
|
throw new Error('VPS URL not set. Please enter VPS URL in the side panel.');
|
||||||
|
|
||||||
// Switch to VPS panel tab
|
|
||||||
const alive = await isTabAlive('vps-panel');
|
|
||||||
if (!alive) {
|
|
||||||
await addLog('Step 9: Opening VPS panel...');
|
|
||||||
const tab = await chrome.tabs.create({ url: vpsUrl, active: true });
|
|
||||||
await new Promise(resolve => {
|
|
||||||
const listener = (tabId, info) => {
|
|
||||||
if (tabId === tab.id && info.status === 'complete') {
|
|
||||||
chrome.tabs.onUpdated.removeListener(listener);
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
chrome.tabs.onUpdated.addListener(listener);
|
|
||||||
});
|
|
||||||
await chrome.scripting.executeScript({
|
|
||||||
target: { tabId: tab.id },
|
|
||||||
files: ['content/utils.js', 'content/vps-panel.js'],
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const tabId = await getTabId('vps-panel');
|
|
||||||
if (tabId) await chrome.tabs.update(tabId, { active: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await addLog('Step 9: Opening VPS panel...');
|
||||||
|
await reuseOrCreateTab('vps-panel', state.vpsUrl, { inject: ['content/utils.js', 'content/vps-panel.js'] });
|
||||||
|
|
||||||
await sendToContentScript('vps-panel', {
|
await sendToContentScript('vps-panel', {
|
||||||
type: 'EXECUTE_STEP',
|
type: 'EXECUTE_STEP',
|
||||||
step: 9,
|
step: 9,
|
||||||
|
|||||||
+1
-1
@@ -39,7 +39,7 @@ async function step6_loginChatGPT() {
|
|||||||
// Get state for email and password
|
// Get state for email and password
|
||||||
const state = await chrome.runtime.sendMessage({ type: 'GET_STATE' });
|
const state = await chrome.runtime.sendMessage({ type: 'GET_STATE' });
|
||||||
const email = state.email;
|
const email = state.email;
|
||||||
const password = state.password || 'mimashisha0.0';
|
const password = state.password;
|
||||||
|
|
||||||
if (!email) throw new Error('No email found in state. Complete earlier steps first.');
|
if (!email) throw new Error('No email found in state. Complete earlier steps first.');
|
||||||
|
|
||||||
|
|||||||
+37
-25
@@ -184,37 +184,49 @@ async function deleteEmail(item, step) {
|
|||||||
try {
|
try {
|
||||||
log(`Step ${step}: Deleting email...`);
|
log(`Step ${step}: Deleting email...`);
|
||||||
|
|
||||||
// Right-click on the mail item to trigger context menu
|
// Strategy 1: Click the trash icon inside the mail item
|
||||||
const rect = item.getBoundingClientRect();
|
// Each mail item has: <b class="nui-ico nui-ico-delete" title="删除邮件" sign="trash">
|
||||||
item.dispatchEvent(new MouseEvent('contextmenu', {
|
// These icons appear on hover, so we trigger mouseover first
|
||||||
bubbles: true, cancelable: true, button: 2,
|
item.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
|
||||||
clientX: rect.left + rect.width / 2,
|
item.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
|
||||||
clientY: rect.top + rect.height / 2,
|
await sleep(300);
|
||||||
}));
|
|
||||||
|
|
||||||
// Wait for context menu to appear
|
const trashIcon = item.querySelector('[sign="trash"], .nui-ico-delete, [title="删除邮件"]');
|
||||||
let deleteMenuItem = null;
|
if (trashIcon) {
|
||||||
for (let i = 0; i < 10; i++) {
|
trashIcon.click();
|
||||||
|
log(`Step ${step}: Clicked trash icon`, 'ok');
|
||||||
|
await sleep(1500);
|
||||||
|
|
||||||
|
// Check if item disappeared (confirm deletion)
|
||||||
|
const stillExists = document.getElementById(item.id);
|
||||||
|
if (!stillExists || stillExists.style.display === 'none') {
|
||||||
|
log(`Step ${step}: Email deleted successfully`);
|
||||||
|
} else {
|
||||||
|
log(`Step ${step}: Email may not have been deleted, item still visible`, 'warn');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 2: Select checkbox then click toolbar delete button
|
||||||
|
log(`Step ${step}: Trash icon not found, trying checkbox + toolbar delete...`);
|
||||||
|
const checkbox = item.querySelector('[sign="checkbox"], .nui-chk');
|
||||||
|
if (checkbox) {
|
||||||
|
checkbox.click();
|
||||||
await sleep(300);
|
await sleep(300);
|
||||||
const menuItems = document.querySelectorAll('.nui-menu-item .nui-menu-item-text');
|
|
||||||
for (const mi of menuItems) {
|
// Click toolbar delete button
|
||||||
if (mi.textContent.trim() === '删除邮件') {
|
const toolbarBtns = document.querySelectorAll('.nui-btn .nui-btn-text');
|
||||||
deleteMenuItem = mi.closest('.nui-menu-item');
|
for (const btn of toolbarBtns) {
|
||||||
break;
|
if (btn.textContent.replace(/\s/g, '').includes('删除')) {
|
||||||
|
btn.closest('.nui-btn').click();
|
||||||
|
log(`Step ${step}: Clicked toolbar delete`, 'ok');
|
||||||
|
await sleep(1500);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (deleteMenuItem) break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deleteMenuItem) {
|
log(`Step ${step}: Could not delete email (no delete button found)`, 'warn');
|
||||||
deleteMenuItem.click();
|
|
||||||
log(`Step ${step}: Clicked "删除邮件"`, 'ok');
|
|
||||||
// Wait for the delete to process and item to disappear
|
|
||||||
await sleep(1000);
|
|
||||||
log(`Step ${step}: Email deleted successfully`);
|
|
||||||
} else {
|
|
||||||
log(`Step ${step}: Context menu "删除邮件" not found`, 'warn');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(`Step ${step}: Failed to delete email: ${err.message}`, 'warn');
|
log(`Step ${step}: Failed to delete email: ${err.message}`, 'warn');
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-3
@@ -111,7 +111,8 @@ async function step3_fillEmailPassword(payload) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fillInput(passwordInput, payload.password || 'mimashisha0.0');
|
if (!payload.password) throw new Error('No password provided. Step 3 requires a generated password.');
|
||||||
|
fillInput(passwordInput, payload.password);
|
||||||
log('Step 3: Password filled');
|
log('Step 3: Password filled');
|
||||||
|
|
||||||
// Report complete BEFORE submit, because submit causes page navigation
|
// Report complete BEFORE submit, because submit causes page navigation
|
||||||
@@ -265,8 +266,20 @@ async function step8_clickContinue() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log('Step 8: Found "继续" button, clicking...');
|
log('Step 8: Found "继续" button, clicking...');
|
||||||
simulateClick(continueBtn);
|
|
||||||
log('Step 8: Clicked "继续", redirecting to localhost... (background will capture URL)');
|
// Use native .click() — simulateClick (dispatchEvent) may not trigger form submit
|
||||||
|
continueBtn.click();
|
||||||
|
log('Step 8: Clicked via .click()');
|
||||||
|
|
||||||
|
// Also try submitting the form directly as a fallback
|
||||||
|
await sleep(500);
|
||||||
|
const form = continueBtn.closest('form');
|
||||||
|
if (form) {
|
||||||
|
form.requestSubmit(continueBtn);
|
||||||
|
log('Step 8: Also triggered form.requestSubmit()');
|
||||||
|
}
|
||||||
|
|
||||||
|
log('Step 8: Redirecting to localhost... (background will capture URL)');
|
||||||
|
|
||||||
// Don't reportComplete — background handles it via webNavigation listener
|
// Don't reportComplete — background handles it via webNavigation listener
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -541,6 +541,94 @@ header {
|
|||||||
|
|
||||||
.log-line { animation: fadeIn 120ms ease-out; }
|
.log-line { animation: fadeIn 120ms ease-out; }
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Toast Notifications
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
#toast-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 12px;
|
||||||
|
left: 12px;
|
||||||
|
right: 12px;
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
box-shadow: var(--shadow-md), 0 4px 12px rgba(0,0,0,0.1);
|
||||||
|
pointer-events: auto;
|
||||||
|
animation: toastIn 250ms ease-out;
|
||||||
|
border: 1px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.toast-exit {
|
||||||
|
animation: toastOut 200ms ease-in forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-error {
|
||||||
|
background: var(--red-soft);
|
||||||
|
border-color: var(--red);
|
||||||
|
color: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-warn {
|
||||||
|
background: var(--orange-soft);
|
||||||
|
border-color: var(--orange);
|
||||||
|
color: var(--orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-success {
|
||||||
|
background: var(--green-soft);
|
||||||
|
border-color: var(--green);
|
||||||
|
color: var(--green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-info {
|
||||||
|
background: var(--blue-soft);
|
||||||
|
border-color: var(--blue);
|
||||||
|
color: var(--blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast svg {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-msg {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: inherit;
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.toast-close:hover { opacity: 1; }
|
||||||
|
|
||||||
|
@keyframes toastIn {
|
||||||
|
from { opacity: 0; transform: translateY(-10px) scale(0.96); }
|
||||||
|
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes toastOut {
|
||||||
|
from { opacity: 1; transform: translateY(0) scale(1); }
|
||||||
|
to { opacity: 0; transform: translateY(-10px) scale(0.96); }
|
||||||
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
*, *::before, *::after {
|
*, *::before, *::after {
|
||||||
animation-duration: 0.01ms !important;
|
animation-duration: 0.01ms !important;
|
||||||
|
|||||||
@@ -140,6 +140,7 @@
|
|||||||
<div id="log-area"></div>
|
<div id="log-area"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<div id="toast-container"></div>
|
||||||
<script src="sidepanel.js"></script>
|
<script src="sidepanel.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+37
-2
@@ -23,6 +23,38 @@ const inputVpsUrl = document.getElementById('input-vps-url');
|
|||||||
const selectMailProvider = document.getElementById('select-mail-provider');
|
const selectMailProvider = document.getElementById('select-mail-provider');
|
||||||
const inputRunCount = document.getElementById('input-run-count');
|
const inputRunCount = document.getElementById('input-run-count');
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Toast Notifications
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
const toastContainer = document.getElementById('toast-container');
|
||||||
|
|
||||||
|
const TOAST_ICONS = {
|
||||||
|
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>',
|
||||||
|
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>',
|
||||||
|
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>',
|
||||||
|
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>',
|
||||||
|
};
|
||||||
|
|
||||||
|
function showToast(message, type = 'error', duration = 4000) {
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = `toast toast-${type}`;
|
||||||
|
toast.innerHTML = `${TOAST_ICONS[type] || ''}<span class="toast-msg">${escapeHtml(message)}</span><button class="toast-close">×</button>`;
|
||||||
|
|
||||||
|
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
|
// State Restore on load
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -193,7 +225,7 @@ document.querySelectorAll('.step-btn').forEach(btn => {
|
|||||||
if (step === 3) {
|
if (step === 3) {
|
||||||
const email = inputEmail.value.trim();
|
const email = inputEmail.value.trim();
|
||||||
if (!email) {
|
if (!email) {
|
||||||
appendLog({ message: 'Please paste email address first', level: 'error', timestamp: Date.now() });
|
showToast('Please paste email address first', 'warn');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await chrome.runtime.sendMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step, email } });
|
await chrome.runtime.sendMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step, email } });
|
||||||
@@ -215,7 +247,7 @@ btnAutoRun.addEventListener('click', async () => {
|
|||||||
btnAutoContinue.addEventListener('click', async () => {
|
btnAutoContinue.addEventListener('click', async () => {
|
||||||
const email = inputEmail.value.trim();
|
const email = inputEmail.value.trim();
|
||||||
if (!email) {
|
if (!email) {
|
||||||
appendLog({ message: 'Please paste DuckDuckGo email first!', level: 'error', timestamp: Date.now() });
|
showToast('Please paste DuckDuckGo email first!', 'warn');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
autoContinueBar.style.display = 'none';
|
autoContinueBar.style.display = 'none';
|
||||||
@@ -279,6 +311,9 @@ chrome.runtime.onMessage.addListener((message) => {
|
|||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case 'LOG_ENTRY':
|
case 'LOG_ENTRY':
|
||||||
appendLog(message.payload);
|
appendLog(message.payload);
|
||||||
|
if (message.payload.level === 'error') {
|
||||||
|
showToast(message.payload.message, 'error');
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'STEP_STATUS_CHANGED': {
|
case 'STEP_STATUS_CHANGED': {
|
||||||
|
|||||||
Reference in New Issue
Block a user