c6f1070ace
- VPS URL: now an input field in Side Panel, no longer hardcoded - Dynamic script injection via chrome.scripting.executeScript - host_permissions changed to <all_urls> for flexibility - 163 Mail: new content script (mail-163.js) with actual selectors - Mail items: div[sign="letter"], sender: .nui-user, subject: span.da0 - Supports aria-label fallback for matching - Mail provider selector: dropdown to switch between QQ Mail and 163 Mail - Background routes steps 4/7 to correct mail content script - Settings persist in chrome.storage.session
368 lines
12 KiB
JavaScript
368 lines
12 KiB
JavaScript
// sidepanel/sidepanel.js — Side Panel logic
|
|
|
|
const STATUS_ICONS = {
|
|
pending: '',
|
|
running: '',
|
|
completed: '\u2713', // ✓
|
|
failed: '\u2717', // ✗
|
|
};
|
|
|
|
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 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');
|
|
|
|
// ============================================================
|
|
// 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;
|
|
}
|
|
if (state.vpsUrl) {
|
|
inputVpsUrl.value = state.vpsUrl;
|
|
}
|
|
if (state.mailProvider) {
|
|
selectMailProvider.value = state.mailProvider;
|
|
}
|
|
|
|
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();
|
|
} catch (err) {
|
|
console.error('Failed to restore state:', err);
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// UI Updates
|
|
// ============================================================
|
|
|
|
function updateStepUI(step, status) {
|
|
const statusEl = document.querySelector(`.step-status[data-step="${step}"]`);
|
|
const row = document.querySelector(`.step-row[data-step="${step}"]`);
|
|
const indicator = document.querySelector(`.step-indicator[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 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');
|
|
}
|
|
}
|
|
}
|
|
|
|
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 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 = `<span class="log-time">${time}</span> `;
|
|
html += `<span class="log-level log-level-${entry.level}">${levelLabel}</span> `;
|
|
if (stepNum) {
|
|
html += `<span class="log-step-tag step-${stepNum}">S${stepNum}</span>`;
|
|
}
|
|
html += `<span class="log-msg">${escapeHtml(entry.message)}</span>`;
|
|
|
|
line.innerHTML = html;
|
|
logArea.appendChild(line);
|
|
logArea.scrollTop = logArea.scrollHeight;
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// ============================================================
|
|
// 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) {
|
|
appendLog({ message: 'Please paste email address first', level: 'error', timestamp: Date.now() });
|
|
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 } });
|
|
}
|
|
});
|
|
});
|
|
|
|
// Auto Run
|
|
btnAutoRun.addEventListener('click', async () => {
|
|
btnAutoRun.disabled = true;
|
|
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...';
|
|
await chrome.runtime.sendMessage({ type: 'AUTO_RUN', source: 'sidepanel' });
|
|
});
|
|
|
|
btnAutoContinue.addEventListener('click', async () => {
|
|
const email = inputEmail.value.trim();
|
|
if (!email) {
|
|
appendLog({ message: 'Please paste DuckDuckGo email first!', level: 'error', timestamp: Date.now() });
|
|
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;
|
|
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';
|
|
autoContinueBar.style.display = 'none';
|
|
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 } });
|
|
}
|
|
});
|
|
|
|
selectMailProvider.addEventListener('change', async () => {
|
|
await chrome.runtime.sendMessage({
|
|
type: 'SAVE_SETTING', source: 'sidepanel',
|
|
payload: { mailProvider: selectMailProvider.value },
|
|
});
|
|
});
|
|
|
|
// ============================================================
|
|
// Listen for Background broadcasts
|
|
// ============================================================
|
|
|
|
chrome.runtime.onMessage.addListener((message) => {
|
|
switch (message.type) {
|
|
case 'LOG_ENTRY':
|
|
appendLog(message.payload);
|
|
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 => {
|
|
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 'DATA_UPDATED': {
|
|
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 } = message.payload;
|
|
switch (phase) {
|
|
case 'waiting_email':
|
|
autoContinueBar.style.display = 'flex';
|
|
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"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg> Paused';
|
|
break;
|
|
case 'complete':
|
|
btnAutoRun.disabled = false;
|
|
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';
|
|
autoContinueBar.style.display = 'none';
|
|
break;
|
|
case 'stopped':
|
|
btnAutoRun.disabled = false;
|
|
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';
|
|
autoContinueBar.style.display = 'none';
|
|
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(() => {
|
|
updateButtonStates();
|
|
});
|