diff --git a/README.md b/README.md index 584b08d..b120186 100644 --- a/README.md +++ b/README.md @@ -215,6 +215,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。 - 手动输入:使用你自定义的密码 - 可通过输入框右侧的眼睛图标切换显示 - 配置会自动保存,也可以点击右侧 `保存` 按钮手动保存一次 +- 右上角 `配置` 按钮支持导出当前配置到 JSON 文件,也支持从 JSON 文件覆盖导入配置 扩展会把本轮实际使用的密码同步回侧边栏,便于查看和复制。 diff --git a/background.js b/background.js index ab1bf59..1902e12 100644 --- a/background.js +++ b/background.js @@ -70,6 +70,8 @@ const PERSISTED_SETTING_DEFAULTS = { }; const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS); +const SETTINGS_EXPORT_SCHEMA_VERSION = 1; +const SETTINGS_EXPORT_FILENAME_PREFIX = 'multipage-settings'; const DEFAULT_STATE = { currentStep: 0, // 当前流程执行到的步骤编号。 @@ -141,6 +143,24 @@ function normalizeEmailGenerator(value = '') { return String(value || '').trim().toLowerCase() === 'cloudflare' ? 'cloudflare' : 'duck'; } +function normalizePanelMode(value = '') { + return String(value || '').trim().toLowerCase() === 'sub2api' ? 'sub2api' : 'cpa'; +} + +function normalizeMailProvider(value = '') { + const normalized = String(value || '').trim().toLowerCase(); + switch (normalized) { + case HOTMAIL_PROVIDER: + case '163': + case '163-vip': + case 'qq': + case 'inbucket': + return normalized; + default: + return PERSISTED_SETTING_DEFAULTS.mailProvider; + } +} + function normalizeLocalCpaStep9Mode(value = '') { return String(value || '').trim().toLowerCase() === 'bypass' ? 'bypass' @@ -157,18 +177,99 @@ function normalizeCloudflareDomain(rawValue = '') { return value; } +function normalizeCloudflareDomains(values) { + const normalizedDomains = []; + const seen = new Set(); + + for (const value of Array.isArray(values) ? values : []) { + const normalized = normalizeCloudflareDomain(value); + if (!normalized || seen.has(normalized)) continue; + seen.add(normalized); + normalizedDomains.push(normalized); + } + + return normalizedDomains; +} + +function normalizePersistentSettingValue(key, value) { + switch (key) { + case 'panelMode': + return normalizePanelMode(value); + case 'vpsUrl': + return String(value || '').trim(); + case 'vpsPassword': + return String(value || ''); + case 'localCpaStep9Mode': + return normalizeLocalCpaStep9Mode(value); + case 'sub2apiUrl': + return String(value || '').trim(); + case 'sub2apiEmail': + return String(value || '').trim(); + case 'sub2apiPassword': + return String(value || ''); + case 'sub2apiGroupName': + return String(value || '').trim(); + case 'customPassword': + return String(value || ''); + case 'autoRunSkipFailures': + case 'autoRunDelayEnabled': + return Boolean(value); + case 'autoRunDelayMinutes': + return normalizeAutoRunDelayMinutes(value); + case 'mailProvider': + return normalizeMailProvider(value); + case 'emailGenerator': + return normalizeEmailGenerator(value); + case 'inbucketHost': + return String(value || '').trim(); + case 'inbucketMailbox': + return String(value || '').trim(); + case 'cloudflareDomain': + return normalizeCloudflareDomain(value); + case 'cloudflareDomains': + return normalizeCloudflareDomains(value); + case 'hotmailAccounts': + return normalizeHotmailAccounts(value); + default: + return value; + } +} + +function buildPersistentSettingsPayload(input = {}, options = {}) { + const { fillDefaults = false, requireKnownKeys = false } = options; + if (!input || typeof input !== 'object' || Array.isArray(input)) { + throw new Error('\u914d\u7f6e\u5185\u5bb9\u683c\u5f0f\u65e0\u6548\u3002'); + } + + const payload = {}; + let matchedKeyCount = 0; + for (const key of PERSISTED_SETTING_KEYS) { + if (input[key] !== undefined) { + payload[key] = normalizePersistentSettingValue(key, input[key]); + matchedKeyCount += 1; + } else if (fillDefaults) { + payload[key] = normalizePersistentSettingValue(key, PERSISTED_SETTING_DEFAULTS[key]); + } + } + + if (requireKnownKeys && matchedKeyCount === 0) { + throw new Error('\u914d\u7f6e\u6587\u4ef6\u4e2d\u6ca1\u6709\u53ef\u8bc6\u522b\u7684\u914d\u7f6e\u5185\u5bb9\u3002'); + } + + if (payload.cloudflareDomains) { + const domains = normalizeCloudflareDomains(payload.cloudflareDomains); + if (payload.cloudflareDomain && !domains.includes(payload.cloudflareDomain)) { + domains.unshift(payload.cloudflareDomain); + } + payload.cloudflareDomains = domains; + } + + return payload; +} + async function getPersistedSettings() { const stored = await chrome.storage.local.get(PERSISTED_SETTING_KEYS); - return { - ...PERSISTED_SETTING_DEFAULTS, - ...stored, - autoRunSkipFailures: Boolean(stored.autoRunSkipFailures ?? PERSISTED_SETTING_DEFAULTS.autoRunSkipFailures), - autoRunDelayEnabled: Boolean(stored.autoRunDelayEnabled ?? PERSISTED_SETTING_DEFAULTS.autoRunDelayEnabled), - autoRunDelayMinutes: normalizeAutoRunDelayMinutes(stored.autoRunDelayMinutes ?? PERSISTED_SETTING_DEFAULTS.autoRunDelayMinutes), - emailGenerator: normalizeEmailGenerator(stored.emailGenerator ?? PERSISTED_SETTING_DEFAULTS.emailGenerator), - localCpaStep9Mode: normalizeLocalCpaStep9Mode(stored.localCpaStep9Mode ?? PERSISTED_SETTING_DEFAULTS.localCpaStep9Mode), - hotmailAccounts: normalizeHotmailAccounts(stored.hotmailAccounts), - }; + return buildPersistentSettingsPayload(stored, { fillDefaults: true }); } async function getState() { @@ -200,28 +301,75 @@ async function setState(updates) { } async function setPersistentSettings(updates) { - const persistedUpdates = {}; - for (const key of PERSISTED_SETTING_KEYS) { - if (updates[key] !== undefined) { - if (key === 'autoRunSkipFailures' || key === 'autoRunDelayEnabled') { - persistedUpdates[key] = Boolean(updates[key]); - } else if (key === 'autoRunDelayMinutes') { - persistedUpdates[key] = normalizeAutoRunDelayMinutes(updates[key]); - } else if (key === 'localCpaStep9Mode') { - persistedUpdates[key] = normalizeLocalCpaStep9Mode(updates[key]); - } else if (key === 'hotmailAccounts') { - persistedUpdates[key] = normalizeHotmailAccounts(updates[key]); - } else { - persistedUpdates[key] = updates[key]; - } - } - } + const persistedUpdates = buildPersistentSettingsPayload(updates); if (Object.keys(persistedUpdates).length > 0) { await chrome.storage.local.set(persistedUpdates); } } +function buildSettingsExportFilename(date = new Date()) { + const pad = (value) => String(value).padStart(2, '0'); + return `${SETTINGS_EXPORT_FILENAME_PREFIX}-${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}.json`; +} + +async function exportSettingsBundle() { + const settings = await getPersistedSettings(); + const bundle = { + schemaVersion: SETTINGS_EXPORT_SCHEMA_VERSION, + exportedAt: new Date().toISOString(), + extensionVersion: chrome.runtime.getManifest().version, + settings, + }; + + return { + fileName: buildSettingsExportFilename(), + fileContent: JSON.stringify(bundle, null, 2), + }; +} + +async function importSettingsBundle(configBundle) { + const state = await ensureManualInteractionAllowed('\u5bfc\u5165\u914d\u7f6e'); + if (Object.values(state.stepStatuses || {}).some((status) => status === 'running')) { + throw new Error('\u5f53\u524d\u6709\u6b65\u9aa4\u6b63\u5728\u6267\u884c\uff0c\u65e0\u6cd5\u5bfc\u5165\u914d\u7f6e\u3002'); + } + if (!configBundle || typeof configBundle !== 'object' || Array.isArray(configBundle)) { + throw new Error('\u914d\u7f6e\u6587\u4ef6\u5185\u5bb9\u65e0\u6548\u3002'); + } + + const schemaVersion = Number(configBundle.schemaVersion); + if (schemaVersion !== SETTINGS_EXPORT_SCHEMA_VERSION) { + throw new Error(`\u4ec5\u652f\u6301\u5bfc\u5165 schemaVersion=${SETTINGS_EXPORT_SCHEMA_VERSION} \u7684\u914d\u7f6e\u6587\u4ef6\u3002`); + } + if (!configBundle.settings || typeof configBundle.settings !== 'object' || Array.isArray(configBundle.settings)) { + throw new Error('\u914d\u7f6e\u6587\u4ef6\u7f3a\u5c11 settings \u914d\u7f6e\u6bb5\u3002'); + } + + const importedSettings = buildPersistentSettingsPayload(configBundle.settings, { + fillDefaults: true, + requireKnownKeys: true, + }); + + await setPersistentSettings(importedSettings); + + const sessionUpdates = { + ...importedSettings, + currentHotmailAccountId: null, + }; + if (importedSettings.mailProvider === HOTMAIL_PROVIDER) { + sessionUpdates.email = null; + } + + await setState(sessionUpdates); + broadcastDataUpdate({ + ...importedSettings, + currentHotmailAccountId: null, + ...(sessionUpdates.email !== undefined ? { email: sessionUpdates.email } : {}), + }); + + return getState(); +} + function broadcastDataUpdate(payload) { chrome.runtime.sendMessage({ type: 'DATA_UPDATED', @@ -2459,32 +2607,21 @@ async function handleMessage(message, sender) { } case 'SAVE_SETTING': { - const updates = {}; - if (message.payload.panelMode !== undefined) updates.panelMode = message.payload.panelMode; - if (message.payload.vpsUrl !== undefined) updates.vpsUrl = message.payload.vpsUrl; - if (message.payload.vpsPassword !== undefined) updates.vpsPassword = message.payload.vpsPassword; - if (message.payload.localCpaStep9Mode !== undefined) updates.localCpaStep9Mode = normalizeLocalCpaStep9Mode(message.payload.localCpaStep9Mode); - if (message.payload.sub2apiUrl !== undefined) updates.sub2apiUrl = message.payload.sub2apiUrl; - if (message.payload.sub2apiEmail !== undefined) updates.sub2apiEmail = message.payload.sub2apiEmail; - if (message.payload.sub2apiPassword !== undefined) updates.sub2apiPassword = message.payload.sub2apiPassword; - if (message.payload.sub2apiGroupName !== undefined) updates.sub2apiGroupName = message.payload.sub2apiGroupName; - if (message.payload.customPassword !== undefined) updates.customPassword = message.payload.customPassword; - if (message.payload.autoRunSkipFailures !== undefined) updates.autoRunSkipFailures = Boolean(message.payload.autoRunSkipFailures); - if (message.payload.autoRunDelayEnabled !== undefined) updates.autoRunDelayEnabled = Boolean(message.payload.autoRunDelayEnabled); - if (message.payload.autoRunDelayMinutes !== undefined) updates.autoRunDelayMinutes = normalizeAutoRunDelayMinutes(message.payload.autoRunDelayMinutes); - if (message.payload.mailProvider !== undefined) updates.mailProvider = message.payload.mailProvider; - if (message.payload.emailGenerator !== undefined) updates.emailGenerator = normalizeEmailGenerator(message.payload.emailGenerator); - if (message.payload.inbucketHost !== undefined) updates.inbucketHost = message.payload.inbucketHost; - if (message.payload.inbucketMailbox !== undefined) updates.inbucketMailbox = message.payload.inbucketMailbox; - if (message.payload.cloudflareDomain !== undefined) updates.cloudflareDomain = normalizeCloudflareDomain(message.payload.cloudflareDomain); - if (message.payload.cloudflareDomains !== undefined) updates.cloudflareDomains = Array.isArray(message.payload.cloudflareDomains) - ? message.payload.cloudflareDomains.map(domain => normalizeCloudflareDomain(domain)).filter(Boolean) - : []; + const updates = buildPersistentSettingsPayload(message.payload || {}); await setPersistentSettings(updates); await setState(updates); return { ok: true }; } + case 'EXPORT_SETTINGS': { + return { ok: true, ...(await exportSettingsBundle()) }; + } + + case 'IMPORT_SETTINGS': { + const state = await importSettingsBundle(message.payload?.config || null); + return { ok: true, state }; + } + case 'UPSERT_HOTMAIL_ACCOUNT': { const account = await upsertHotmailAccount(message.payload || {}); return { ok: true, account }; diff --git a/sidepanel/sidepanel.css b/sidepanel/sidepanel.css index 7b1a9a7..b627d9d 100644 --- a/sidepanel/sidepanel.css +++ b/sidepanel/sidepanel.css @@ -142,6 +142,67 @@ header { [data-theme="dark"] .theme-toggle .icon-moon { display: none; } [data-theme="dark"] .theme-toggle .icon-sun { display: block; } +.header-menu { + position: relative; + display: flex; + align-items: center; +} + +.header-config-btn { + min-width: 0; + padding-inline: 10px; +} + +.header-config-btn[aria-expanded="true"] { + background: var(--bg-hover); + color: var(--text-primary); +} + +.header-dropdown { + position: absolute; + top: calc(100% + 6px); + right: 0; + min-width: 120px; + padding: 6px; + display: flex; + flex-direction: column; + gap: 4px; + background: var(--bg-base); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + box-shadow: var(--shadow-md); + z-index: 1300; +} + +.header-dropdown[hidden] { + display: none !important; +} + +.header-dropdown-item { + width: 100%; + padding: 8px 10px; + border: none; + border-radius: var(--radius-sm); + background: transparent; + color: var(--text-secondary); + font: inherit; + font-size: 12px; + font-weight: 600; + text-align: left; + cursor: pointer; + transition: background var(--transition), color var(--transition); +} + +.header-dropdown-item:hover:not(:disabled) { + background: var(--bg-hover); + color: var(--text-primary); +} + +.header-dropdown-item:disabled { + color: var(--text-muted); + cursor: not-allowed; +} + /* ============================================================ Buttons ============================================================ */ diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 04d6332..af607f4 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -58,6 +58,14 @@ +
+ + +
@@ -343,6 +351,7 @@
+ diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index 8f9129b..918c507 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -35,6 +35,12 @@ const autoScheduleMeta = document.getElementById('auto-schedule-meta'); const btnAutoRunNow = document.getElementById('btn-auto-run-now'); const btnAutoCancelSchedule = document.getElementById('btn-auto-cancel-schedule'); const btnClearLog = document.getElementById('btn-clear-log'); +const configMenuShell = document.getElementById('config-menu-shell'); +const btnConfigMenu = document.getElementById('btn-config-menu'); +const configMenu = document.getElementById('config-menu'); +const btnExportSettings = document.getElementById('btn-export-settings'); +const btnImportSettings = document.getElementById('btn-import-settings'); +const inputImportSettingsFile = document.getElementById('input-import-settings-file'); const selectPanelMode = document.getElementById('select-panel-mode'); const rowVpsUrl = document.getElementById('row-vps-url'); const inputVpsUrl = document.getElementById('input-vps-url'); @@ -120,6 +126,8 @@ let currentModalActions = []; let scheduledCountdownTimer = null; let hotmailActionInFlight = false; let hotmailListExpanded = false; +let configMenuOpen = false; +let configActionInFlight = false; const EYE_OPEN_ICON = ''; const EYE_CLOSED_ICON = ''; @@ -258,6 +266,71 @@ async function openConfirmModal({ title, message, confirmLabel = '确认', confi return choice === 'confirm'; } +function updateConfigMenuControls() { + const disabled = configActionInFlight || settingsSaveInFlight; + const importLocked = disabled + || currentAutoRun.autoRunning + || Object.values(getStepStatuses()).some((status) => status === 'running'); + if (btnConfigMenu) { + btnConfigMenu.disabled = disabled; + btnConfigMenu.setAttribute('aria-expanded', String(configMenuOpen)); + } + if (configMenu) { + configMenu.hidden = !configMenuOpen; + } + if (btnExportSettings) { + btnExportSettings.disabled = disabled; + } + if (btnImportSettings) { + btnImportSettings.disabled = importLocked; + } +} + +function closeConfigMenu() { + configMenuOpen = false; + updateConfigMenuControls(); +} + +function openConfigMenu() { + configMenuOpen = true; + updateConfigMenuControls(); +} + +function toggleConfigMenu() { + configMenuOpen ? closeConfigMenu() : openConfigMenu(); +} + +async function waitForSettingsSaveIdle() { + while (settingsSaveInFlight) { + await new Promise((resolve) => setTimeout(resolve, 50)); + } +} + +async function flushPendingSettingsBeforeExport() { + clearTimeout(settingsAutoSaveTimer); + await waitForSettingsSaveIdle(); + if (settingsDirty) { + await saveSettings({ silent: true }); + } +} + +async function settlePendingSettingsBeforeImport() { + clearTimeout(settingsAutoSaveTimer); + await waitForSettingsSaveIdle(); +} + +function downloadTextFile(content, fileName, mimeType = 'application/json;charset=utf-8') { + const blob = new Blob([content], { type: mimeType }); + const objectUrl = URL.createObjectURL(blob); + const anchor = document.createElement('a'); + anchor.href = objectUrl; + anchor.download = fileName; + document.body.appendChild(anchor); + anchor.click(); + anchor.remove(); + setTimeout(() => URL.revokeObjectURL(objectUrl), 0); +} + function isDoneStatus(status) { return status === 'completed' || status === 'manual_completed' || status === 'skipped'; } @@ -600,6 +673,7 @@ function markSettingsDirty(isDirty = true) { function updateSaveButtonState() { btnSaveSettings.disabled = settingsSaveInFlight || !settingsDirty; + updateConfigMenuControls(); btnSaveSettings.textContent = settingsSaveInFlight ? '保存中' : '保存'; } @@ -707,6 +781,7 @@ function applyAutoRunStatus(payload = currentAutoRun) { updateAutoDelayInputState(); syncScheduledCountdownTicker(); updateStopButtonState(scheduled || paused || locked || Object.values(getStepStatuses()).some(status => status === 'running')); + updateConfigMenuControls(); } function initializeManualStepActions() { @@ -744,11 +819,45 @@ function initializeManualStepActions() { // State Restore on load // ============================================================ +function applySettingsState(state) { + syncLatestState(state); + syncAutoRunState(state); + + inputEmail.value = state?.email || ''; + syncPasswordField(state || {}); + inputVpsUrl.value = state?.vpsUrl || ''; + inputVpsPassword.value = state?.vpsPassword || ''; + setLocalCpaStep9Mode(state?.localCpaStep9Mode); + selectPanelMode.value = state?.panelMode || 'cpa'; + inputSub2ApiUrl.value = state?.sub2apiUrl || ''; + inputSub2ApiEmail.value = state?.sub2apiEmail || ''; + inputSub2ApiPassword.value = state?.sub2apiPassword || ''; + inputSub2ApiGroup.value = state?.sub2apiGroupName || ''; + selectMailProvider.value = state?.mailProvider || '163'; + selectEmailGenerator.value = state?.emailGenerator || 'duck'; + inputInbucketHost.value = state?.inbucketHost || ''; + inputInbucketMailbox.value = state?.inbucketMailbox || ''; + renderCloudflareDomainOptions(state?.cloudflareDomain || ''); + setCloudflareDomainEditMode(false, { clearInput: true }); + inputAutoSkipFailures.checked = Boolean(state?.autoRunSkipFailures); + inputAutoDelayEnabled.checked = Boolean(state?.autoRunDelayEnabled); + inputAutoDelayMinutes.value = String(normalizeAutoDelayMinutes(state?.autoRunDelayMinutes)); + if (state?.autoRunTotalRuns) { + inputRunCount.value = String(state.autoRunTotalRuns); + } + + applyAutoRunStatus(state); + markSettingsDirty(false); + updateAutoDelayInputState(); + updatePanelModeUI(); + updateMailProviderUI(); + updateButtonStates(); +} + async function restoreState() { try { const state = await chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sidepanel' }); - syncLatestState(state); - syncAutoRunState(state); + applySettingsState(state); if (state.oauthUrl) { displayOauthUrl.textContent = state.oauthUrl; @@ -758,53 +867,6 @@ async function restoreState() { 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.vpsPassword) { - inputVpsPassword.value = state.vpsPassword; - } - setLocalCpaStep9Mode(state.localCpaStep9Mode); - if (state.panelMode) { - selectPanelMode.value = state.panelMode; - } - if (state.sub2apiUrl) { - inputSub2ApiUrl.value = state.sub2apiUrl; - } - if (state.sub2apiEmail) { - inputSub2ApiEmail.value = state.sub2apiEmail; - } - if (state.sub2apiPassword) { - inputSub2ApiPassword.value = state.sub2apiPassword; - } - if (state.sub2apiGroupName) { - inputSub2ApiGroup.value = state.sub2apiGroupName; - } - if (state.mailProvider) { - selectMailProvider.value = state.mailProvider; - } - if (state.emailGenerator) { - selectEmailGenerator.value = state.emailGenerator; - } - if (state.inbucketHost) { - inputInbucketHost.value = state.inbucketHost; - } - if (state.inbucketMailbox) { - inputInbucketMailbox.value = state.inbucketMailbox; - } - renderCloudflareDomainOptions(state.cloudflareDomain || ''); - setCloudflareDomainEditMode(false, { clearInput: true }); - inputAutoSkipFailures.checked = Boolean(state.autoRunSkipFailures); - inputAutoDelayEnabled.checked = Boolean(state.autoRunDelayEnabled); - inputAutoDelayMinutes.value = String(normalizeAutoDelayMinutes(state.autoRunDelayMinutes)); - if (state.autoRunTotalRuns) { - inputRunCount.value = String(state.autoRunTotalRuns); - } - if (state.stepStatuses) { for (const [step, status] of Object.entries(state.stepStatuses)) { updateStepUI(Number(step), status); @@ -817,14 +879,8 @@ async function restoreState() { } } - applyAutoRunStatus(state); - markSettingsDirty(false); - updateAutoDelayInputState(); updateStatusDisplay(latestState); updateProgressCounter(); - updatePanelModeUI(); - updateMailProviderUI(); - updateButtonStates(); } catch (err) { console.error('Failed to restore state:', err); } @@ -1177,6 +1233,7 @@ function updateStepUI(step, status) { updateButtonStates(); updateProgressCounter(); + updateConfigMenuControls(); } function updateProgressCounter() { @@ -1397,6 +1454,93 @@ async function copyTextToClipboard(text) { await navigator.clipboard.writeText(value); } +async function exportSettingsFile() { + closeConfigMenu(); + configActionInFlight = true; + updateConfigMenuControls(); + + try { + await flushPendingSettingsBeforeExport(); + const response = await chrome.runtime.sendMessage({ + type: 'EXPORT_SETTINGS', + source: 'sidepanel', + payload: {}, + }); + + if (response?.error) { + throw new Error(response.error); + } + if (!response?.fileContent || !response?.fileName) { + throw new Error('\u672a\u751f\u6210\u53ef\u4e0b\u8f7d\u7684\u914d\u7f6e\u6587\u4ef6\u3002'); + } + + downloadTextFile(response.fileContent, response.fileName); + showToast('\u914d\u7f6e\u5df2\u5bfc\u51fa\uff1a' + response.fileName, 'success', 2200); + } catch (err) { + showToast('\u5bfc\u51fa\u914d\u7f6e\u5931\u8d25\uff1a' + err.message, 'error'); + } finally { + configActionInFlight = false; + updateConfigMenuControls(); + } +} + +async function importSettingsFromFile(file) { + if (!file) return; + + configActionInFlight = true; + closeConfigMenu(); + updateConfigMenuControls(); + + try { + await settlePendingSettingsBeforeImport(); + const rawText = await file.text(); + + let parsedConfig = null; + try { + parsedConfig = JSON.parse(rawText); + } catch { + throw new Error('\u914d\u7f6e\u6587\u4ef6\u4e0d\u662f\u6709\u6548\u7684 JSON\u3002'); + } + + const confirmed = await openConfirmModal({ + title: '\u5bfc\u5165\u914d\u7f6e', + message: '\u786e\u8ba4\u5bfc\u5165\u914d\u7f6e\u6587\u4ef6 "' + file.name + '" \u5417\uff1f\u5bfc\u5165\u540e\u4f1a\u8986\u76d6\u5f53\u524d\u914d\u7f6e\u3002', + confirmLabel: '\u786e\u8ba4\u8986\u76d6\u5bfc\u5165', + confirmVariant: 'btn-danger', + }); + if (!confirmed) { + return; + } + + const response = await chrome.runtime.sendMessage({ + type: 'IMPORT_SETTINGS', + source: 'sidepanel', + payload: { + config: parsedConfig, + }, + }); + + if (response?.error) { + throw new Error(response.error); + } + if (!response?.state) { + throw new Error('\u5bfc\u5165\u540e\u672a\u8fd4\u56de\u6700\u65b0\u914d\u7f6e\u72b6\u6001\u3002'); + } + + applySettingsState(response.state); + updateStatusDisplay(latestState); + showToast('\u914d\u7f6e\u5df2\u5bfc\u5165\uff0c\u5f53\u524d\u914d\u7f6e\u5df2\u8986\u76d6\u3002', 'success', 2200); + } catch (err) { + showToast('\u5bfc\u5165\u914d\u7f6e\u5931\u8d25\uff1a' + err.message, 'error'); + } finally { + configActionInFlight = false; + updateConfigMenuControls(); + if (inputImportSettingsFile) { + inputImportSettingsFile.value = ''; + } + } +} + async function deleteHotmailAccountsByMode(mode) { const isUsedMode = mode === 'used'; const targetAccounts = getHotmailAccountsByUsage(isUsedMode ? 'used' : 'all'); @@ -1838,6 +1982,38 @@ btnStop.addEventListener('click', async () => { showToast(isAutoRunScheduledPhase() ? '正在取消倒计时计划...' : '正在停止当前流程...', 'warn', 2000); }); +btnConfigMenu?.addEventListener('click', (event) => { + event.stopPropagation(); + toggleConfigMenu(); +}); + +configMenu?.addEventListener('click', (event) => { + event.stopPropagation(); +}); + +btnExportSettings?.addEventListener('click', async () => { + if (configActionInFlight || settingsSaveInFlight) { + return; + } + await exportSettingsFile(); +}); + +btnImportSettings?.addEventListener('click', async () => { + if (configActionInFlight || settingsSaveInFlight) { + return; + } + closeConfigMenu(); + if (inputImportSettingsFile) { + inputImportSettingsFile.value = ''; + inputImportSettingsFile.click(); + } +}); + +inputImportSettingsFile?.addEventListener('change', async () => { + const file = inputImportSettingsFile.files?.[0] || null; + await importSettingsFromFile(file); +}); + autoStartModal?.addEventListener('click', (event) => { if (event.target === autoStartModal) { resolveModalChoice(null); @@ -2280,6 +2456,22 @@ btnTheme.addEventListener('click', () => { setTheme(current === 'dark' ? 'light' : 'dark'); }); +document.addEventListener('click', (event) => { + if (!configMenuOpen) { + return; + } + if (configMenuShell?.contains(event.target)) { + return; + } + closeConfigMenu(); +}); + +document.addEventListener('keydown', (event) => { + if (event.key === 'Escape' && configMenuOpen) { + closeConfigMenu(); + } +}); + // ============================================================ // Init // ============================================================ @@ -2288,6 +2480,7 @@ initializeManualStepActions(); initTheme(); initHotmailListExpandedState(); updateSaveButtonState(); +updateConfigMenuControls(); setLocalCpaStep9Mode(DEFAULT_LOCAL_CPA_STEP9_MODE); restoreState().then(() => { syncPasswordToggleLabel();