// ==UserScript== // @name 已访问链接样式修改 - 控制面板 // @version 2026.10.1 // @description 自定义已访问链接样式 // @tag 链接 标记 自定义 下划线 // @author zzz妄炁 & AI // @match *://*/* // @run-at document-end // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @compatible chrome // @compatible firefox // @compatible safari // @namespace https://scriptcat.org/zh-CN/search // @supportURL https://scriptcat.org/zh-CN/script-show-page/2822 // @icon https://img.soogif.com/6QlDQPIFsYakXFhmBt1a02vNk9G5ecYf.gif // @license MIT // ==/UserScript== (function() { 'use strict'; if (window.__visitedLinkScriptInited) return; window.__visitedLinkScriptInited = true; /* ========== 兼容性 Polyfill ========== */ if (typeof requestIdleCallback !== 'function') { window.requestIdleCallback = function(cb, options) { const start = Date.now(); return setTimeout(function() { cb({ didTimeout: false, timeRemaining: function() { return Math.max(0, 50 - (Date.now() - start)); }}); }, options?.timeout || 1); }; window.cancelIdleCallback = function(id) { clearTimeout(id); }; } if (typeof CSS === 'undefined' || !CSS.escape) { window.CSS = window.CSS || {}; CSS.escape = function(str) { return String(str).replace(/[^a-zA-Z0-9_-]/g, function(c) { return '\\' + c; }); }; } function escAttrValue(s) { return String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"'); } /* ========== 常量配置 ========== */ const MAX_LINKS = 5000; const CLEANUP_INTERVAL = 30000; const DEBOUNCE_SAVE = 300; const DEBOUNCE_STYLE = 150; const CHUNK_SIZE = 200; const MAX_PENDING_SCAN = 200; const CANDIDATE_SELECTOR = 'a[href], [data-href], [data-url], button, [role="button"], [role="link"], [onclick], [download]'; const SCRIPT_UI_SELECTOR = '.vlx, #visited-links-style, #glass-dialog-styles'; const INTERACTIVE_SELECTOR = 'a, button, img, input, label, [role="button"], [role="link"], [onclick], [download], [data-href], [data-url]'; const underlineLabels = { 'solid': '实线', 'dashed': '虚线', 'wavy': '波浪线', 'double': '双线', 'dotted': '点状线', 'none': '无' }; const dayLabels = { 1: '保留 1 天', 7: '保留 7 天', 30: '保留 30 天', 90: '保留 90 天' }; /* ========== 状态管理 ========== */ const settings = { color: GM_getValue('linkColor', '#FF0000'), underline: GM_getValue('underlineType', 'solid'), weight: GM_getValue('linkWeight', '400'), glassAlpha: GM_getValue('glassAlpha', 0.7), blurEnabled: GM_getValue('blurEnabled', true) }; let autoCleanEnabled = GM_getValue('autoCleanEnabled', false); let autoCleanDays = GM_getValue('autoCleanDays', 7); let lastCleanInfo = { time: GM_getValue('lastCleanTime', 0), count: GM_getValue('lastCleanCount', 0) }; (function validateSettings() { if (!/^#[0-9a-fA-F]{6}$/.test(settings.color)) { settings.color = '#FF0000'; GM_setValue('linkColor', settings.color); } if (!underlineLabels[settings.underline]) { settings.underline = 'solid'; GM_setValue('underlineType', settings.underline); } const w = parseInt(settings.weight, 10); if (!(w >= 100 && w <= 900)) { settings.weight = '400'; GM_setValue('linkWeight', '400'); } else { settings.weight = String(w); } const a = parseFloat(settings.glassAlpha); if (isNaN(a) || a < 0.3 || a > 0.8) { settings.glassAlpha = 0.7; GM_setValue('glassAlpha', 0.7); } else { settings.glassAlpha = a; } if (typeof settings.blurEnabled !== 'boolean') { settings.blurEnabled = true; GM_setValue('blurEnabled', true); } if (!dayLabels[autoCleanDays]) { autoCleanDays = 7; GM_setValue('autoCleanDays', 7); } })(); let visitedLinksMap = new Map(); let customVisitedMap = new Map(); let dragTarget = null; let dragTime = 0; let observer = null; let hrefObserver = null; let styleElement = null; const shadowStyleMap = new Map(); let saveTimer = null; let styleUpdateTimer = null; let panelOverlay = null; let touchStartX = 0, touchStartY = 0; let touchHandledTarget = null; let touchHandledTime = 0; let touchHandledTimer = null; let routeRetryTimer = null; let saveWeightTimer = null; let saveAlphaTimer = null; let saveColorTimer = null; let statsUpdateTimer = null; const processedNodeSet = new WeakSet(); let pendingNodes = []; let pendingFlushScheduled = false; let lastRemovalDeepScan = 0; let isCoreInited = false; let panelResizeObserver = null; /* ========== 数据持久化 ========== */ function initVisitedLinks() { try { let raw = GM_getValue('visitedLinks', '[]'); if (typeof raw === 'string') raw = JSON.parse(raw); if (!Array.isArray(raw)) raw = []; visitedLinksMap.clear(); raw.forEach(item => { if (typeof item === 'string') { visitedLinksMap.set(item, { time: Date.now() }); } else if (item.url) { visitedLinksMap.set(item.url, { time: item.time || Date.now() }); } }); let customRaw = GM_getValue('customVisitedKeys', '[]'); if (typeof customRaw === 'string') customRaw = JSON.parse(customRaw); if (!Array.isArray(customRaw)) customRaw = []; customVisitedMap.clear(); customRaw.forEach(item => { if (typeof item === 'string') { if (isStableKey(item)) customVisitedMap.set(item, { time: Date.now() }); } else if (item.key && isStableKey(item.key)) { customVisitedMap.set(item.key, { time: item.time || Date.now() }); } }); } catch (_) { visitedLinksMap.clear(); customVisitedMap.clear(); } } function isStableKey(key) { return !!key && (key.startsWith('id:') || key.startsWith('dh:') || key.startsWith('du:')); } function forceSave() { try { const linksArray = Array.from(visitedLinksMap.entries()) .map(([url, data]) => ({ url, time: data.time })); const customArray = Array.from(customVisitedMap.entries()) .map(([key, data]) => ({ key, time: data.time })); GM_setValue('visitedLinks', JSON.stringify(linksArray)); GM_setValue('customVisitedKeys', JSON.stringify(customArray)); GM_setValue('lastCleanTime', lastCleanInfo.time); GM_setValue('lastCleanCount', lastCleanInfo.count); } catch (_) {} } function saveVisitedLinksDebounced() { clearTimeout(saveTimer); saveTimer = setTimeout(forceSave, DEBOUNCE_SAVE); } /* ========== 遍历辅助 ========== */ // 遍历主文档与所有活动 Shadow Root 内的 .visited-link function eachVisitedLinkElement(cb) { try { document.querySelectorAll('.visited-link').forEach(cb); } catch (_) {} shadowStyleMap.forEach((data, root) => { if (root.host && root.host.isConnected) { try { root.querySelectorAll('.visited-link').forEach(cb); } catch (_) {} } }); } // 清理:不再匹配当前记录的 .visited-link 类 function removeUnmatchedVisitedClasses() { eachVisitedLinkElement(el => { if (el.closest && el.closest(SCRIPT_UI_SELECTOR)) return; if (el.tagName === 'A' && el.href) { if (!visitedLinksMap.has(el.href)) el.classList.remove('visited-link'); } else { const key = getVisitedKeyFromElement(el); if (!key || !customVisitedMap.has(key)) el.classList.remove('visited-link'); } }); } // 全部清理(仅真实页面元素,UI 保留) function removeAllVisitedClasses() { eachVisitedLinkElement(el => { if (el.closest && el.closest(SCRIPT_UI_SELECTOR)) return; el.classList.remove('visited-link'); }); } /* ========== 清理逻辑 ========== */ function removeVisitedClassFromAll() { try { removeAllVisitedClasses(); } catch (e) { console.warn('[已访问链接样式] 清理出错', e); } } function collectCurrentPageKeys() { const urls = new Set(); const keys = new Set(); const collectFrom = (root) => { try { root.querySelectorAll('a[href], [data-href], [data-url], [id]').forEach(el => { if (el.closest(SCRIPT_UI_SELECTOR)) return; if (el.tagName === 'A' && el.href) { urls.add(el.href); } else { const key = getVisitedKeyFromElement(el); if (key && isStableKey(key)) keys.add(key); } }); } catch (_) {} }; collectFrom(document); shadowStyleMap.forEach((data, sroot) => { if (sroot.host && sroot.host.isConnected) collectFrom(sroot); }); if (location.href) urls.add(location.href); return { urls, keys }; } function clearCurrentPageRecords(dryRun = false) { const { urls, keys } = collectCurrentPageKeys(); let removed = 0; if (dryRun) { for (const url of urls) if (visitedLinksMap.has(url)) removed++; for (const key of keys) if (customVisitedMap.has(key)) removed++; return removed; } for (const url of urls) { if (visitedLinksMap.delete(url)) removed++; } for (const key of keys) { if (customVisitedMap.delete(key)) removed++; } if (removed > 0) { lastCleanInfo = { time: Date.now(), count: removed }; forceSave(); removeUnmatchedVisitedClasses(); updateStatsInPanel(); } return removed; } function applyAutoClean() { try { if (!autoCleanEnabled) return; const threshold = Date.now() - autoCleanDays * 86400000; let removedCount = 0; for (const [url, data] of visitedLinksMap) { if (data.time < threshold) { visitedLinksMap.delete(url); removedCount++; } } for (const [key, data] of customVisitedMap) { if (data.time < threshold) { customVisitedMap.delete(key); removedCount++; } } if (removedCount > 0) { lastCleanInfo = { time: Date.now(), count: removedCount }; removeUnmatchedVisitedClasses(); forceSave(); } enforceLimits(); updateStatsInPanelDebounced(); } catch (e) { console.warn('[已访问链接样式] 自动清理出错', e); } } function enforceLimits() { if (visitedLinksMap.size <= MAX_LINKS && customVisitedMap.size <= MAX_LINKS) return; while (visitedLinksMap.size > MAX_LINKS) { const firstKey = visitedLinksMap.keys().next().value; visitedLinksMap.delete(firstKey); } while (customVisitedMap.size > MAX_LINKS) { const firstKey = customVisitedMap.keys().next().value; customVisitedMap.delete(firstKey); } } /* ========== 元素标识 ========== */ // 只依赖稳定属性:data-href > data-url > id function getVisitedKeyFromElement(el) { if (!el || el.nodeType !== 1) return null; if (el.hasAttribute('data-href')) return 'dh:' + el.getAttribute('data-href'); if (el.hasAttribute('data-url')) return 'du:' + el.getAttribute('data-url'); if (el.id) return 'id:' + el.id; return null; } function isClickableElement(el) { if (!el || el.nodeType !== 1) return false; if (el.closest(SCRIPT_UI_SELECTOR)) return false; const tag = el.tagName.toUpperCase(); if (['A', 'BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'IMG', 'LABEL'].includes(tag)) return true; if (el.hasAttribute('onclick') || el.hasAttribute('download') || el.getAttribute('role') === 'button' || el.getAttribute('role') === 'link' || el.hasAttribute('data-href') || el.hasAttribute('data-url')) return true; return false; } function markElementVisited(el) { if (!el || !el.classList) return; if (!el.classList.contains('visited-link')) el.classList.add('visited-link'); } /* ========== 添加访问记录 ========== */ function addVisitedLink(url) { if (!url) return; const existing = visitedLinksMap.get(url); if (existing) { existing.time = Date.now(); visitedLinksMap.delete(url); visitedLinksMap.set(url, existing); } else { visitedLinksMap.set(url, { time: Date.now() }); } enforceLimits(); } function addCustomKey(key) { if (!key) return; const existing = customVisitedMap.get(key); if (existing) { existing.time = Date.now(); customVisitedMap.delete(key); customVisitedMap.set(key, existing); } else { customVisitedMap.set(key, { time: Date.now() }); } enforceLimits(); } /* ========== 实时同步同 key 的元素 ========== */ const pendingMatchScan = new Map(); let matchScanScheduled = false; function markMatchingElements(key, isUrl) { if (!key) return; const dedupKey = (isUrl ? 'u:' : 'k:') + key; if (pendingMatchScan.has(dedupKey)) return; if (pendingMatchScan.size >= MAX_PENDING_SCAN) { const first = pendingMatchScan.keys().next().value; if (first !== undefined) pendingMatchScan.delete(first); } pendingMatchScan.set(dedupKey, { key, isUrl }); scheduleMatchScan(); } function scheduleMatchScan() { if (matchScanScheduled) return; matchScanScheduled = true; const run = () => { matchScanScheduled = false; const items = Array.from(pendingMatchScan.values()); pendingMatchScan.clear(); for (const it of items) { try { doMatchScan(it.key, it.isUrl); } catch (_) {} } }; const useRaf = typeof requestAnimationFrame === 'function' && document.visibilityState === 'visible'; if (useRaf) requestAnimationFrame(run); else setTimeout(run, 0); } function doMatchScan(key, isUrl) { let selector, matchFn; if (isUrl) { selector = 'a[href]'; matchFn = (el) => el.tagName === 'A' && el.href === key; } else if (key.startsWith('dh:')) { const v = key.slice(3); selector = `[data-href="${escAttrValue(v)}"]`; matchFn = (el) => el.getAttribute('data-href') === v; } else if (key.startsWith('du:')) { const v = key.slice(3); selector = `[data-url="${escAttrValue(v)}"]`; matchFn = (el) => el.getAttribute('data-url') === v; } else if (key.startsWith('id:')) { const v = key.slice(3); selector = `[id="${escAttrValue(v)}"]`; matchFn = (el) => el.id === v; } else { return; } const scan = (root) => { let list; try { list = root.querySelectorAll(selector); } catch (_) { return; } for (let i = 0, n = list.length; i < n; i++) { const el = list[i]; if (el.classList.contains('visited-link')) continue; if (el.closest && el.closest(SCRIPT_UI_SELECTOR)) continue; if (matchFn(el)) el.classList.add('visited-link'); } }; scan(document); if (shadowStyleMap.size > 0) { shadowStyleMap.forEach((data, sroot) => { if (sroot.host && sroot.host.isConnected) scan(sroot); }); } } function markLinkAsVisited(link) { if (!link) return; if (link.tagName === 'A' && link.href) { addVisitedLink(link.href); markElementVisited(link); markMatchingElements(link.href, true); saveVisitedLinksDebounced(); return; } const key = getVisitedKeyFromElement(link); if (key) { addCustomKey(key); markElementVisited(link); markMatchingElements(key, false); saveVisitedLinksDebounced(); } // 无 key:不打类,避免刷新后样式闪一下消失 } /* ========== 拖拽处理 ========== */ function handleDragLink() { if (!dragTarget) return; const link = dragTarget.closest('a') || dragTarget.closest('[data-href], [data-url], [role="link"], [role="button"], [onclick], [download]'); if (link) handleInteraction(link); dragTarget = null; dragTime = 0; } /* ========== 样式生成与注入 ========== */ function getStyleText() { const underlineLine = settings.underline === 'none' ? 'none' : 'underline'; const underlineStyle = settings.underline === 'none' ? 'none' : settings.underline; return ` html body a.visited-link[href], html body button.visited-link, html body .visited-link { color: ${settings.color} !important; text-decoration-line: ${underlineLine} !important; text-decoration-style: ${underlineStyle} !important; text-decoration-color: currentColor !important; font-weight: ${settings.weight} !important; } `; } function updateDocumentStyle() { const css = getStyleText(); if (!styleElement || !styleElement.isConnected) { if (styleElement) styleElement.remove(); styleElement = document.createElement('style'); styleElement.id = 'visited-links-style'; (document.head || document.documentElement).appendChild(styleElement); } if (styleElement.textContent !== css) styleElement.textContent = css; } function injectShadowStyle(shadowRoot) { try { if (shadowStyleMap.has(shadowRoot)) return; const style = document.createElement('style'); style.setAttribute('data-visited-style', 'true'); style.textContent = getStyleText(); shadowRoot.appendChild(style); const shadowObserver = new MutationObserver((mutations) => { const nodes = []; for (const m of mutations) { if (m.type === 'childList') { m.addedNodes.forEach(n => nodes.push(n)); } } if (nodes.length) applyStylesToNewNodes(nodes); }); shadowObserver.observe(shadowRoot, { childList: true, subtree: true }); shadowStyleMap.set(shadowRoot, { style, observer: shadowObserver }); } catch (e) { console.warn('[已访问链接样式] 注入shadow样式失败', e); } } function cleanupShadowStyleMap() { const toDelete = []; for (const [root, data] of shadowStyleMap) { if ((root.host && !root.host.isConnected) || !data.style.parentNode) { if (data.observer) data.observer.disconnect(); toDelete.push(root); } } toDelete.forEach(root => shadowStyleMap.delete(root)); } function updateAllShadowStyles() { const css = getStyleText(); cleanupShadowStyleMap(); for (const [, data] of shadowStyleMap) { if (data.style.textContent !== css) data.style.textContent = css; } } function updateStylesDebounced() { clearTimeout(styleUpdateTimer); styleUpdateTimer = setTimeout(() => { updateDocumentStyle(); updateAllShadowStyles(); }, DEBOUNCE_STYLE); } /* ========== 样式应用 ========== */ function hasValidBody() { return !!(document.body && document.body.childNodes.length > 0); } function processElement(el) { if (!el || el.nodeType !== 1) return; if (el.tagName === 'A' && el.href) { if (visitedLinksMap.has(el.href)) markElementVisited(el); else if (el.classList.contains('visited-link')) el.classList.remove('visited-link'); return; } const key = getVisitedKeyFromElement(el); if (key && customVisitedMap.has(key)) markElementVisited(el); else if (el.classList.contains('visited-link')) el.classList.remove('visited-link'); } function applyVisitedStyles() { if (!hasValidBody()) return; if (visitedLinksMap.size === 0 && customVisitedMap.size === 0) return; const allElements = document.querySelectorAll(CANDIDATE_SELECTOR); if (allElements.length === 0) return; let idx = 0; const processNext = () => { const end = Math.min(idx + CHUNK_SIZE, allElements.length); for (; idx < end; idx++) processElement(allElements[idx]); if (idx < allElements.length) { requestIdleCallback(processNext, { timeout: 100 }); } }; requestIdleCallback(processNext, { timeout: 100 }); } function schedulePendingFlush() { if (pendingFlushScheduled) return; pendingFlushScheduled = true; requestIdleCallback(() => { pendingFlushScheduled = false; flushPendingNodes(); }, { timeout: 300 }); } function flushPendingNodes() { if (pendingNodes.length === 0) return; const batch = pendingNodes.splice(0, CHUNK_SIZE); for (let i = 0; i < batch.length; i++) { try { processElement(batch[i]); } catch (_) {} } if (pendingNodes.length) schedulePendingFlush(); } function applyStylesToNewNodes(nodes) { try { const collected = []; for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; if (node.nodeType !== 1 || processedNodeSet.has(node)) continue; processedNodeSet.add(node); if (node.matches && node.matches(CANDIDATE_SELECTOR)) { collected.push(node); } if (node.firstElementChild) { const inner = node.querySelectorAll(CANDIDATE_SELECTOR); for (let j = 0; j < inner.length; j++) { const el = inner[j]; if (processedNodeSet.has(el)) continue; processedNodeSet.add(el); collected.push(el); } } } if (collected.length) { for (let i = 0; i < collected.length; i++) pendingNodes.push(collected[i]); schedulePendingFlush(); } } catch (e) { console.warn('[已访问链接样式] 应用样式出错', e); } } /* ========== 交互事件处理 ========== */ function handleInteraction(target, evt) { if (!target) return; let realTarget = target; if (evt && typeof evt.composedPath === 'function') { const path = evt.composedPath(); if (path && path.length) { for (const n of path) { if (n && n.nodeType === 1) { realTarget = n; break; } } } } if (!realTarget || !realTarget.closest) return; if (realTarget.closest(SCRIPT_UI_SELECTOR)) return; const anchor = realTarget.closest('a[href]'); if (anchor) { markLinkAsVisited(anchor); return; } const hit = realTarget.closest(INTERACTIVE_SELECTOR); if (!hit) return; if (isClickableElement(hit)) markLinkAsVisited(hit); } /* ========== 颜色转换 ========== */ function hsvToHex(h, s, v) { const i = Math.floor(h / 60), f = h / 60 - i; const p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); let r, g, b; switch (i % 6) { case 0: r = v; g = t; b = p; break; case 1: r = q; g = v; b = t; break; case 2: r = p; g = v; b = t; break; case 3: r = p; g = q; b = v; break; case 4: r = t; g = p; b = v; break; case 5: r = v; g = p; b = q; break; } const toHex = x => Math.round(x * 255).toString(16).padStart(2, '0'); return `#${toHex(r)}${toHex(g)}${toHex(b)}`; } function hexToHsv(hex) { hex = hex.replace('#', ''); const r = parseInt(hex.substring(0,2),16)/255, g = parseInt(hex.substring(2,4),16)/255, b = parseInt(hex.substring(4,6),16)/255; const max = Math.max(r,g,b), min = Math.min(r,g,b), delta = max - min; let h = 0, s = 0, v = max; if (delta !== 0) { s = delta / max; if (r === max) h = ((g - b) / delta) % 6; else if (g === max) h = (b - r) / delta + 2; else h = (r - g) / delta + 4; h = Math.round(h * 60); if (h < 0) h += 360; } return { h, s, v }; } /* ========== 全局 UI 样式 ========== */ function injectGlobalDialogStyles() { const css = ` .vlx.glass-overlay { position: fixed; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.3); backdrop-filter: blur(var(--vlx-blur, 12px)); -webkit-backdrop-filter: blur(var(--vlx-blur, 12px)); display: flex; align-items: center; justify-content: center; z-index: 2147483647; opacity:0; visibility: hidden; pointer-events: none; transition: opacity 0.25s ease, visibility 0s linear 0.25s; } .vlx.glass-overlay.show { opacity:1; visibility: visible; pointer-events: auto; transition: opacity 0.25s ease, visibility 0s; } .vlx .glass-card { background: rgba(255,255,255, var(--vlx-alpha, 0.7)); border-radius: 20px; box-shadow: 0 25px 45px rgba(0,0,0,0.15); border: 1px solid rgba(255,255,255,0.5); backdrop-filter: blur(25px); -webkit-backdrop-filter: blur(25px); padding: 28px 26px; width: 440px; max-width: 94vw; max-height: 85vh; overflow-y: auto; position: relative; color: #2c2c2c; font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; transform: scale(0.92); transition: transform 0.25s ease; box-sizing: border-box; user-select: none; } .vlx.glass-overlay.show .glass-card { transform: scale(1); } .vlx .card-header { display: flex; align-items: center; gap: 10px; margin-bottom: 20px; padding-bottom: 14px; border-bottom: 1.5px solid rgba(255,255,255,0.8); cursor: move; } .vlx .card-header img { width: 32px; height: 32px; border-radius: 8px; object-fit: cover; pointer-events: none; } .vlx .card-header h2 { margin:0; font-size:20px; font-weight:600; color:#1a1a1a; pointer-events: none; } .vlx .close-btn { position: absolute; top:14px; right:18px; background: rgba(255,255,255,0.6); border: none; font-size:20px; cursor:pointer; color:#666; width:32px; height:32px; border-radius:50%; display:flex; align-items:center; justify-content:center; transition: background 0.2s, color 0.2s; z-index: 2; } .vlx .close-btn:hover { background: rgba(0,0,0,0.08); color:#222; } .vlx .close-btn:focus-visible { outline: 2px solid #4a90e2; outline-offset: 2px; } .vlx .setting-row { display: flex; align-items: center; gap:12px; margin-bottom:15px; flex-wrap: wrap; } .vlx .setting-row label { min-width:70px; font-size:14px; color:#444; font-weight:500; } .vlx .preview-btn { flex:1; display:flex; align-items:center; justify-content:center; gap:10px; padding:10px 16px; background:rgba(255,255,255,0.5); border: 1px solid rgba(255,255,255,0.7); border-radius:12px; cursor: pointer; font-size:14px; color:#333; transition: all 0.2s; } .vlx .preview-btn:hover { background:rgba(255,255,255,0.8); border-color:#4a90e2; } .vlx .preview-btn:focus-visible { outline: 2px solid #4a90e2; outline-offset: 2px; } .vlx .weight-row { display:flex; align-items:center; gap:10px; flex:1; } .vlx .weight-row input[type="range"] { flex:1; height:8px; -webkit-appearance: none; background: rgba(255,255,255,0.45); border-radius:8px; outline:none; border:1px solid rgba(255,255,255,0.6); backdrop-filter: blur(4px); transition: background 0.2s; } .vlx .weight-row input[type="range"]:hover { background:rgba(255,255,255,0.65); } .vlx .weight-row input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width:26px; height:26px; background: white; border-radius:50%; box-shadow: 0 2px 10px rgba(0,0,0,0.15), 0 0 0 2px rgba(255,255,255,0.5); cursor: pointer; transition: transform 0.15s; } .vlx .weight-row input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.1); box-shadow: 0 4px 14px rgba(0,0,0,0.2), 0 0 0 3px rgba(74,144,226,0.3); } .vlx .weight-value { min-width:45px; text-align:center; font-weight:700; font-size:16px; } .vlx .danger-btn { background: rgba(255,255,255,0.55); border: 1px solid rgba(229,62,62,0.6); color:#d63031; padding:12px 18px; border-radius:14px; font-size:15px; font-weight:500; cursor:pointer; width:100%; transition: all 0.2s; margin-top:10px; } .vlx .danger-btn:hover { background:#d63031; color:white; border-color:#d63031; } .vlx .danger-btn:focus-visible { outline: 2px solid #d63031; outline-offset: 2px; } .vlx .danger-btn.page-clear-btn { border-color: rgba(243,156,18,0.6); color:#e67e22; } .vlx .danger-btn.page-clear-btn:hover { background:#e67e22; color:white; border-color:#e67e22; } .vlx .danger-btn.page-clear-btn:focus-visible { outline: 2px solid #e67e22; outline-offset: 2px; } .vlx .danger-row { display: flex; gap: 10px; margin-top: 10px; } .vlx .danger-row .danger-btn { flex: 1 1 0; width: auto; margin-top: 0; padding: 12px 10px; } .vlx .confirm-buttons { display:flex; gap:14px; justify-content:flex-end; margin-top:24px; } .vlx .confirm-buttons button { padding:12px 24px; border-radius:12px; font-size:15px; font-weight:500; cursor:pointer; border:1px solid rgba(255,255,255,0.5); background:rgba(255,255,255,0.45); color:#333; transition: all 0.2s; } .vlx .confirm-buttons button:hover { background:rgba(255,255,255,0.8); } .vlx .confirm-buttons button.confirm-yes { background:#d63031; color:white; border-color:#d63031; } .vlx .confirm-buttons button.confirm-yes:hover { background:#c0262c; } .vlx .picker-area { display: flex; flex-direction: column; gap: 10px; margin: 12px 0 20px; } .vlx .picker-row { display: flex; gap: 12px; align-items: stretch; } .vlx .sv-panel { position: relative; flex: 1; min-height: 160px; border-radius: 12px; overflow: hidden; cursor: crosshair; border: 1px solid rgba(255,255,255,0.7); box-shadow: 0 4px 10px rgba(0,0,0,0.05); } .vlx .sv-panel .sv-bg { position: absolute; top:0; left:0; width:100%; height:100%; background: linear-gradient(to top, #000, transparent), linear-gradient(to right, #fff, transparent); } .vlx .sv-panel .sv-cursor { position: absolute; width:16px; height:16px; border:2px solid white; border-radius:50%; box-shadow: 0 0 0 1px rgba(0,0,0,0.3); pointer-events: none; transform: translate(-50%, -50%); } .vlx .hue-slider { position: relative; width:100%; height:18px; border-radius:9px; background: linear-gradient(to right, red, yellow, lime, cyan, blue, magenta, red); cursor: pointer; border: 1px solid rgba(255,255,255,0.7); box-shadow: 0 4px 10px rgba(0,0,0,0.05); } .vlx .hue-slider .hue-thumb { position: absolute; top:-5px; width:28px; height:28px; background: white; border-radius:50%; box-shadow: 0 2px 8px rgba(0,0,0,0.3); pointer-events: none; transform: translateX(-50%); transition: left 0.05s linear; } .vlx .color-preview-block { display: flex; flex-direction: column; align-items: center; gap: 8px; width: 80px; } .vlx .color-preview-block .preview-box { width:100%; height:36px; border-radius:9px; border:1px solid rgba(255,255,255,0.8); box-shadow: 0 2px 6px rgba(0,0,0,0.08); } .vlx .color-preview-block input[type="text"] { width:100%; padding:6px 4px; background:rgba(255,255,255,0.45); border:1px solid rgba(255,255,255,0.7); border-radius:8px; font-size:12px; text-align:center; outline:none; box-sizing: border-box; } .vlx .underline-options { display: flex; flex-wrap: wrap; gap: 8px; margin: 15px 0; } .vlx .underline-option { flex:1 1 auto; display:flex; align-items:center; justify-content:center; padding:10px 14px; background:rgba(255,255,255,0.5); border:1px solid rgba(255,255,255,0.7); border-radius:10px; cursor:pointer; font-size:14px; transition: all 0.15s; } .vlx .underline-option:hover { background:rgba(255,255,255,0.8); border-color:#4a90e2; } .vlx .underline-option.active { background:#4a90e2; color:white; border-color:#4a90e2; font-weight:600; } .vlx .underline-option:focus-visible { outline: 2px solid #4a90e2; outline-offset: 2px; } .vlx .switch { position: relative; display: inline-block; width: 48px; height: 26px; flex-shrink: 0; } .vlx .switch input { opacity: 0; width: 0; height: 0; } .vlx .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .3s; border-radius: 26px; border: 1px solid rgba(255,255,255,0.7); } .vlx .slider:before { position: absolute; content: ""; height: 20px; width: 20px; left: 2px; bottom: 2px; background-color: white; transition: .3s; border-radius: 50%; box-shadow: 0 1px 4px rgba(0,0,0,0.2); } .vlx input:checked + .slider:before { left: auto; right: 2px; transform: none; } .vlx input:checked + .slider { background-color: #4a90e2; } .vlx .stats-row { display: flex; justify-content: space-between; background: rgba(255,255,255,0.5); border-radius:10px; padding:8px 12px; margin-bottom:14px; font-size:13px; } .vlx .clean-info { font-size:12px; color:#555; margin-top:4px; } .vlx .preview-link-box { padding: 10px 14px; background: rgba(255,255,255,0.45); border: 1px solid rgba(255,255,255,0.7); border-radius: 12px; flex: 1; text-align: center; font-size: 15px; } @media (max-width: 480px) { .vlx .glass-card { padding: 18px 14px; width: 96vw; max-height: 90vh; } .vlx .card-header h2 { font-size: 18px; } .vlx .setting-row { flex-direction: column; align-items: stretch; width: 100%; } .vlx .setting-row label { min-width: auto; margin-bottom: 4px; } .vlx .preview-btn { width: 100%; flex: none; box-sizing: border-box; } .vlx .weight-row { width: 100%; } .vlx .picker-area { margin: 8px 0 10px; gap: 8px; } .vlx .picker-row { flex-direction: column; align-items: stretch; gap: 10px; } .vlx .sv-panel { width: 100%; min-height: 150px; max-height: 220px; } .vlx .color-preview-block { flex-direction: row; width: 100%; gap: 10px; } .vlx .color-preview-block .preview-box { width: 56px; height: 36px; } .vlx .color-preview-block input[type="text"] { flex: 1; } .vlx .hue-slider { margin-top: 4px; } .vlx .underline-options { flex-direction: column; width: 100%; } .vlx .underline-option { flex: 1 0 auto; width: 100%; box-sizing: border-box; } .vlx .danger-row .danger-btn { font-size: 14px; padding: 11px 6px; } } `; let style = document.getElementById('glass-dialog-styles'); if (!style) { style = document.createElement('style'); style.id = 'glass-dialog-styles'; (document.head || document.documentElement).appendChild(style); } style.textContent = css; } /* ========== 面板拖拽 ========== */ function makeDraggable(card, headerSelector = '.card-header') { const header = card.querySelector(headerSelector); if (!header) return; let startX, startY, startLeft, startTop, dragging = false; const onStart = (e) => { if (e.target.closest('button, input, select, textarea, .close-btn')) return; e.preventDefault(); const rect = card.getBoundingClientRect(); startLeft = rect.left; startTop = rect.top; startX = e.touches ? e.touches[0].clientX : e.clientX; startY = e.touches ? e.touches[0].clientY : e.clientY; card.style.position = 'fixed'; card.style.left = startLeft + 'px'; card.style.top = startTop + 'px'; card.style.margin = '0'; card.style.transform = 'none'; dragging = false; }; const onMove = (e) => { if (startX === undefined) return; const clientX = e.touches ? e.touches[0].clientX : e.clientX; const clientY = e.touches ? e.touches[0].clientY : e.clientY; const dx = clientX - startX, dy = clientY - startY; if (!dragging && Math.abs(dx) < 5 && Math.abs(dy) < 5) return; dragging = true; e.preventDefault(); card.style.left = (startLeft + dx) + 'px'; card.style.top = (startTop + dy) + 'px'; }; const onEnd = () => { startX = undefined; if (!dragging) { card.style.position = ''; card.style.left = ''; card.style.top = ''; card.style.margin = ''; card.style.transform = ''; } document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onEnd); document.removeEventListener('touchmove', onMove); document.removeEventListener('touchend', onEnd); }; header.addEventListener('mousedown', (e) => { onStart(e); document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onEnd); }); header.addEventListener('touchstart', (e) => { onStart(e); document.addEventListener('touchmove', onMove, { passive: false }); document.addEventListener('touchend', onEnd); }, { passive: false }); } /* ========== 工具 ========== */ function escapeHtml(text) { const div = document.createElement('div'); div.textContent = String(text); return div.innerHTML; } /* ========== 面板统计 ========== */ function updateStatsInPanel() { if (!panelOverlay || !panelOverlay.isConnected) return; if (!panelOverlay._refs) { panelOverlay._refs = { recorded: panelOverlay.querySelector('#recorded-count'), pageLink: panelOverlay.querySelector('#page-link-count'), cleanInfo: panelOverlay.querySelector('#clean-info') }; } const refs = panelOverlay._refs; if (refs.recorded) refs.recorded.textContent = visitedLinksMap.size + customVisitedMap.size; if (refs.pageLink) { if (panelOverlay._pageLinkCount === undefined) { try { panelOverlay._pageLinkCount = document.querySelectorAll('a[href]').length; } catch (_) { panelOverlay._pageLinkCount = 0; } } refs.pageLink.textContent = panelOverlay._pageLinkCount; } if (refs.cleanInfo) { if (lastCleanInfo.time) { const date = new Date(lastCleanInfo.time); refs.cleanInfo.textContent = `上次清理:${date.toLocaleString()},移除 ${lastCleanInfo.count} 条`; } else { refs.cleanInfo.textContent = '尚未执行清理'; } } } function updateStatsInPanelDebounced() { clearTimeout(statsUpdateTimer); statsUpdateTimer = setTimeout(updateStatsInPanel, 200); } /* ========== 对话框工具 ========== */ function bindDialogOverlay(overlay) { const close = () => { overlay.classList.remove('show'); setTimeout(() => overlay.remove(), 300); }; overlay.querySelector('.close-btn')?.addEventListener('click', close); overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); }); return close; } function showDayPicker(currentDays, onSelect) { const overlay = document.createElement('div'); overlay.className = 'glass-overlay vlx show'; const options = [1, 7, 30, 90]; overlay.innerHTML = `
icon

保留记录天数

${options.map(opt => `
${dayLabels[opt]}
`).join('')}
`; (document.body || document.documentElement).appendChild(overlay); makeDraggable(overlay.querySelector('.glass-card')); const close = bindDialogOverlay(overlay); overlay.querySelectorAll('.underline-option').forEach(el => { el.addEventListener('click', () => { onSelect(parseInt(el.dataset.value, 10)); close(); }); el.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); el.click(); } }); }); } function showUnderlinePicker(current, onSelect) { const overlay = document.createElement('div'); overlay.className = 'glass-overlay vlx show'; const options = ['solid', 'dashed', 'wavy', 'double', 'dotted', 'none']; overlay.innerHTML = `
icon

选择下划线

${options.map(opt => `
${underlineLabels[opt]} (${opt})
`).join('')}
`; (document.body || document.documentElement).appendChild(overlay); makeDraggable(overlay.querySelector('.glass-card')); const close = bindDialogOverlay(overlay); overlay.querySelectorAll('.underline-option').forEach(el => { el.addEventListener('click', () => { onSelect(el.dataset.value); close(); }); el.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); el.click(); } }); }); } function showConfirmDialog(message, onConfirm) { const overlay = document.createElement('div'); overlay.className = 'glass-overlay vlx show'; overlay.innerHTML = `
icon

确认操作

${escapeHtml(message)}

`; (document.body || document.documentElement).appendChild(overlay); makeDraggable(overlay.querySelector('.glass-card')); const close = bindDialogOverlay(overlay); overlay.querySelector('.cancel-btn')?.addEventListener('click', close); overlay.querySelector('.confirm-yes')?.addEventListener('click', () => { try { if (onConfirm) onConfirm(); } finally { close(); } }); } /* ========== 主面板 ========== */ function createSettingsPanel() { const overlay = document.createElement('div'); overlay.className = 'glass-overlay vlx'; const initHsv = hexToHsv(settings.color); let currentH = initHsv.h, currentS = initHsv.s, currentV = initHsv.v; overlay.innerHTML = `
Logo

已访问链接样式

📋 已记录:${visitedLinksMap.size + customVisitedMap.size} / ${MAX_LINKS} 🔗 页面链接:${document.querySelectorAll('a[href]').length}
${underlineLabels[settings.underline]} (${settings.underline})
${settings.weight}
${settings.glassAlpha}
`; (document.body || document.documentElement).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} 条访问记录吗?此操作不可恢复,且不影响自动清理设置。`, () => { 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, 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 newHsv = hexToHsv(hex); currentH = newHsv.h; currentS = newHsv.s; currentV = newHsv.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)); 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(); }); 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 / id 属性变化 ========== */ 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' || m.attributeName === 'id') { 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 }); } /* ========== 全局 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._refs = null; 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(); }); })();