添加引导弹窗
This commit is contained in:
+60
-1
@@ -255,6 +255,7 @@ const DEFAULT_CPA_CALLBACK_MODE = 'step8';
|
|||||||
const MAIL_2925_MODE_PROVIDE = 'provide';
|
const MAIL_2925_MODE_PROVIDE = 'provide';
|
||||||
const MAIL_2925_MODE_RECEIVE = 'receive';
|
const MAIL_2925_MODE_RECEIVE = 'receive';
|
||||||
const DEFAULT_MAIL_2925_MODE = MAIL_2925_MODE_PROVIDE;
|
const DEFAULT_MAIL_2925_MODE = MAIL_2925_MODE_PROVIDE;
|
||||||
|
const NEW_USER_GUIDE_PROMPT_DISMISSED_STORAGE_KEY = 'multipage-new-user-guide-prompt-dismissed';
|
||||||
const AUTO_SKIP_FAILURES_PROMPT_DISMISSED_STORAGE_KEY = 'multipage-auto-skip-failures-prompt-dismissed';
|
const AUTO_SKIP_FAILURES_PROMPT_DISMISSED_STORAGE_KEY = 'multipage-auto-skip-failures-prompt-dismissed';
|
||||||
const AUTO_RUN_FALLBACK_RISK_PROMPT_DISMISSED_STORAGE_KEY = 'multipage-auto-run-fallback-risk-prompt-dismissed';
|
const AUTO_RUN_FALLBACK_RISK_PROMPT_DISMISSED_STORAGE_KEY = 'multipage-auto-run-fallback-risk-prompt-dismissed';
|
||||||
const CONTRIBUTION_CONTENT_PROMPT_DISMISSED_VERSION_STORAGE_KEY = 'multipage-contribution-content-prompt-dismissed-version';
|
const CONTRIBUTION_CONTENT_PROMPT_DISMISSED_VERSION_STORAGE_KEY = 'multipage-contribution-content-prompt-dismissed-version';
|
||||||
@@ -876,6 +877,59 @@ function setPromptDismissed(storageKey, dismissed) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isNewUserGuidePromptDismissed() {
|
||||||
|
return isPromptDismissed(NEW_USER_GUIDE_PROMPT_DISMISSED_STORAGE_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNewUserGuidePromptDismissed(dismissed) {
|
||||||
|
setPromptDismissed(NEW_USER_GUIDE_PROMPT_DISMISSED_STORAGE_KEY, dismissed);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldPromptNewUserGuide() {
|
||||||
|
if (isNewUserGuidePromptDismissed()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!btnContributionMode || btnContributionMode.disabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (latestState?.contributionMode) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getContributionPortalUrl() {
|
||||||
|
return String(contributionContentService?.portalUrl || 'https://apikey.qzz.io').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openNewUserGuidePrompt() {
|
||||||
|
return openActionModal({
|
||||||
|
title: '新手引导',
|
||||||
|
message: '如果你是第一次使用,可以先查看贡献页里的公告和使用教程。点击“查看引导”会自动打开贡献页面。',
|
||||||
|
alert: {
|
||||||
|
text: '本提示仅出现一次。',
|
||||||
|
},
|
||||||
|
actions: [
|
||||||
|
{ id: null, label: '取消', variant: 'btn-ghost' },
|
||||||
|
{ id: 'confirm', label: '查看引导', variant: 'btn-primary' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function maybeShowNewUserGuidePrompt() {
|
||||||
|
if (!shouldPromptNewUserGuide()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setNewUserGuidePromptDismissed(true);
|
||||||
|
const choice = await openNewUserGuidePrompt();
|
||||||
|
if (choice === 'confirm') {
|
||||||
|
openExternalUrl(getContributionPortalUrl());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function getDismissedContributionContentPromptVersion() {
|
function getDismissedContributionContentPromptVersion() {
|
||||||
return String(localStorage.getItem(CONTRIBUTION_CONTENT_PROMPT_DISMISSED_VERSION_STORAGE_KEY) || '').trim();
|
return String(localStorage.getItem(CONTRIBUTION_CONTENT_PROMPT_DISMISSED_VERSION_STORAGE_KEY) || '').trim();
|
||||||
}
|
}
|
||||||
@@ -4948,7 +5002,12 @@ restoreState().then(() => {
|
|||||||
updatePanelModeUI();
|
updatePanelModeUI();
|
||||||
updateButtonStates();
|
updateButtonStates();
|
||||||
updateStatusDisplay(latestState);
|
updateStatusDisplay(latestState);
|
||||||
return refreshContributionContentHint();
|
return refreshContributionContentHint()
|
||||||
|
.catch((error) => {
|
||||||
|
console.warn('Failed to refresh contribution content hint during initialization:', error);
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.then(() => maybeShowNewUserGuidePrompt());
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
console.error('Failed to initialize sidepanel state:', err);
|
console.error('Failed to initialize sidepanel state:', err);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
|
||||||
|
const source = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
|
||||||
|
|
||||||
|
function extractFunction(name) {
|
||||||
|
const markers = [`async function ${name}(`, `function ${name}(`];
|
||||||
|
const start = markers
|
||||||
|
.map((marker) => source.indexOf(marker))
|
||||||
|
.find((index) => index >= 0);
|
||||||
|
if (start < 0) {
|
||||||
|
throw new Error(`missing function ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let parenDepth = 0;
|
||||||
|
let signatureEnded = false;
|
||||||
|
let braceStart = -1;
|
||||||
|
for (let i = start; i < source.length; i += 1) {
|
||||||
|
const ch = source[i];
|
||||||
|
if (ch === '(') {
|
||||||
|
parenDepth += 1;
|
||||||
|
} else if (ch === ')') {
|
||||||
|
parenDepth -= 1;
|
||||||
|
if (parenDepth === 0) {
|
||||||
|
signatureEnded = true;
|
||||||
|
}
|
||||||
|
} else if (ch === '{' && signatureEnded) {
|
||||||
|
braceStart = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let depth = 0;
|
||||||
|
let end = braceStart;
|
||||||
|
for (; end < source.length; end += 1) {
|
||||||
|
const ch = source[end];
|
||||||
|
if (ch === '{') depth += 1;
|
||||||
|
if (ch === '}') {
|
||||||
|
depth -= 1;
|
||||||
|
if (depth === 0) {
|
||||||
|
end += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return source.slice(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('new user guide prompt is only eligible before the one-time dismissal is set', () => {
|
||||||
|
const bundle = [
|
||||||
|
extractFunction('isPromptDismissed'),
|
||||||
|
extractFunction('setPromptDismissed'),
|
||||||
|
extractFunction('isNewUserGuidePromptDismissed'),
|
||||||
|
extractFunction('setNewUserGuidePromptDismissed'),
|
||||||
|
extractFunction('shouldPromptNewUserGuide'),
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const api = new Function(`
|
||||||
|
const NEW_USER_GUIDE_PROMPT_DISMISSED_STORAGE_KEY = 'multipage-new-user-guide-prompt-dismissed';
|
||||||
|
const storage = new Map();
|
||||||
|
const localStorage = {
|
||||||
|
getItem(key) {
|
||||||
|
return storage.has(key) ? storage.get(key) : null;
|
||||||
|
},
|
||||||
|
setItem(key, value) {
|
||||||
|
storage.set(key, String(value));
|
||||||
|
},
|
||||||
|
removeItem(key) {
|
||||||
|
storage.delete(key);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const btnContributionMode = { disabled: false };
|
||||||
|
let latestState = { contributionMode: false };
|
||||||
|
${bundle}
|
||||||
|
return {
|
||||||
|
shouldPromptNewUserGuide,
|
||||||
|
setDismissed(value) {
|
||||||
|
setNewUserGuidePromptDismissed(value);
|
||||||
|
},
|
||||||
|
setButtonDisabled(value) {
|
||||||
|
btnContributionMode.disabled = Boolean(value);
|
||||||
|
},
|
||||||
|
setContributionMode(value) {
|
||||||
|
latestState = { contributionMode: Boolean(value) };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
`)();
|
||||||
|
|
||||||
|
assert.equal(api.shouldPromptNewUserGuide(), true);
|
||||||
|
|
||||||
|
api.setDismissed(true);
|
||||||
|
assert.equal(api.shouldPromptNewUserGuide(), false);
|
||||||
|
|
||||||
|
api.setDismissed(false);
|
||||||
|
api.setButtonDisabled(true);
|
||||||
|
assert.equal(api.shouldPromptNewUserGuide(), false);
|
||||||
|
|
||||||
|
api.setButtonDisabled(false);
|
||||||
|
api.setContributionMode(true);
|
||||||
|
assert.equal(api.shouldPromptNewUserGuide(), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('new user guide prompt persists dismissal before awaiting the user choice and opens the contribution page on confirm', async () => {
|
||||||
|
const bundle = [
|
||||||
|
extractFunction('isPromptDismissed'),
|
||||||
|
extractFunction('setPromptDismissed'),
|
||||||
|
extractFunction('isNewUserGuidePromptDismissed'),
|
||||||
|
extractFunction('setNewUserGuidePromptDismissed'),
|
||||||
|
extractFunction('shouldPromptNewUserGuide'),
|
||||||
|
extractFunction('getContributionPortalUrl'),
|
||||||
|
extractFunction('openNewUserGuidePrompt'),
|
||||||
|
extractFunction('maybeShowNewUserGuidePrompt'),
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const api = new Function(`
|
||||||
|
const NEW_USER_GUIDE_PROMPT_DISMISSED_STORAGE_KEY = 'multipage-new-user-guide-prompt-dismissed';
|
||||||
|
const storage = new Map();
|
||||||
|
const localStorage = {
|
||||||
|
getItem(key) {
|
||||||
|
return storage.has(key) ? storage.get(key) : null;
|
||||||
|
},
|
||||||
|
setItem(key, value) {
|
||||||
|
storage.set(key, String(value));
|
||||||
|
},
|
||||||
|
removeItem(key) {
|
||||||
|
storage.delete(key);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const btnContributionMode = { disabled: false };
|
||||||
|
const latestState = { contributionMode: false };
|
||||||
|
const contributionContentService = { portalUrl: 'https://apikey.qzz.io' };
|
||||||
|
const openedUrls = [];
|
||||||
|
let modalOptions = null;
|
||||||
|
let nextChoice = 'confirm';
|
||||||
|
function openExternalUrl(url) {
|
||||||
|
openedUrls.push(url);
|
||||||
|
}
|
||||||
|
function openActionModal(options) {
|
||||||
|
modalOptions = options;
|
||||||
|
return Promise.resolve(nextChoice);
|
||||||
|
}
|
||||||
|
${bundle}
|
||||||
|
return {
|
||||||
|
maybeShowNewUserGuidePrompt,
|
||||||
|
getDismissed() {
|
||||||
|
return localStorage.getItem(NEW_USER_GUIDE_PROMPT_DISMISSED_STORAGE_KEY);
|
||||||
|
},
|
||||||
|
getOpenedUrls() {
|
||||||
|
return openedUrls.slice();
|
||||||
|
},
|
||||||
|
getModalOptions() {
|
||||||
|
return modalOptions;
|
||||||
|
},
|
||||||
|
setNextChoice(choice) {
|
||||||
|
nextChoice = choice;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
`)();
|
||||||
|
|
||||||
|
const confirmed = await api.maybeShowNewUserGuidePrompt();
|
||||||
|
const modalOptions = api.getModalOptions();
|
||||||
|
|
||||||
|
assert.equal(confirmed, true);
|
||||||
|
assert.equal(api.getDismissed(), '1');
|
||||||
|
assert.deepStrictEqual(api.getOpenedUrls(), ['https://apikey.qzz.io']);
|
||||||
|
assert.equal(modalOptions.title, '新手引导');
|
||||||
|
assert.equal(modalOptions.alert.text, '本提示仅出现一次。');
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
modalOptions.actions.map((item) => ({ id: item.id, label: item.label })),
|
||||||
|
[
|
||||||
|
{ id: null, label: '取消' },
|
||||||
|
{ id: 'confirm', label: '查看引导' },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
api.setNextChoice(null);
|
||||||
|
const skipped = await api.maybeShowNewUserGuidePrompt();
|
||||||
|
assert.equal(skipped, false);
|
||||||
|
assert.deepStrictEqual(api.getOpenedUrls(), ['https://apikey.qzz.io']);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user