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:
unknown
2026-04-05 10:49:40 +08:00
parent 69b5b780f8
commit 1286130023
7 changed files with 265 additions and 110 deletions
+88
View File
@@ -541,6 +541,94 @@ header {
.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) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
+1
View File
@@ -140,6 +140,7 @@
<div id="log-area"></div>
</section>
<div id="toast-container"></div>
<script src="sidepanel.js"></script>
</body>
</html>
+37 -2
View File
@@ -23,6 +23,38 @@ const inputVpsUrl = document.getElementById('input-vps-url');
const selectMailProvider = document.getElementById('select-mail-provider');
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">&times;</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
// ============================================================
@@ -193,7 +225,7 @@ document.querySelectorAll('.step-btn').forEach(btn => {
if (step === 3) {
const email = inputEmail.value.trim();
if (!email) {
appendLog({ message: 'Please paste email address first', level: 'error', timestamp: Date.now() });
showToast('Please paste email address first', 'warn');
return;
}
await chrome.runtime.sendMessage({ type: 'EXECUTE_STEP', source: 'sidepanel', payload: { step, email } });
@@ -215,7 +247,7 @@ btnAutoRun.addEventListener('click', async () => {
btnAutoContinue.addEventListener('click', async () => {
const email = inputEmail.value.trim();
if (!email) {
appendLog({ message: 'Please paste DuckDuckGo email first!', level: 'error', timestamp: Date.now() });
showToast('Please paste DuckDuckGo email first!', 'warn');
return;
}
autoContinueBar.style.display = 'none';
@@ -279,6 +311,9 @@ 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': {