feat(sidepanel): 添加配置菜单,支持导入和导出设置功能
This commit is contained in:
@@ -215,6 +215,7 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。
|
||||
- 手动输入:使用你自定义的密码
|
||||
- 可通过输入框右侧的眼睛图标切换显示
|
||||
- 配置会自动保存,也可以点击右侧 `保存` 按钮手动保存一次
|
||||
- 右上角 `配置` 按钮支持导出当前配置到 JSON 文件,也支持从 JSON 文件覆盖导入配置
|
||||
|
||||
扩展会把本轮实际使用的密码同步回侧边栏,便于查看和复制。
|
||||
|
||||
|
||||
+184
-47
@@ -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 };
|
||||
|
||||
@@ -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
|
||||
============================================================ */
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user