📋 已记录:${visitedLinksMap.size + customVisitedMap.size} / ${MAX_TOTAL_RECORDS}
🔗 页面链接:${document.querySelectorAll('a[href]').length}
${underlineLabels[settings.underline]} (${settings.underline})
`;
parent.appendChild(overlay);
makeDraggable(overlay.querySelector('.glass-card'));
overlay.querySelector('#clear-page-btn').addEventListener('click', () => {
const count = clearCurrentPageRecords(true);
if (count === 0) { showConfirmDialog('当前页面没有可清理的访问记录。', null); return; }
showConfirmDialog(`确定要清理当前页面的 ${count} 条访问记录吗?\n(仅影响当前页面出现的链接,其它页面的记录保留)`,
() => { clearCurrentPageRecords(false); });
});
overlay.querySelector('#clear-all-btn').addEventListener('click', () => {
const count = visitedLinksMap.size + customVisitedMap.size;
if (count === 0) { showConfirmDialog('当前没有可清除的访问记录。', null); return; }
showConfirmDialog(`确定要清除所有 ${count} 条访问记录吗?此操作不可恢复,且不影响自动清理设置。`, () => {
const newToken = bumpCleanToken();
lastSeenCleanToken = newToken;
lastCleanInfo = { time: newToken, count };
cleanInfoDirty = true;
visitedLinksMap.clear();
customVisitedMap.clear();
forceSave(true);
GM_setValue('lastCleanToken', newToken);
removeVisitedClassFromAll();
updateStatsInPanel();
});
});
const hexInput = overlay.querySelector('#hex-input');
const svPanel = overlay.querySelector('#sv-panel');
const svBg = overlay.querySelector('.sv-bg');
const svCursor = overlay.querySelector('.sv-cursor');
const hueSlider = overlay.querySelector('#hue-slider');
const hueThumb = overlay.querySelector('.hue-thumb');
const previewBox = overlay.querySelector('#color-preview-box');
const weightSlider = overlay.querySelector('#weight-slider');
const weightDisplay = overlay.querySelector('#weight-display');
const alphaSlider = overlay.querySelector('#alpha-slider');
const alphaDisplay = overlay.querySelector('#alpha-display');
const underlineBtn = overlay.querySelector('#underline-preview-btn');
const underlineLabel = overlay.querySelector('#underline-label');
const highlightSwitch = overlay.querySelector('#highlight-switch');
const blurSwitch = overlay.querySelector('#blur-switch');
const autoCleanCheckbox = overlay.querySelector('#auto-clean-switch');
const autoCleanDaysBtn = overlay.querySelector('#auto-clean-days-btn');
hexInput.value = settings.color;
let svRect = null, hueRect = null;
const measureRects = () => {
try { svRect = svPanel.getBoundingClientRect(); } catch (_) { svRect = null; }
try { hueRect = hueSlider.getBoundingClientRect(); } catch (_) { hueRect = null; }
};
function applyColor(hex) {
settings.color = hex;
updateStylesDebounced();
if (previewBox.style.backgroundColor !== hex) previewBox.style.backgroundColor = hex;
if (document.activeElement !== hexInput && hexInput.value !== hex) hexInput.value = hex;
clearTimeout(saveColorTimer);
saveColorTimer = setTimeout(() => GM_setValue('linkColor', hex), 200);
updatePreviewLink();
}
const updateCursor = () => {
if (svCursor) {
svCursor.style.left = (currentS * 100) + '%';
svCursor.style.top = ((1 - currentV) * 100) + '%';
}
if (hueThumb) hueThumb.style.left = (currentH / 360 * 100) + '%';
};
const updateSvBackground = () => {
const hex = hsvToHex(currentH, 1, 1);
svBg.style.backgroundColor = hex;
svPanel.style.backgroundColor = hex;
};
const updateFromHsv = () => { applyColor(hsvToHex(currentH, currentS, currentV)); updateCursor(); };
function refreshPicker() {
if (!svPanel || svPanel.offsetWidth === 0 || svPanel.offsetHeight === 0) return;
const hsv = hexToHsv(settings.color);
currentH = hsv.h; currentS = hsv.s; currentV = hsv.v;
updateSvBackground();
updateFromHsv();
hexInput.value = settings.color;
previewBox.style.backgroundColor = settings.color;
}
overlay.refreshPicker = refreshPicker;
updateSvBackground();
updateFromHsv();
const setSvFromEvent = (e) => {
const rect = svRect || svPanel.getBoundingClientRect();
if (!rect || rect.width === 0) return;
const cx = e.touches ? e.touches[0].clientX : e.clientX;
const cy = e.touches ? e.touches[0].clientY : e.clientY;
currentS = Math.min(1, Math.max(0, (cx - rect.left) / rect.width));
currentV = 1 - Math.min(1, Math.max(0, (cy - rect.top) / rect.height));
updateFromHsv();
};
const setHueFromEvent = (e) => {
const rect = hueRect || hueSlider.getBoundingClientRect();
if (!rect || rect.width === 0) return;
const cx = e.touches ? e.touches[0].clientX : e.clientX;
const x = Math.min(rect.width, Math.max(0, cx - rect.left));
currentH = (x / rect.width) * 360;
updateSvBackground();
updateFromHsv();
};
const bindPicker = (el, measure, onPick) => {
el.addEventListener('mousedown', (e) => {
e.preventDefault(); measure(); onPick(e);
const onMove = (ev) => { ev.preventDefault(); onPick(ev); };
const onEnd = () => {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onEnd);
svRect = null; hueRect = null;
};
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onEnd);
});
el.addEventListener('touchstart', (e) => {
e.preventDefault(); measure(); onPick(e);
const onMove = (ev) => { ev.preventDefault(); onPick(ev); };
const onEnd = () => {
document.removeEventListener('touchmove', onMove);
document.removeEventListener('touchend', onEnd);
svRect = null; hueRect = null;
};
document.addEventListener('touchmove', onMove, { passive: false });
document.addEventListener('touchend', onEnd);
}, { passive: false });
};
bindPicker(svPanel, measureRects, setSvFromEvent);
bindPicker(hueSlider, measureRects, setHueFromEvent);
const applyHex = (hex, revertOnFail) => {
hex = (hex || '').trim();
if (hex.length === 4 && /^#[0-9a-fA-F]{3}$/.test(hex)) hex = '#' + hex[1] + hex[1] + hex[2] + hex[2] + hex[3] + hex[3];
if (/^#[0-9a-fA-F]{6}$/.test(hex)) {
const hsv = hexToHsv(hex);
currentH = hsv.h; currentS = hsv.s; currentV = hsv.v;
updateSvBackground();
updateFromHsv();
} else if (revertOnFail) hexInput.value = settings.color;
};
hexInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.stopPropagation(); applyHex(hexInput.value, true); } });
hexInput.addEventListener('blur', () => applyHex(hexInput.value, true));
underlineBtn.addEventListener('click', () => {
showUnderlinePicker(settings.underline, (val) => {
settings.underline = val;
GM_setValue('underlineType', val);
updateStylesDebounced();
underlineLabel.textContent = `${underlineLabels[val]} (${val})`;
updatePreviewLink();
});
});
underlineBtn.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); underlineBtn.click(); } });
weightSlider.addEventListener('input', (e) => {
const v = e.target.value;
weightDisplay.textContent = v;
settings.weight = v;
updateStylesDebounced();
clearTimeout(saveWeightTimer);
saveWeightTimer = setTimeout(() => GM_setValue('linkWeight', v), 200);
updatePreviewLink();
});
alphaSlider.addEventListener('input', (e) => {
const v = parseFloat(e.target.value).toFixed(2);
alphaDisplay.textContent = v;
settings.glassAlpha = parseFloat(v);
if (document.documentElement) document.documentElement.style.setProperty('--vlx-alpha', v);
clearTimeout(saveAlphaTimer);
saveAlphaTimer = setTimeout(() => GM_setValue('glassAlpha', settings.glassAlpha), 200);
scheduleThemeDetect(200);
});
blurSwitch.addEventListener('change', (e) => {
settings.blurEnabled = e.target.checked;
GM_setValue('blurEnabled', settings.blurEnabled);
if (document.documentElement) document.documentElement.style.setProperty('--vlx-blur', settings.blurEnabled ? '12px' : '0px');
});
highlightSwitch.addEventListener('change', (e) => {
highlightEnabled = e.target.checked;
GM_setValue('highlightEnabled', highlightEnabled);
applyHighlightState();
updateStatsInPanel();
});
autoCleanCheckbox.addEventListener('change', (e) => {
autoCleanEnabled = e.target.checked;
GM_setValue('autoCleanEnabled', autoCleanEnabled);
if (autoCleanEnabled) applyAutoClean();
});
autoCleanDaysBtn.addEventListener('click', () => {
showDayPicker(autoCleanDays, (d) => {
autoCleanDays = d;
GM_setValue('autoCleanDays', autoCleanDays);
autoCleanDaysBtn.textContent = formatDayLabel(autoCleanDays);
if (autoCleanEnabled) applyAutoClean();
});
});
const closePanel = () => { stopVlxScrollRepaint(); overlay.classList.remove('show'); };
overlay.querySelector('.close-btn').addEventListener('click', closePanel);
overlay.addEventListener('click', (e) => { if (e.target === overlay) closePanel(); });
return overlay;
}
// ---- 路由与重试 ----
function patchRouter() {
const origPush = history.pushState, origReplace = history.replaceState;
history.pushState = function(...args) { origPush.apply(this, args); onRouteChange(); };
history.replaceState = function(...args) { origReplace.apply(this, args); onRouteChange(); };
window.addEventListener('popstate', onRouteChange);
window.addEventListener('hashchange', onRouteChange);
}
function onRouteChange() {
clearTimeout(routeRetryTimer);
routeRetryTimer = setTimeout(() => applyVisitedStylesWithRetry(), 100);
}
function applyVisitedStylesWithRetry(maxRetries = 5, delay = 400) {
let retries = 0;
const tryApply = () => {
if (hasValidBody()) { applyVisitedStyles(); return; }
if (++retries < maxRetries) setTimeout(tryApply, delay);
};
tryApply();
}
// ---- 事件监听 ----
function setupEventListeners() {
document.addEventListener('click', (e) => {
if (touchHandledTarget) {
if (touchHandledTarget.contains(e.target) || e.target === touchHandledTarget) {
if (Date.now() - touchHandledTime < 1000) return;
}
clearTimeout(touchHandledTimer);
touchHandledTarget = null;
}
handleInteraction(e.target, e);
}, true);
document.addEventListener('auxclick', (e) => {
if (e.button === 1) {
if (touchHandledTarget && touchHandledTarget.contains(e.target) && Date.now() - touchHandledTime < 1000) return;
handleInteraction(e.target, e);
}
}, true);
document.addEventListener('touchstart', (e) => {
if (e.touches.length > 0) { touchStartX = e.touches[0].clientX; touchStartY = e.touches[0].clientY; }
}, { passive: true });
document.addEventListener('touchend', (e) => {
const t = e.changedTouches[0];
if (!t) return;
const dx = t.clientX - touchStartX, dy = t.clientY - touchStartY;
if (dx * dx + dy * dy < 100) {
touchHandledTarget = e.target;
touchHandledTime = Date.now();
clearTimeout(touchHandledTimer);
touchHandledTimer = setTimeout(() => { touchHandledTarget = null; }, 1000);
handleInteraction(e.target, e);
}
}, { passive: true });
document.addEventListener('dragstart', (e) => {
if (e.target.closest(SCRIPT_UI_SELECTOR)) return;
dragTarget = e.target;
dragTime = Date.now();
}, true);
document.addEventListener('dragend', () => { handleDragLink(); }, true);
let rangeActiveAdded = false;
document.addEventListener('pointerdown', (e) => {
const slider = e.target?.closest?.('.vlx input[type="range"]');
if (slider) { slider.classList.add('vlx-range-active'); rangeActiveAdded = true; }
const hue = e.target?.closest?.('.vlx .hue-slider');
if (hue) { hue.classList.add('vlx-hue-active'); rangeActiveAdded = true; }
}, true);
const clearRangeActive = () => {
if (!rangeActiveAdded) return;
rangeActiveAdded = false;
document.querySelectorAll('.vlx input[type="range"].vlx-range-active').forEach(el => el.classList.remove('vlx-range-active'));
document.querySelectorAll('.vlx .hue-slider.vlx-hue-active').forEach(el => el.classList.remove('vlx-hue-active'));
};
document.addEventListener('pointerup', clearRangeActive, true);
document.addEventListener('pointercancel', clearRangeActive, true);
window.addEventListener('blur', () => { if (dragTarget && Date.now() - dragTime < 3000) handleDragLink(); });
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
if (dragTarget && Date.now() - dragTime < 3000) handleDragLink();
forceSave();
}
});
document.addEventListener('keydown', (e) => {
if (e.key !== 'Escape') return;
const shown = document.querySelectorAll('.vlx.glass-overlay.show');
if (!shown.length) return;
const top = shown[shown.length - 1];
const btn = top.querySelector('.close-btn');
if (btn) btn.click();
});
window.addEventListener('scroll', handleVlxScroll, { passive: true, capture: true });
}
function setupHrefObserver() {
if (!document.documentElement) return;
hrefObserver = new MutationObserver((mutations) => {
for (const m of mutations) {
if (m.type !== 'attributes') continue;
const t = m.target;
if (t.nodeType !== 1) continue;
if (t.closest && t.closest(SCRIPT_UI_SELECTOR)) continue;
if (m.attributeName === 'href' && t.tagName && t.tagName.toUpperCase() === 'A') {
const href = getElementHref(t);
if (href && visitedLinksMap.has(href)) markElementVisited(t);
else if (t.classList && t.classList.contains('visited-link')) t.classList.remove('visited-link');
} else if (m.attributeName === 'data-href' || m.attributeName === 'data-url' || m.attributeName === 'id') {
// 带 href 的 A 元素优先按 href 判断,避免 id/data-* 变化时丢失已访问标记
if (t.tagName && t.tagName.toUpperCase() === 'A') {
const href = getElementHref(t);
if (href) {
if (visitedLinksMap.has(href)) markElementVisited(t);
else if (t.classList && t.classList.contains('visited-link')) t.classList.remove('visited-link');
continue;
}
}
if (t.classList) t.classList.remove('visited-link');
const key = getVisitedKeyFromElement(t);
if (key && customVisitedMap.has(key)) markElementVisited(t);
}
}
});
hrefObserver.observe(document.documentElement, {
attributes: true, attributeFilter: ['href', 'data-href', 'data-url', 'id'], subtree: true
});
}
function setupMutationObserver() {
if (!document.documentElement) return;
let pendingMutations = [], flushScheduled = false;
const scheduleFlush = () => {
if (flushScheduled) return;
flushScheduled = true;
microtask(() => {
flushScheduled = false;
const newNodes = [], removedNodes = [];
for (const m of pendingMutations) {
if (m.type === 'childList') {
m.addedNodes.forEach(n => newNodes.push(n));
m.removedNodes.forEach(n => removedNodes.push(n));
}
}
pendingMutations = [];
if (newNodes.length) {
if (newNodes.length > CHUNK_SIZE) {
let idx = 0;
const next = () => {
const batch = newNodes.slice(idx, idx + CHUNK_SIZE);
idx += batch.length;
applyStylesToNewNodes(batch);
if (idx < newNodes.length) requestIdleCallback(next, { timeout: 100 });
};
requestIdleCallback(next, { timeout: 100 });
} else applyStylesToNewNodes(newNodes);
}
for (const node of removedNodes) if (node.nodeType === 1) cleanupShadowRootsInNode(node);
});
};
observer = new MutationObserver((mutations) => { pendingMutations.push(...mutations); scheduleFlush(); });
observer.observe(document.documentElement, { childList: true, subtree: true });
}
function detachShadowEntry(shadowRoot) {
if (shadowRoot && shadowStyleMap.has(shadowRoot)) {
const data = shadowStyleMap.get(shadowRoot);
if (data.observer) data.observer.disconnect();
shadowStyleMap.delete(shadowRoot);
}
}
function cleanupShadowRootsInNode(node) {
if (shadowStyleMap.size === 0) return;
detachShadowEntry(node.shadowRoot);
const now = Date.now();
if (now - lastRemovalDeepScan < 2000) return;
lastRemovalDeepScan = now;
if (node.querySelectorAll) node.querySelectorAll('*').forEach(c => detachShadowEntry(c.shadowRoot));
}
function hookAttachShadow() {
if (typeof Element.prototype.attachShadow !== 'function') return;
const orig = Element.prototype.attachShadow;
Element.prototype.attachShadow = function(init) {
const sr = orig.call(this, init);
injectShadowStyle(sr);
return sr;
};
document.querySelectorAll('*').forEach(el => { if (el.shadowRoot) injectShadowStyle(el.shadowRoot); });
}
function ensureStylesHealthy() {
try {
const css = getStyleText();
if (!styleElement || !styleElement.isConnected) updateDocumentStyle();
else if (styleElement.textContent !== css) styleElement.textContent = css;
const dups = document.querySelectorAll('#visited-links-style');
if (dups.length > 1) dups.forEach(el => { if (el !== styleElement) el.remove(); });
cleanupShadowStyleMap();
for (const [, data] of shadowStyleMap) if (data.style.textContent !== css) data.style.textContent = css;
} catch (_) {}
}
// ---- 阶段初始化 ----
function phase0() {
hookAttachShadow();
patchRouter();
initVisitedLinks();
setupCrossContextSync();
setupColorSchemeGuard();
try {
if (document.documentElement) {
document.documentElement.style.setProperty('--vlx-alpha', String(settings.glassAlpha));
document.documentElement.style.setProperty('--vlx-blur', settings.blurEnabled ? '12px' : '0px');
}
} catch (_) {}
updateDocumentStyle();
setupEventListeners();
if (document.documentElement) {
setupMutationObserver();
setupHrefObserver();
} else {
const rootObs = new MutationObserver(() => {
if (document.documentElement) {
rootObs.disconnect();
setupMutationObserver();
setupHrefObserver();
updateDocumentStyle();
}
});
rootObs.observe(document, { childList: true, subtree: true });
}
}
function phase1() {
if (isPhase1Inited) return;
isPhase1Inited = true;
injectGlobalDialogStyles();
applyVisitedStyles();
setInterval(() => {
try { cleanupShadowStyleMap(); applyAutoClean(); ensureStylesHealthy(); } catch (_) {}
}, CLEANUP_INTERVAL);
}
function phase2() {
try { applyVisitedStyles(); } catch (_) {}
try { applyAutoClean(); } catch (_) {}
}
function attemptPhase1() {
if (document.body) { phase1(); return; }
const initObs = new MutationObserver(() => { if (document.body) { initObs.disconnect(); phase1(); } });
if (document.documentElement) initObs.observe(document.documentElement, { childList: true, subtree: true });
setTimeout(() => { initObs.disconnect(); if (document.body) phase1(); }, 5000);
}
function openPanel() {
if (!panelOverlay) {
panelOverlay = createSettingsPanel();
if (!panelOverlay) return;
updateStatsInPanel();
updatePreviewLink();
}
if (!panelOverlay.isConnected) {
const parent = document.body || document.documentElement;
if (!parent) return;
parent.appendChild(panelOverlay);
}
if (!document.getElementById('glass-dialog-styles')) injectGlobalDialogStyles();
panelOverlay._pageLinkCount = undefined;
panelOverlay._refs = null;
quickThemeCheck();
panelOverlay.classList.add('show');
updateStatsInPanel();
updatePreviewLink();
const svPanel = panelOverlay.querySelector('#sv-panel');
if (svPanel) {
const tryRefresh = () => { if (svPanel.offsetWidth > 0 && svPanel.offsetHeight > 0) panelOverlay.refreshPicker?.(); };
tryRefresh();
try {
if (!panelResizeObserver) panelResizeObserver = new ResizeObserver(tryRefresh);
else panelResizeObserver.disconnect();
panelResizeObserver.observe(svPanel);
setTimeout(() => { if (panelResizeObserver) panelResizeObserver.disconnect(); tryRefresh(); }, 1000);
} catch (_) {}
}
setTimeout(detectBackdropTheme, 30);
setTimeout(detectBackdropTheme, 420);
}
// ---- 启动 ----
phase0();
if (isTopFrame) GM_registerMenuCommand('⚙️ 打开已访问链接样式面板', openPanel);
attemptPhase1();
if (document.readyState === 'complete') phase2();
else window.addEventListener('load', phase2, { once: true });
window.addEventListener('pagehide', () => { forceSave(); });
window.addEventListener('beforeunload', () => {
forceSave();
if (observer) observer.disconnect();
if (hrefObserver) hrefObserver.disconnect();
clearTimeout(saveTimer);
clearTimeout(styleUpdateTimer);
clearTimeout(statsUpdateTimer);
for (const [, data] of shadowStyleMap) if (data.observer) data.observer.disconnect();
});
})();