feat(sidepanel): 添加配置菜单,支持导入和导出设置功能

This commit is contained in:
QLHazyCoder
2026-04-13 01:18:14 +08:00
parent 3e3d92de7a
commit b382cbf02f
5 changed files with 503 additions and 102 deletions
+1
View File
@@ -215,6 +215,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
- 手动输入:使用你自定义的密码
- 可通过输入框右侧的眼睛图标切换显示
- 配置会自动保存,也可以点击右侧 `保存` 按钮手动保存一次
- 右上角 `配置` 按钮支持导出当前配置到 JSON 文件,也支持从 JSON 文件覆盖导入配置
扩展会把本轮实际使用的密码同步回侧边栏,便于查看和复制。
+184 -47
View File
@@ -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 };
+61
View File
@@ -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
============================================================ */
+9
View File
@@ -58,6 +58,14 @@
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
</svg>
</button>
<div id="config-menu-shell" class="header-menu">
<button id="btn-config-menu" class="btn btn-ghost btn-sm header-config-btn" type="button" aria-haspopup="menu"
aria-expanded="false">配置</button>
<div id="config-menu" class="header-dropdown" role="menu" aria-labelledby="btn-config-menu" hidden>
<button id="btn-export-settings" class="header-dropdown-item" type="button" role="menuitem">导出配置</button>
<button id="btn-import-settings" class="header-dropdown-item" type="button" role="menuitem">导入配置</button>
</div>
</div>
</div>
</header>
@@ -343,6 +351,7 @@
</div>
<div id="toast-container"></div>
<input id="input-import-settings-file" type="file" accept=".json,application/json" hidden />
<script src="../hotmail-utils.js"></script>
<script src="sidepanel.js"></script>
</body>
+248 -55
View File
@@ -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 = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg>';
const EYE_CLOSED_ICON = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17.94 17.94A10.94 10.94 0 0 1 12 19C5 19 1 12 1 12a21.77 21.77 0 0 1 5.06-6.94"/><path d="M9.9 4.24A10.94 10.94 0 0 1 12 5c7 0 11 7 11 7a21.86 21.86 0 0 1-2.16 3.19"/><path d="M1 1l22 22"/><path d="M14.12 14.12a3 3 0 1 1-4.24-4.24"/></svg>';
@@ -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();