📋 已记录:${visitedLinksMap.size + customVisitedMap.size} / ${MAX_LINKS}
🔗 页面链接:${document.querySelectorAll('a[href]').length}
${underlineLabels[settings.underline]} (${settings.underline})
`;
(document.body || document.documentElement).appendChild(overlay);
const card = overlay.querySelector('.glass-card');
makeDraggable(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} 条访问记录吗?此操作不可恢复,且不影响自动清理设置。`, () => {
lastCleanInfo = { time: Date.now(), count: count };
visitedLinksMap.clear();
customVisitedMap.clear();
forceSave();
removeVisitedClassFromAll();
updateStatsInPanel();
});
});
const hexInput = overlay.querySelector('#hex-input');
hexInput.value = settings.color;
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');
function applyColor(hex) {
settings.color = hex;
updateStylesDebounced();
previewBox.style.backgroundColor = hex;
if (document.activeElement !== hexInput) hexInput.value = hex;
clearTimeout(saveColorTimer);
saveColorTimer = setTimeout(() => GM_setValue('linkColor', hex), 200);
}
const updateCursor = () => {
if (svCursor && svPanel) {
const rect = svPanel.getBoundingClientRect();
svCursor.style.left = (currentS * rect.width) + 'px';
svCursor.style.top = ((1 - currentV) * rect.height) + 'px';
}
if (hueThumb && hueSlider) hueThumb.style.left = (currentH / 360 * 100) + '%';
};
const updateSvBackground = () => { svBg.style.backgroundColor = hsvToHex(currentH, 1, 1); };
const updateFromHsv = () => {
const hex = hsvToHex(currentH, currentS, currentV);
applyColor(hex);
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 = svPanel.getBoundingClientRect();
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
currentS = Math.min(1, Math.max(0, (clientX - rect.left) / rect.width));
currentV = 1 - Math.min(1, Math.max(0, (clientY - rect.top) / rect.height));
updateFromHsv();
};
svPanel.addEventListener('mousedown', (e) => {
e.preventDefault();
setSvFromEvent(e);
const moveH = (ev) => { ev.preventDefault(); setSvFromEvent(ev); };
const upH = () => {
document.removeEventListener('mousemove', moveH);
document.removeEventListener('mouseup', upH);
};
document.addEventListener('mousemove', moveH);
document.addEventListener('mouseup', upH);
});
svPanel.addEventListener('touchstart', (e) => {
e.preventDefault();
setSvFromEvent(e);
const moveH = (ev) => { ev.preventDefault(); setSvFromEvent(ev); };
const endH = () => {
document.removeEventListener('touchmove', moveH);
document.removeEventListener('touchend', endH);
};
document.addEventListener('touchmove', moveH, { passive: false });
document.addEventListener('touchend', endH);
}, { passive: false });
const setHueFromEvent = (e) => {
const rect = hueSlider.getBoundingClientRect();
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
let x = clientX - rect.left;
x = Math.min(rect.width, Math.max(0, x));
currentH = Math.round((x / rect.width) * 360);
updateSvBackground();
updateFromHsv();
};
hueSlider.addEventListener('mousedown', (e) => {
e.preventDefault();
setHueFromEvent(e);
const moveH = (ev) => { ev.preventDefault(); setHueFromEvent(ev); };
const upH = () => {
document.removeEventListener('mousemove', moveH);
document.removeEventListener('mouseup', upH);
};
document.addEventListener('mousemove', moveH);
document.addEventListener('mouseup', upH);
});
hueSlider.addEventListener('touchstart', (e) => {
e.preventDefault();
setHueFromEvent(e);
const moveH = (ev) => { ev.preventDefault(); setHueFromEvent(ev); };
const endH = () => {
document.removeEventListener('touchmove', moveH);
document.removeEventListener('touchend', endH);
};
document.addEventListener('touchmove', moveH, { passive: false });
document.addEventListener('touchend', endH);
}, { passive: false });
const applyHex = (hex) => {
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 newHsv = hexToHsv(hex);
currentH = newHsv.h; currentS = newHsv.s; currentV = newHsv.v;
updateSvBackground();
updateFromHsv();
}
};
hexInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.stopPropagation(); applyHex(hexInput.value); }
});
hexInput.addEventListener('blur', () => applyHex(hexInput.value));
const underlineBtn = overlay.querySelector('#underline-preview-btn');
underlineBtn.addEventListener('click', () => {
showUnderlinePicker(settings.underline, (val) => {
settings.underline = val;
GM_setValue('underlineType', val);
updateStylesDebounced();
overlay.querySelector('#underline-label').textContent = `${underlineLabels[val]} (${val})`;
});
});
underlineBtn.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); underlineBtn.click(); }
});
overlay.querySelector('#weight-slider').addEventListener('input', (e) => {
const val = e.target.value;
overlay.querySelector('#weight-display').textContent = val;
settings.weight = val;
updateStylesDebounced();
clearTimeout(saveWeightTimer);
saveWeightTimer = setTimeout(() => GM_setValue('linkWeight', val), 200);
});
overlay.querySelector('#alpha-slider').addEventListener('input', (e) => {
const val = parseFloat(e.target.value).toFixed(2);
overlay.querySelector('#alpha-display').textContent = val;
settings.glassAlpha = parseFloat(val);
document.documentElement.style.setProperty('--vlx-alpha', val);
clearTimeout(saveAlphaTimer);
saveAlphaTimer = setTimeout(() => GM_setValue('glassAlpha', settings.glassAlpha), 200);
});
document.documentElement.style.setProperty('--vlx-alpha', settings.glassAlpha);
overlay.querySelector('#blur-switch').addEventListener('change', (e) => {
settings.blurEnabled = e.target.checked;
GM_setValue('blurEnabled', settings.blurEnabled);
document.documentElement.style.setProperty('--vlx-blur', settings.blurEnabled ? '12px' : '0px');
});
const autoCleanCheckbox = overlay.querySelector('#auto-clean-switch');
const autoCleanDaysBtn = overlay.querySelector('#auto-clean-days-btn');
autoCleanCheckbox.addEventListener('change', (e) => {
autoCleanEnabled = e.target.checked;
GM_setValue('autoCleanEnabled', autoCleanEnabled);
if (autoCleanEnabled) applyAutoClean();
});
autoCleanDaysBtn.addEventListener('click', () => {
showDayPicker(autoCleanDays, (newDays) => {
autoCleanDays = newDays;
GM_setValue('autoCleanDays', autoCleanDays);
autoCleanDaysBtn.textContent = dayLabels[autoCleanDays];
if (autoCleanEnabled) applyAutoClean();
});
});
const closeBtn = overlay.querySelector('.close-btn');
const closePanel = () => overlay.classList.remove('show');
closeBtn.addEventListener('click', closePanel);
overlay.addEventListener('click', (e) => { if (e.target === overlay) closePanel(); });
overlay.addEventListener('keydown', (e) => {
if (e.key === 'Escape') { e.stopPropagation(); closePanel(); }
});
return overlay;
}
/* ========== 路由监听 ========== */
function patchRouter() {
const origPush = history.pushState;
const 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) {
const now = Date.now();
if (touchHandledTarget.contains(e.target) || e.target === touchHandledTarget) {
if (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 touch = e.changedTouches[0];
if (!touch) return;
const dx = Math.abs(touch.clientX - touchStartX);
const dy = Math.abs(touch.clientY - touchStartY);
if (Math.sqrt(dx*dx+dy*dy) < 10) {
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);
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();
});
}
/* ========== 监听 href / data-href / data-url 属性变化 ========== */
// 修复(疏漏 6):属性变化时,若新值不再匹配,移除旧 .visited-link,避免残留
function setupHrefObserver() {
hrefObserver = new MutationObserver((mutations) => {
for (const m of mutations) {
if (m.type !== 'attributes') continue;
const t = m.target;
if (t.nodeType !== 1) continue;
if (m.attributeName === 'href' && t.tagName === 'A') {
if (t.href && visitedLinksMap.has(t.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') {
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'],
subtree: true
});
}
/* ========== 全局 MutationObserver ========== */
function setupMutationObserver() {
let pendingMutations = [];
let flushScheduled = false;
const scheduleFlush = () => {
if (flushScheduled) return;
flushScheduled = true;
queueMicrotask(() => {
flushScheduled = false;
const newNodes = [];
const removedNodes = [];
for (const m of pendingMutations) {
if (m.type === 'childList') {
m.addedNodes.forEach(node => newNodes.push(node));
m.removedNodes.forEach(node => removedNodes.push(node));
}
}
pendingMutations = [];
if (newNodes.length) {
if (newNodes.length > CHUNK_SIZE) {
let idx = 0;
const processNext = () => {
const batch = newNodes.slice(idx, idx + CHUNK_SIZE);
idx += batch.length;
applyStylesToNewNodes(batch);
if (idx < newNodes.length) {
requestIdleCallback(processNext, { timeout: 100 });
}
};
requestIdleCallback(processNext, { 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(child => detachShadowEntry(child.shadowRoot));
}
}
/* ========== 拦截 attachShadow ========== */
function hookAttachShadow() {
const orig = Element.prototype.attachShadow;
Element.prototype.attachShadow = function(init) {
const shadowRoot = orig.call(this, init);
injectShadowStyle(shadowRoot);
return shadowRoot;
};
document.querySelectorAll('*').forEach(el => {
if (el.shadowRoot) injectShadowStyle(el.shadowRoot);
});
}
/* ========== 初始化 ========== */
function coreInit() {
if (isCoreInited) return;
isCoreInited = true;
hookAttachShadow();
initVisitedLinks();
enforceLimits();
applyAutoClean();
updateDocumentStyle();
injectGlobalDialogStyles();
document.documentElement.style.setProperty('--vlx-alpha', settings.glassAlpha);
document.documentElement.style.setProperty('--vlx-blur', settings.blurEnabled ? '12px' : '0px');
panelOverlay = createSettingsPanel();
updateStatsInPanel();
const openPanel = () => {
if (!panelOverlay) return;
if (!panelOverlay.isConnected) {
(document.body || document.documentElement).appendChild(panelOverlay);
}
if (!document.getElementById('glass-dialog-styles')) injectGlobalDialogStyles();
panelOverlay._pageLinkCount = undefined;
panelOverlay.classList.add('show');
updateStatsInPanel();
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 (e) {}
}
};
GM_registerMenuCommand('⚙️ 打开已访问链接样式面板', openPanel);
setupEventListeners();
setupMutationObserver();
setupHrefObserver();
applyVisitedStyles();
patchRouter();
setInterval(() => {
try {
cleanupShadowStyleMap();
applyAutoClean();
} catch (e) {}
}, CLEANUP_INTERVAL);
}
function attemptInit() {
if (document.body) {
coreInit();
} else {
const initObserver = new MutationObserver(() => {
if (document.body) {
initObserver.disconnect();
coreInit();
}
});
initObserver.observe(document.documentElement, { childList: true, subtree: true });
setTimeout(() => {
initObserver.disconnect();
if (document.body) coreInit();
}, 5000);
}
}
attemptInit();
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();
});
})();