// ==UserScript== // @name 已访问链接样式修改 - 控制面板 // @namespace https://scriptcat.org/zh-CN/search // @version 2026.5.11 // @description 自定义已访问链接样式 // @tag 链接 工具 link tools // @license MIT // @author zzz妄炁 & AI // @match *://*/* // @run-at document-idle // @grant GM_addStyle // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @compatible chrome // @compatible firefox // @supportURL https://scriptcat.org/zh-CN/script-show-page/2822/issue // @icon https://img.soogif.com/6QlDQPIFsYakXFhmBt1a02vNk9G5ecYf.gif // ==/UserScript== (function() { 'use strict'; const MAX_LINKS = 2000; const underlineLabels = { 'solid': '实线', 'dashed': '虚线', 'wavy': '波浪线', 'double': '双线', 'dotted': '点状线', 'none': '无' }; const dayLabels = { 0: '立即清除', 1: '保留 1 天', 7: '保留 7 天', 30: '保留 30 天' }; 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 visitedLinksArray = []; let visitedLinksSet = new Set(); let customVisitedKeys = []; let dragTarget = null; let dragTime = 0; let contextTarget = null; // 右键点击的元素 let contextTime = 0; // 右键时间戳 let contextTimer = null; // 右键延迟处理定时器 let observer = null; let styleElement = null; const shadowStyleMap = new Map(); // shadowRoot -> {style, observer} let observerTimer = null; let pendingNodes = []; let panelOverlay = null; let touchStartX = 0, touchStartY = 0; let touchHandledTarget = null; let touchHandledTimer = null; let routeRetryTimer = null; let saveWeightTimer = null; let saveAlphaTimer = null; let saveColorTimer = null; const processedNodeSet = new WeakSet(); /* -------- 数据管理 -------- */ function initVisitedLinks() { try { let raw = GM_getValue('visitedLinks', '[]'); if (typeof raw === 'string') raw = JSON.parse(raw); if (!Array.isArray(raw)) raw = []; const needsUpgrade = raw.length > 0 && typeof raw[0] === 'string'; visitedLinksArray = raw.map(item => { if (typeof item === 'string') return { url: item, time: Date.now() }; return item; }); if (needsUpgrade) GM_setValue('visitedLinks', JSON.stringify(visitedLinksArray)); visitedLinksSet = new Set(visitedLinksArray.map(item => item.url)); let customRaw = GM_getValue('customVisitedKeys', '[]'); if (typeof customRaw === 'string') customRaw = JSON.parse(customRaw); if (!Array.isArray(customRaw)) customRaw = []; const customNeedsUpgrade = customRaw.length > 0 && typeof customRaw[0] === 'string'; customVisitedKeys = customRaw.map(item => { if (typeof item === 'string') return { key: item, time: Date.now() }; return item; }); if (customNeedsUpgrade) GM_setValue('customVisitedKeys', JSON.stringify(customVisitedKeys)); } catch (_) { visitedLinksArray = []; visitedLinksSet = new Set(); customVisitedKeys = []; } } function forceSave() { try { GM_setValue('visitedLinks', JSON.stringify(visitedLinksArray)); GM_setValue('customVisitedKeys', JSON.stringify(customVisitedKeys)); } catch (_) {} } function saveVisitedLinksDebounced() { clearTimeout(observerTimer); observerTimer = setTimeout(forceSave, 250); } /* -------- 清除所有 shadow DOM 中的 visited-link class -------- */ function removeVisitedClassFromAll() { const roots = []; const collect = (node) => { if (node.nodeType !== 1) return; if (node.shadowRoot && !roots.includes(node.shadowRoot)) roots.push(node.shadowRoot); for (const child of node.children) collect(child); }; collect(document.documentElement); roots.forEach(root => { root.querySelectorAll('.visited-link').forEach(el => el.classList.remove('visited-link')); }); document.querySelectorAll('.visited-link').forEach(el => el.classList.remove('visited-link')); } function applyAutoClean() { const isEnabled = GM_getValue('autoCleanEnabled', false); const days = GM_getValue('autoCleanDays', 7); if (!isEnabled) return; if (days === 0) { visitedLinksArray = []; visitedLinksSet.clear(); customVisitedKeys = []; forceSave(); removeVisitedClassFromAll(); updateStatsInPanel(); return; } const now = Date.now(); const threshold = now - days * 86400000; const newArray = visitedLinksArray.filter(item => item.time >= threshold); const newCustom = customVisitedKeys.filter(item => item.time >= threshold); if (newArray.length !== visitedLinksArray.length || newCustom.length !== customVisitedKeys.length) { visitedLinksArray = newArray; customVisitedKeys = newCustom; visitedLinksSet.clear(); visitedLinksArray.forEach(item => visitedLinksSet.add(item.url)); // 清除主 DOM 中过期的 class document.querySelectorAll('.visited-link').forEach(el => { if (el.tagName === 'A' && el.href) { if (!visitedLinksSet.has(el.href)) el.classList.remove('visited-link'); } else { const key = getPersistentElementKey(el); if (key && !customVisitedKeys.some(c => c.key === key)) el.classList.remove('visited-link'); } }); // 清除所有 Shadow DOM 中的过期 class const roots = []; const collect = (node) => { if (node.nodeType !== 1) return; if (node.shadowRoot && !roots.includes(node.shadowRoot)) roots.push(node.shadowRoot); for (const child of node.children) collect(child); }; collect(document.documentElement); roots.forEach(root => { root.querySelectorAll('.visited-link').forEach(el => { if (el.tagName === 'A' && el.href) { if (!visitedLinksSet.has(el.href)) el.classList.remove('visited-link'); } else { const key = getPersistentElementKey(el); if (key && !customVisitedKeys.some(c => c.key === key)) el.classList.remove('visited-link'); } }); }); } updateStatsInPanel(); } /* -------- 辅助:生成元素持久化标识键(用于非 href 可点击元素) -------- */ function getPersistentElementKey(el) { if (!el || el.nodeType !== 1) return null; // 优先使用 data-href / data-url 键(兼容原有逻辑) 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; // 生成 XPath 索引作为后备 try { let parts = []; let current = el; while (current && current.nodeType === Node.ELEMENT_NODE) { let index = 1; let sibling = current.previousSibling; while (sibling) { if (sibling.nodeType === Node.ELEMENT_NODE && sibling.nodeName === current.nodeName) index++; sibling = sibling.previousSibling; } parts.unshift(current.nodeName.toLowerCase() + '[' + index + ']'); current = current.parentNode; } return 'xpath:' + '/' + parts.join('/'); } catch (e) { return null; } } function isClickableElement(el) { if (!el || el.nodeType !== 1) return false; if (el.closest('.glass-overlay, .glass-card, #visited-links-style, #glass-dialog-styles')) 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 (visitedLinksSet.has(url)) return; while (visitedLinksArray.length >= MAX_LINKS) { const removed = visitedLinksArray.shift(); if (removed && removed.url) visitedLinksSet.delete(removed.url); } visitedLinksArray.push({ url: url, time: Date.now() }); visitedLinksSet.add(url); // 点击后立即标记页面上所有相同 URL 的链接 try { document.querySelectorAll('a[href]').forEach(link => { if (link.href === url) markElementVisited(link); }); } catch (_) {} } function addCustomKey(key) { if (customVisitedKeys.some(c => c.key === key)) return; while (customVisitedKeys.length >= MAX_LINKS) { customVisitedKeys.shift(); } customVisitedKeys.push({ key, time: Date.now() }); // 点击后立即标记页面上所有相同键的元素 try { document.querySelectorAll('[data-href], [data-url]').forEach(el => { if (getPersistentElementKey(el) === key) markElementVisited(el); }); // 也尝试通过 XPath/id 等键标记 if (key.startsWith('id:') || key.startsWith('xpath:')) { const all = document.querySelectorAll('button, img, [role="button"], [role="link"], [onclick], [download]'); all.forEach(el => { if (getPersistentElementKey(el) === key) markElementVisited(el); }); } } catch (_) {} } function markLinkAsVisited(link) { if (!link) return; if (link.tagName === 'A' && link.href) { const href = link.href; if (!visitedLinksSet.has(href)) { addVisitedLink(href); markElementVisited(link); saveVisitedLinksDebounced(); } return; } const key = getPersistentElementKey(link); if (key) { if (!customVisitedKeys.some(c => c.key === key)) { addCustomKey(key); markElementVisited(link); saveVisitedLinksDebounced(); } return; } // 兜底:无法生成键,仅临时标记 if (isClickableElement(link)) markElementVisited(link); } /* -------- 拖拽/右键处理 -------- */ 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 handleContextOpen() { if (!contextTarget) return; handleInteraction(contextTarget); contextTarget = null; contextTime = 0; if (contextTimer) { clearTimeout(contextTimer); contextTimer = null; } } /* -------- 样式生成与注入 -------- */ 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: inherit !important; font-weight: ${settings.weight} !important; } `; } function updateDocumentStyle() { const css = getStyleText(); if (!styleElement) { styleElement = document.createElement('style'); styleElement.id = 'visited-links-style'; (document.head || document.documentElement).appendChild(styleElement); } styleElement.textContent = css; } function injectShadowStyle(shadowRoot) { if (shadowStyleMap.has(shadowRoot)) return; const style = document.createElement('style'); style.setAttribute('data-visited-style', 'true'); style.textContent = getStyleText(); shadowRoot.appendChild(style); // 为此 Shadow DOM 添加局部 MutationObserver,处理后续动态节点 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 }); } function cleanupShadowStyleMap() { for (const [root, data] of shadowStyleMap) { if (data.style.parentNode !== root) { if (data.observer) data.observer.disconnect(); shadowStyleMap.delete(root); } } } function updateAllShadowStyles() { const css = getStyleText(); for (const [root, data] of shadowStyleMap) { if (data.style.parentNode !== root) { if (data.observer) data.observer.disconnect(); shadowStyleMap.delete(root); } else { data.style.textContent = css; } } } function processShadowRoots(node) { if (node.nodeType !== 1) return; if (node.shadowRoot) { injectShadowStyle(node.shadowRoot); node.shadowRoot.querySelectorAll('*').forEach(processShadowRoots); } if (node.children) { for (const child of node.children) processShadowRoots(child); } } function injectAllShadowStyles() { document.querySelectorAll('*').forEach(processShadowRoots); } function updateStyles() { updateDocumentStyle(); updateAllShadowStyles(); } /* -------- 链接样式应用 -------- */ function hasValidBody() { return !!(document.body && document.body.childNodes.length > 0); } function applyVisitedStyles() { if (!hasValidBody()) return; document.querySelectorAll('a[href]').forEach(link => { if (visitedLinksSet.has(link.href)) markElementVisited(link); }); document.querySelectorAll('[data-href], [data-url], button, img, [role="button"], [role="link"], [onclick], [download]').forEach(el => { if (el.tagName === 'A' && el.href) return; // 已处理 const key = getPersistentElementKey(el); if (key && customVisitedKeys.some(c => c.key === key)) markElementVisited(el); }); } function applyStylesToNewNodes(nodes) { for (const node of nodes) { if (node.nodeType !== 1) continue; if (processedNodeSet.has(node)) continue; processedNodeSet.add(node); if (node.tagName === 'A' && node.href && visitedLinksSet.has(node.href)) { markElementVisited(node); } else if (isClickableElement(node)) { const key = getPersistentElementKey(node); if (key && customVisitedKeys.some(c => c.key === key)) markElementVisited(node); } if (node.querySelectorAll) { node.querySelectorAll('a[href]').forEach(link => { if (visitedLinksSet.has(link.href)) markElementVisited(link); }); node.querySelectorAll('[data-href], [data-url], button, img, [role="button"], [role="link"], [onclick], [download]').forEach(el => { if (el.tagName === 'A' || !isClickableElement(el)) return; const key = getPersistentElementKey(el); if (key && customVisitedKeys.some(c => c.key === key)) markElementVisited(el); }); } processShadowRoots(node); } } function flushPendingNodes() { if (pendingNodes.length === 0) return; const nodes = pendingNodes.splice(0); applyStylesToNewNodes(nodes); injectAllShadowStyles(); } function handleInteraction(target) { if (!target || !target.closest) return; if (target.closest('#glass-dialog-styles, .glass-overlay, .glass-card, #visited-links-style')) return; const link = target.closest('a'); if (link && link.href) { markLinkAsVisited(link); return; } const clickable = target.closest('a, button, img, input, label, [role="button"], [role="link"], [onclick], [download], [data-href], [data-url]'); if (clickable && isClickableElement(clickable)) markLinkAsVisited(clickable); } /* -------- 颜色转换 -------- */ 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 = ` .glass-overlay { position: fixed; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.3); backdrop-filter: blur(var(--blur, 12px)); -webkit-backdrop-filter: blur(var(--blur, 12px)); display: flex; align-items: center; justify-content: center; z-index: 2147483647; opacity:0; pointer-events: none; transition: opacity 0.25s ease; } .glass-overlay.show { opacity:1; pointer-events: auto; } .glass-card { background: rgba(255,255,255, var(--glass-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; } .glass-overlay.show .glass-card { transform: scale(1); } .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; } .card-header img { width: 32px; height: 32px; border-radius: 8px; object-fit: cover; pointer-events: none; } .card-header h2 { margin:0; font-size:20px; font-weight:600; color:#1a1a1a; pointer-events: none; } .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; } .close-btn:hover { background: rgba(0,0,0,0.08); color:#222; } .setting-row { display: flex; align-items: center; gap:12px; margin-bottom:15px; flex-wrap: wrap; } .setting-row label { min-width:70px; font-size:14px; color:#444; font-weight:500; } .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; } .preview-btn:hover { background:rgba(255,255,255,0.8); border-color:#4a90e2; } .weight-row { display:flex; align-items:center; gap:10px; flex:1; } .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; } .weight-row input[type="range"]:hover { background:rgba(255,255,255,0.65); } .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; } .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); } .weight-value { min-width:45px; text-align:center; font-weight:700; font-size:16px; } .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; } .danger-btn:hover { background:#d63031; color:white; border-color:#d63031; } .confirm-buttons { display:flex; gap:14px; justify-content:flex-end; margin-top:24px; } .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; } .confirm-buttons button:hover { background:rgba(255,255,255,0.8); } .confirm-buttons button.confirm-yes { background:#d63031; color:white; border-color:#d63031; } .confirm-buttons button.confirm-yes:hover { background:#c0262c; } .picker-area { display: flex; flex-direction: column; gap: 10px; margin: 12px 0 20px; } .picker-row { display: flex; gap: 12px; align-items: stretch; } .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); } .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); } .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%); } .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); } .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; } .color-preview-block { display: flex; flex-direction: column; align-items: center; gap: 8px; width: 80px; } .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); } .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; } .underline-options { display: flex; flex-wrap: wrap; gap: 8px; margin: 15px 0; } .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; } .underline-option:hover { background:rgba(255,255,255,0.8); border-color:#4a90e2; } .underline-option.active { background:#4a90e2; color:white; border-color:#4a90e2; font-weight:600; } .switch { position: relative; display: inline-block; width: 48px; height: 26px; flex-shrink: 0; } .switch input { opacity: 0; width: 0; height: 0; } .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); } .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); } input:checked + .slider:before { left: auto; right: 2px; transform: none; } input:checked + .slider { background-color: #4a90e2; } .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; } @media (max-width: 480px) { .glass-card { padding: 18px 14px; width: 96vw; max-height: 90vh; } .card-header h2 { font-size: 18px; } .setting-row { flex-direction: column; align-items: stretch; width: 100%; } .setting-row label { min-width: auto; margin-bottom: 4px; } .preview-btn { width: 100%; flex: none; box-sizing: border-box; } .weight-row { width: 100%; } .picker-area { margin: 8px 0 10px; gap: 8px; } .picker-row { flex-direction: column; align-items: stretch; gap: 10px; } .sv-panel { width: 100%; min-height: 200px; max-height: 260px; } .color-preview-block { flex-direction: row; width: 100%; gap: 10px; } .color-preview-block .preview-box { width: 56px; height: 36px; } .color-preview-block input[type="text"] { flex: 1; } .hue-slider { margin-top: 4px; } .underline-options { flex-direction: column; width: 100%; } .underline-option { flex: 1 0 auto; width: 100%; box-sizing: border-box; justify-content: center; } } `; const style = document.createElement('style'); style.id = 'glass-dialog-styles'; style.textContent = css; (document.head || document.documentElement).appendChild(style); } /* -------- 面板拖拽功能 -------- */ 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) 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; const 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 = null; 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 updateStatsInPanel() { if (!panelOverlay) return; const recordedEl = document.getElementById('recorded-count'); const pageLinkEl = document.getElementById('page-link-count'); if (recordedEl) recordedEl.textContent = visitedLinksArray.length + customVisitedKeys.length; if (pageLinkEl) { try { let count = document.querySelectorAll('a[href]').length; const roots = []; const collect = (node) => { if (node.nodeType === 1) { if (node.shadowRoot) roots.push(node.shadowRoot); for (const c of node.children) collect(c); } }; collect(document.documentElement); roots.forEach(r => count += r.querySelectorAll('a[href]').length); pageLinkEl.textContent = count; } catch (_) {} } } function showDayPicker(currentDays, onSelect) { const overlay = document.createElement('div'); overlay.className = 'glass-overlay show'; const options = [0, 1, 7, 30]; overlay.innerHTML = `
${escapeHtml(message)}