添加自动运行广告条的样式和逻辑,并实现相关功能的测试

This commit is contained in:
QLHazycoder
2026-05-19 08:36:18 +00:00
parent 85a6cc4157
commit 7bd07f5947
5 changed files with 471 additions and 8 deletions
+103
View File
@@ -652,6 +652,109 @@ header {
display: none !important;
}
.auto-run-ad-bar {
position: relative;
display: flex;
align-items: center;
min-height: 34px;
padding: 7px 46px;
border: 1px solid color-mix(in srgb, var(--amber) 24%, var(--border));
border-radius: var(--radius-sm);
background: color-mix(in srgb, var(--amber) 10%, var(--bg-base));
box-shadow: inset 0 1px 0 color-mix(in srgb, var(--bg-base) 75%, transparent);
overflow: hidden;
}
.auto-run-ad-bar[hidden] {
display: none !important;
}
.auto-run-ad-badge {
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
padding: 3px 7px;
border-radius: 999px;
background: color-mix(in srgb, var(--amber) 22%, var(--bg-base));
color: color-mix(in srgb, var(--amber) 80%, var(--text-primary));
font-size: 10px;
font-weight: 700;
letter-spacing: 0.08em;
}
.auto-run-ad-viewport {
width: 100%;
min-width: 0;
overflow: hidden;
}
.auto-run-ad-track {
display: flex;
align-items: center;
justify-content: center;
width: max-content;
min-width: 100%;
transform: translateX(0);
}
.auto-run-ad-text {
display: block;
font-size: 12px;
line-height: 1.4;
color: var(--text-secondary);
white-space: nowrap;
}
.auto-run-ad-text--clone {
display: none;
padding-left: var(--auto-run-ad-gap, 28px);
}
.auto-run-ad-link {
color: color-mix(in srgb, var(--amber) 80%, var(--link));
text-decoration: underline;
text-decoration-thickness: 1px;
text-underline-offset: 2px;
transition: color var(--transition);
}
.auto-run-ad-link:hover {
color: var(--link-hover);
}
.auto-run-ad-link:focus-visible {
outline: 2px solid color-mix(in srgb, var(--amber) 36%, var(--border));
outline-offset: 2px;
border-radius: 3px;
}
.auto-run-ad-bar.is-scrolling .auto-run-ad-track {
justify-content: flex-start;
min-width: max-content;
animation: auto-run-ad-marquee var(--auto-run-ad-duration, 16s) linear infinite;
will-change: transform;
}
.auto-run-ad-bar.is-scrolling:hover .auto-run-ad-track,
.auto-run-ad-bar.is-scrolling:focus-within .auto-run-ad-track {
animation-play-state: paused;
}
.auto-run-ad-bar.is-scrolling .auto-run-ad-text--clone {
display: block;
}
@keyframes auto-run-ad-marquee {
from {
transform: translateX(0);
}
to {
transform: translateX(calc(-1 * var(--auto-run-ad-scroll-distance, 0px)));
}
}
.contribution-mode-panel {
display: flex;
flex-direction: column;
+9
View File
@@ -124,6 +124,15 @@
<option value="sub2api">SUB2API</option>
<option value="codex2api">Codex2API</option>
</select>
</div>
<div id="auto-run-ad-bar" class="auto-run-ad-bar" hidden aria-live="polite">
<span class="auto-run-ad-badge">广告</span>
<div id="auto-run-ad-viewport" class="auto-run-ad-viewport">
<div id="auto-run-ad-track" class="auto-run-ad-track">
<span id="auto-run-ad-text" class="auto-run-ad-text"></span>
<span id="auto-run-ad-text-clone" class="auto-run-ad-text auto-run-ad-text--clone" aria-hidden="true"></span>
</div>
</div>
</div>
<div id="contribution-mode-panel" class="contribution-mode-panel" hidden>
<div class="contribution-mode-panel-header">
+249 -4
View File
@@ -66,6 +66,11 @@ const contributionUpdateLayer = document.getElementById('contribution-update-lay
const contributionUpdateHint = document.getElementById('contribution-update-hint');
const contributionUpdateHintText = document.getElementById('contribution-update-hint-text');
const btnDismissContributionUpdateHint = document.getElementById('btn-dismiss-contribution-update-hint');
const autoRunAdBar = document.getElementById('auto-run-ad-bar');
const autoRunAdViewport = document.getElementById('auto-run-ad-viewport');
const autoRunAdTrack = document.getElementById('auto-run-ad-track');
const autoRunAdText = document.getElementById('auto-run-ad-text');
const autoRunAdTextClone = document.getElementById('auto-run-ad-text-clone');
const stepsProgress = document.getElementById('steps-progress');
const btnAutoRun = document.getElementById('btn-auto-run');
const btnAutoContinue = document.getElementById('btn-auto-continue');
@@ -1346,6 +1351,7 @@ let configActionInFlight = false;
let currentReleaseSnapshot = null;
let currentContributionContentSnapshot = null;
let contributionContentSnapshotRequestInFlight = null;
let autoRunAdScrollSyncFrame = 0;
function normalizeAutomationWindowId(value) {
if (value === null || value === undefined || value === '') {
@@ -11013,6 +11019,156 @@ function getContributionUpdatePromptLines(snapshot = currentContributionContentS
return lines;
}
function getAutoRunAdConfig(snapshot = currentContributionContentSnapshot) {
const items = Array.isArray(snapshot?.items) ? snapshot.items : [];
const adItem = items.find((item) =>
item
&& String(item.slug || '').trim().toLowerCase() === 'extension_auto_run_ad'
);
if (!adItem || !adItem.isVisible) {
return null;
}
const text = String(adItem.text || '').replace(/\s+/g, ' ').trim();
if (!text) {
return null;
}
return {
text,
title: String(adItem.title || '').trim(),
};
}
function sanitizeAutoRunAdUrl(value = '') {
const raw = String(value || '').trim();
if (!raw) {
return '';
}
try {
const parsed = new URL(raw);
if (!['http:', 'https:'].includes(parsed.protocol)) {
return '';
}
return parsed.href;
} catch (_error) {
return '';
}
}
function parseAutoRunAdSegments(value = '') {
const rawText = String(value || '').trim();
if (!rawText) {
return [];
}
const segments = [];
let cursor = 0;
while (cursor < rawText.length) {
const labelStart = rawText.indexOf('[', cursor);
if (labelStart < 0) {
segments.push({
type: 'text',
text: rawText.slice(cursor),
});
break;
}
const labelEnd = rawText.indexOf(']', labelStart + 1);
if (labelEnd < 0 || rawText[labelEnd + 1] !== '(') {
segments.push({
type: 'text',
text: rawText.slice(cursor),
});
break;
}
if (labelStart > cursor) {
segments.push({
type: 'text',
text: rawText.slice(cursor, labelStart),
});
}
let urlEnd = labelEnd + 2;
let depth = 1;
while (urlEnd < rawText.length && depth > 0) {
const ch = rawText[urlEnd];
if (ch === '(') {
depth += 1;
} else if (ch === ')') {
depth -= 1;
}
urlEnd += 1;
}
if (depth > 0) {
segments.push({
type: 'text',
text: rawText.slice(labelStart),
});
break;
}
const label = rawText.slice(labelStart + 1, labelEnd).trim();
const rawSegment = rawText.slice(labelStart, urlEnd);
const url = sanitizeAutoRunAdUrl(rawText.slice(labelEnd + 2, urlEnd - 1));
if (label && url) {
segments.push({
type: 'link',
text: label,
url,
});
} else {
segments.push({
type: 'text',
text: rawSegment,
});
}
cursor = urlEnd;
}
return segments.filter((segment) => String(segment?.text || '').length > 0);
}
function getAutoRunAdPlainText(segments = []) {
return segments.map((segment) => String(segment?.text || '')).join('').replace(/\s+/g, ' ').trim();
}
function renderAutoRunAdSegments(container, segments = [], options = {}) {
if (!container) {
return;
}
const { tabIndex = undefined } = options;
container.textContent = '';
for (const segment of Array.isArray(segments) ? segments : []) {
const text = String(segment?.text || '');
if (!text) {
continue;
}
if (segment?.type === 'link' && segment?.url) {
const link = document.createElement('a');
link.className = 'auto-run-ad-link';
link.href = segment.url;
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.dataset.externalUrl = segment.url;
if (Number.isInteger(tabIndex)) {
link.tabIndex = tabIndex;
}
link.textContent = text;
container.appendChild(link);
continue;
}
container.appendChild(document.createTextNode(text));
}
}
function positionContributionUpdateHint() {
if (!contributionUpdateLayer || !contributionUpdateHint || !btnContributionMode) {
return;
@@ -11086,6 +11242,86 @@ function renderContributionUpdateHint(snapshot = currentContributionContentSnaps
positionContributionUpdateHint();
}
function resetAutoRunAdScrollState() {
if (!autoRunAdBar || !autoRunAdTrack || !autoRunAdTextClone) {
return;
}
autoRunAdBar.classList.remove('is-scrolling');
autoRunAdBar.style.removeProperty('--auto-run-ad-gap');
autoRunAdBar.style.removeProperty('--auto-run-ad-duration');
autoRunAdBar.style.removeProperty('--auto-run-ad-scroll-distance');
autoRunAdTextClone.textContent = '';
}
function syncAutoRunAdScrollState() {
if (!autoRunAdBar || !autoRunAdViewport || !autoRunAdTrack || !autoRunAdText || !autoRunAdTextClone || autoRunAdBar.hidden) {
resetAutoRunAdScrollState();
return;
}
const text = String(autoRunAdText.textContent || '').trim();
if (!text) {
resetAutoRunAdScrollState();
return;
}
const viewportWidth = autoRunAdViewport.clientWidth || 0;
const textWidth = Math.ceil(autoRunAdText.getBoundingClientRect().width || 0);
if (!viewportWidth || !textWidth || textWidth <= viewportWidth) {
resetAutoRunAdScrollState();
return;
}
const scrollGap = 32;
const scrollDistance = textWidth + scrollGap;
const durationSeconds = Math.max(18, scrollDistance / 24);
autoRunAdBar.classList.add('is-scrolling');
autoRunAdBar.style.setProperty('--auto-run-ad-gap', `${scrollGap}px`);
autoRunAdBar.style.setProperty('--auto-run-ad-duration', `${durationSeconds.toFixed(2)}s`);
autoRunAdBar.style.setProperty('--auto-run-ad-scroll-distance', `${scrollDistance}px`);
}
function scheduleAutoRunAdScrollSync() {
if (autoRunAdScrollSyncFrame && typeof window.cancelAnimationFrame === 'function') {
window.cancelAnimationFrame(autoRunAdScrollSyncFrame);
autoRunAdScrollSyncFrame = 0;
}
if (typeof window.requestAnimationFrame === 'function') {
autoRunAdScrollSyncFrame = window.requestAnimationFrame(() => {
autoRunAdScrollSyncFrame = 0;
syncAutoRunAdScrollState();
});
return;
}
syncAutoRunAdScrollState();
}
function renderAutoRunAd(snapshot = currentContributionContentSnapshot) {
if (!autoRunAdBar || !autoRunAdText || !autoRunAdTextClone) {
return;
}
const config = getAutoRunAdConfig(snapshot);
const visible = Boolean(config);
autoRunAdBar.hidden = !visible;
if (!visible) {
autoRunAdBar.title = '';
autoRunAdText.textContent = '';
autoRunAdTextClone.textContent = '';
resetAutoRunAdScrollState();
return;
}
const segments = parseAutoRunAdSegments(config.text);
const plainText = getAutoRunAdPlainText(segments);
autoRunAdBar.title = plainText || config.text;
renderAutoRunAdSegments(autoRunAdText, segments);
renderAutoRunAdSegments(autoRunAdTextClone, segments, { tabIndex: -1 });
scheduleAutoRunAdScrollSync();
}
function dismissContributionUpdateHint() {
const promptVersion = String(currentContributionContentSnapshot?.promptVersion || '').trim();
if (promptVersion) {
@@ -11098,6 +11334,7 @@ async function refreshContributionContentHint() {
if (!contributionContentService?.getContentUpdateSnapshot) {
currentContributionContentSnapshot = null;
renderContributionUpdateHint();
renderAutoRunAd();
return null;
}
if (contributionContentSnapshotRequestInFlight) {
@@ -11108,11 +11345,13 @@ async function refreshContributionContentHint() {
.then((snapshot) => {
currentContributionContentSnapshot = snapshot;
renderContributionUpdateHint(snapshot);
renderAutoRunAd(snapshot);
return snapshot;
})
.catch((error) => {
currentContributionContentSnapshot = null;
renderContributionUpdateHint(null);
renderAutoRunAd(null);
throw error;
})
.finally(() => {
@@ -13631,6 +13870,12 @@ autoStartMessage?.addEventListener('click', (event) => {
event.preventDefault();
openExternalUrl(link.dataset.externalUrl || link.href);
});
autoRunAdBar?.addEventListener('click', (event) => {
const link = event.target?.closest?.('a[data-external-url]');
if (!link) return;
event.preventDefault();
openExternalUrl(link.dataset.externalUrl || link.href);
});
btnAutoStartClose?.addEventListener('click', () => resolveModalChoice(null));
async function startAutoRunFromCurrentSettings() {
@@ -13642,11 +13887,10 @@ async function startAutoRunFromCurrentSettings() {
: getRunCountValue();
registerPendingAutoRunStartRunCount(requestedTotalRuns);
try {
await refreshContributionContentHint();
} catch (error) {
// 站点内容刷新只影响提示/广告展示,不应阻塞自动流程启动。
refreshContributionContentHint().catch((error) => {
console.warn('Failed to refresh contribution content hint before auto run:', error);
}
});
if (typeof persistCurrentSettingsForAction === 'function') {
await persistCurrentSettingsForAction();
@@ -16892,6 +17136,7 @@ document.addEventListener('keydown', (event) => {
window.addEventListener('resize', () => {
positionContributionUpdateHint();
scheduleAutoRunAdScrollSync();
});
document.addEventListener('scroll', () => {