// ==UserScript== // @name 已访问链接样式修改 - 控制面板 // @version 2026.10.30 // @description 自定义已访问链接样式 // @tag 链接标记 自定义 颜色 下划线 字重 UI 美化外观 工具 // @author zzz妄炁 & AI // @match *://*/* // @run-at document-start // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @grant GM_addValueChangeListener // @compatible chrome // @compatible firefox // @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 = (cb, opt) => setTimeout(() => { const s = Date.now(); cb({ didTimeout: false, timeRemaining: () => Math.max(0, 50 - (Date.now() - s)) }); }, opt?.timeout || 1); window.cancelIdleCallback = (id) => clearTimeout(id); } if (typeof CSS === 'undefined' || !CSS.escape) { window.CSS = window.CSS || {}; CSS.escape = (str) => String(str).replace(/[^a-zA-Z0-9_-]/g, (c) => '\\' + c); } const microtask = typeof queueMicrotask === 'function' ? queueMicrotask : (fn) => Promise.resolve().then(fn); const escAttrValue = (s) => String(s) .replace(/\\/g, '\\\\').replace(/"/g, '\\"') .replace(/\n/g, '\\a ').replace(/\r/g, '\\d ').replace(/\f/g, '\\c ') .replace(/\t/g, '\\9 '); // ---- constants ---- const MAX_TOTAL_RECORDS = 5000; const CLEANUP_INTERVAL = 30000; const DEBOUNCE_SAVE = 300; const DEBOUNCE_STYLE = 150; const CHUNK_SIZE = 200; const MAX_PENDING_SCAN = 200; const MAX_IMPORT_SIZE = 10 * 1024 * 1024; 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 天' }; const formatDayLabel = (days) => { const n = parseInt(days, 10); return isNaN(n) ? '7 天' : (dayLabels[n] || `${n} 天`); }; const isTopFrame = (() => { try { return window.self === window.top; } catch (_) { return false; } })(); // ---- 安全读写 GM ---- function gmGet(key, def) { try { const v = GM_getValue(key, def); return v === undefined ? def : v; } catch (_) { return def; } } function gmSet(key, val) { try { GM_setValue(key, val); } catch (_) {} } // ---- settings ---- const settings = { color: gmGet('linkColor', '#FF0000'), underline: gmGet('underlineType', 'solid'), weight: gmGet('linkWeight', '400'), glassAlpha: gmGet('glassAlpha', 0.7), blurEnabled: gmGet('blurEnabled', true) }; let autoCleanEnabled = gmGet('autoCleanEnabled', false); let autoCleanDays = gmGet('autoCleanDays', 7); let highlightEnabled = gmGet('highlightEnabled', true); let lastCleanInfo = { time: Number(gmGet('lastCleanTime', 0)) || 0, count: Number(gmGet('lastCleanCount', 0)) || 0 }; let cleanInfoDirty = false; let pendingTokenBump = false; let lastTimeCounter = 0; (function validate() { try { if (!/^#[0-9a-fA-F]{6}$/.test(settings.color)) { settings.color = '#FF0000'; gmSet('linkColor', settings.color); } if (!underlineLabels[settings.underline]) { settings.underline = 'solid'; gmSet('underlineType', settings.underline); } const w = parseInt(settings.weight, 10); if (!(w >= 100 && w <= 900)) { settings.weight = '400'; gmSet('linkWeight', '400'); } else settings.weight = String(w); const a = parseFloat(settings.glassAlpha); if (isNaN(a) || a < 0.1 || a > 0.9) { settings.glassAlpha = 0.7; gmSet('glassAlpha', 0.7); } else settings.glassAlpha = a; if (typeof settings.blurEnabled !== 'boolean') { settings.blurEnabled = true; gmSet('blurEnabled', true); } if (typeof highlightEnabled !== 'boolean') { highlightEnabled = true; gmSet('highlightEnabled', true); } if (typeof autoCleanEnabled !== 'boolean') { autoCleanEnabled = false; gmSet('autoCleanEnabled', false); } const d = parseInt(autoCleanDays, 10); if (isNaN(d) || d < 1 || d > 3650) { autoCleanDays = 7; gmSet('autoCleanDays', 7); } else autoCleanDays = d; } catch (_) {} })(); // ---- runtime state ---- let visitedLinksMap = new Map(); let customVisitedMap = new Map(); let lastSeenCleanToken = Number(gmGet('lastCleanToken', 0)) || 0; let lastVisitedRaw = null, lastCustomRaw = null; let dragTarget = null, dragTime = 0; let observer = null, hrefObserver = null, styleElement = null; const shadowStyleMap = new Map(); let saveTimer = null, styleUpdateTimer = null, panelOverlay = null; let touchStartX = 0, touchStartY = 0; let touchHandledTarget = null, touchHandledTime = 0, touchHandledTimer = null; let routeRetryTimer = null, saveWeightTimer = null, saveAlphaTimer = null, saveColorTimer = null, statsUpdateTimer = null; const processedNodeSet = new WeakSet(); let pendingNodes = [], pendingFlushScheduled = false, lastRemovalDeepScan = 0; let isPhase1Inited = false, panelResizeObserver = null; let styleCacheDoc = null, styleCacheDocKey = null; let styleCacheShadow = null, styleCacheShadowKey = null; let currentTheme = null; let undoSnapshot = null; // ---- 时间与令牌 ---- function monotonicNow() { const t = Date.now(); lastTimeCounter = Math.max(t, lastTimeCounter + 1, lastSeenCleanToken + 1); return lastTimeCounter; } function bumpCleanToken() { const stored = Number(gmGet('lastCleanToken', 0)) || 0; const t = Math.max(Date.now(), stored + 1, lastSeenCleanToken + 1); lastTimeCounter = Math.max(lastTimeCounter, t); return t; } function initVisitedLinks() { try { const storedToken = Number(gmGet('lastCleanToken', 0)) || 0; const tokenAdvanced = storedToken > lastSeenCleanToken; lastSeenCleanToken = storedToken; lastCleanInfo = { time: Number(gmGet('lastCleanTime', 0)) || 0, count: Number(gmGet('lastCleanCount', 0)) || 0 }; cleanInfoDirty = false; const preserveLinks = new Map(); const preserveCustom = new Map(); if (tokenAdvanced) { visitedLinksMap.forEach((data, url) => { if (data.time > storedToken) preserveLinks.set(url, data); }); customVisitedMap.forEach((data, key) => { if (data.time > storedToken) preserveCustom.set(key, data); }); } const rawStr = gmGet('visitedLinks', '[]'); const customStr = gmGet('customVisitedKeys', '[]'); lastVisitedRaw = rawStr; lastCustomRaw = customStr; let raw = typeof rawStr === 'string' ? JSON.parse(rawStr) : rawStr; if (!Array.isArray(raw)) raw = []; visitedLinksMap.clear(); raw.forEach(item => { if (!item) return; if (typeof item === 'string') visitedLinksMap.set(item, { time: 0 }); else if (item.url) visitedLinksMap.set(item.url, { time: item.time || 0 }); }); let customRaw = typeof customStr === 'string' ? JSON.parse(customStr) : customStr; if (!Array.isArray(customRaw)) customRaw = []; customVisitedMap.clear(); customRaw.forEach(item => { if (!item) return; if (typeof item === 'string') { if (isStableKey(item)) customVisitedMap.set(item, { time: 0 }); } else if (item.key && isStableKey(item.key)) customVisitedMap.set(item.key, { time: item.time || 0 }); }); preserveLinks.forEach((data, url) => { const ex = visitedLinksMap.get(url); if (!ex || data.time > ex.time) visitedLinksMap.set(url, data); }); preserveCustom.forEach((data, key) => { const ex = customVisitedMap.get(key); if (!ex || data.time > ex.time) customVisitedMap.set(key, data); }); } catch (_) { visitedLinksMap.clear(); customVisitedMap.clear(); } } function isStableKey(key) { return !!key && (key.startsWith('id:') || key.startsWith('dh:') || key.startsWith('du:')); } function mergeVisitedFromStorage() { try { const token = Number(gmGet('lastCleanToken', 0)) || 0; if (token !== lastSeenCleanToken) { initVisitedLinks(); return; } const rawStr = gmGet('visitedLinks', '[]'); if (rawStr !== lastVisitedRaw) { lastVisitedRaw = rawStr; let raw = typeof rawStr === 'string' ? JSON.parse(rawStr) : rawStr; if (Array.isArray(raw)) raw.forEach(item => { if (!item) return; const url = (typeof item === 'string') ? item : item.url; const time = (typeof item === 'string') ? 0 : (item.time || 0); if (!url) return; const ex = visitedLinksMap.get(url); if (!ex) visitedLinksMap.set(url, { time: time || Date.now() }); else if (time > ex.time) { ex.time = time; visitedLinksMap.delete(url); visitedLinksMap.set(url, ex); } }); } const customStr = gmGet('customVisitedKeys', '[]'); if (customStr !== lastCustomRaw) { lastCustomRaw = customStr; let customRaw = typeof customStr === 'string' ? JSON.parse(customStr) : customStr; if (Array.isArray(customRaw)) customRaw.forEach(item => { if (!item) return; const key = (typeof item === 'string') ? item : item.key; const time = (typeof item === 'string') ? 0 : (item.time || 0); if (!key || !isStableKey(key)) return; const ex = customVisitedMap.get(key); if (!ex) customVisitedMap.set(key, { time: time || Date.now() }); else if (time > ex.time) { customVisitedMap.delete(key); customVisitedMap.set(key, ex); } }); } enforceLimits(); } catch (_) {} } function forceSave(skipMerge = false) { try { if (!skipMerge) mergeVisitedFromStorage(); const linksArray = []; visitedLinksMap.forEach((data, url) => linksArray.push({ url, time: data.time })); const customArray = []; customVisitedMap.forEach((data, key) => customArray.push({ key, time: data.time })); const linksStr = JSON.stringify(linksArray); const customStr = JSON.stringify(customArray); if (linksStr !== lastVisitedRaw) { gmSet('visitedLinks', linksStr); lastVisitedRaw = linksStr; } if (customStr !== lastCustomRaw) { gmSet('customVisitedKeys', customStr); lastCustomRaw = customStr; } if (cleanInfoDirty) { gmSet('lastCleanTime', lastCleanInfo.time); gmSet('lastCleanCount', lastCleanInfo.count); cleanInfoDirty = false; } } catch (_) {} } function saveVisitedLinksDebounced() { clearTimeout(saveTimer); saveTimer = setTimeout(() => { if (pendingTokenBump) { pendingTokenBump = false; mergeVisitedFromStorage(); enforceLimits(); const newToken = bumpCleanToken(); lastSeenCleanToken = newToken; forceSave(true); gmSet('lastCleanToken', newToken); } else forceSave(); }, DEBOUNCE_SAVE); } // ---- Toast ---- function showToast(message) { try { const t = document.createElement('div'); t.textContent = String(message); t.style.cssText = 'position:fixed;top:20px;left:50%;transform:translateX(-50%);padding:10px 20px;background:rgba(0,0,0,0.8);color:#fff;border-radius:20px;font-size:14px;z-index:2147483646;font-family:system-ui,-apple-system,sans-serif;pointer-events:none;transition:opacity 0.3s ease;box-shadow:0 4px 16px rgba(0,0,0,0.3);white-space:nowrap;'; document.body.appendChild(t); setTimeout(() => { t.style.opacity = '0'; setTimeout(() => t.remove(), 300); }, 2000); } catch (_) {} } // ---- 导入 / 导出 ---- function exportRecords() { try { mergeVisitedFromStorage(); const links = []; visitedLinksMap.forEach((d, url) => links.push({ url, time: d.time })); const keys = []; customVisitedMap.forEach((d, key) => keys.push({ key, time: d.time })); const data = { version: 1, exportedAt: Date.now(), visitedLinks: links, customVisitedKeys: keys }; const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `visited-links-${new Date().toISOString().slice(0, 10)}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(url), 1000); showToast(`已导出 ${links.length + keys.length} 条记录`); } catch (e) { showToast('导出失败:' + (e && e.message || '未知错误')); } } function pickFile() { return new Promise((resolve) => { const input = document.createElement('input'); input.type = 'file'; input.accept = 'application/json,.json'; input.style.cssText = 'position:fixed;left:-9999px;top:-9999px;opacity:0;'; document.body.appendChild(input); let settled = false; const cleanup = (file) => { if (settled) return; settled = true; try { input.remove(); } catch (_) {} resolve(file); }; input.addEventListener('change', () => cleanup(input.files?.[0] || null)); input.addEventListener('cancel', () => cleanup(null)); input.click(); }); } function importRecords(file) { return new Promise((resolve, reject) => { if (file.size > MAX_IMPORT_SIZE) { return reject(new Error('文件过大(>10MB),已拒绝导入')); } const reader = new FileReader(); reader.onload = () => { try { const data = JSON.parse(reader.result); if (!data || typeof data !== 'object') throw new Error('格式错误'); mergeVisitedFromStorage(); let added = 0, updated = 0, ignored = 0; if (Array.isArray(data.visitedLinks)) { data.visitedLinks.forEach(item => { if (!item) return; const url = typeof item === 'string' ? item : item.url; const time = ((typeof item === 'string' ? 0 : item.time) || Date.now()); if (!url) return; const ex = visitedLinksMap.get(url); if (!ex) { visitedLinksMap.set(url, { time }); added++; } else if (time > ex.time) { ex.time = time; updated++; } else ignored++; }); } if (Array.isArray(data.customVisitedKeys)) { data.customVisitedKeys.forEach(item => { if (!item) return; const key = typeof item === 'string' ? item : item.key; const time = ((typeof item === 'string' ? 0 : item.time) || Date.now()); if (!key || !isStableKey(key)) return; const ex = customVisitedMap.get(key); if (!ex) { customVisitedMap.set(key, { time }); added++; } else if (time > ex.time) { ex.time = time; updated++; } else ignored++; }); } enforceLimits(); forceSave(true); try { applyVisitedStyles(); } catch (_) {} updateStatsInPanel(); resolve({ added, updated, ignored }); } catch (e) { reject(e); } }; reader.onerror = () => reject(reader.error || new Error('读取失败')); reader.readAsText(file); }); } // ---- 快照与撤销 ---- function takeFullSnapshot() { const v = new Map(); visitedLinksMap.forEach((d, k) => v.set(k, { time: d.time })); const c = new Map(); customVisitedMap.forEach((d, k) => c.set(k, { time: d.time })); return { v, c, removedCount: 0 }; } function performUndo() { if (!undoSnapshot) return; mergeVisitedFromStorage(); let restored = 0; undoSnapshot.v.forEach((d, k) => { if (!visitedLinksMap.has(k)) { visitedLinksMap.set(k, { time: d.time }); restored++; } }); undoSnapshot.c.forEach((d, k) => { if (!customVisitedMap.has(k)) { customVisitedMap.set(k, { time: d.time }); restored++; } }); undoSnapshot = null; enforceLimits(); const newToken = bumpCleanToken(); lastSeenCleanToken = newToken; lastCleanInfo = { time: 0, count: 0 }; cleanInfoDirty = true; forceSave(true); gmSet('lastCleanToken', newToken); try { applyVisitedStyles(); } catch (_) {} updateStatsInPanel(); showToast(`已恢复 ${restored} 条记录`); } // ---- 类名标记 ---- 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 (_) {} }); } function removeUnmatchedVisitedClasses() { eachVisitedLinkElement(el => { if (el.closest && el.closest(SCRIPT_UI_SELECTOR)) return; const href = getElementHref(el); if (href) { if (!visitedLinksMap.has(href)) el.classList.remove('visited-link'); } else { const key = getVisitedKeyFromElement(el); if (!key || !customVisitedMap.has(key)) el.classList.remove('visited-link'); } }); } 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(), keys = new Set(); const collectFrom = (root) => { try { root.querySelectorAll('a[href], [data-href], [data-url]').forEach(el => { if (el.closest(SCRIPT_UI_SELECTOR)) return; const href = getElementHref(el); if (href) urls.add(href); 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); const checkIdInRoot = (root, id) => { try { return (typeof root.getElementById === 'function') ? !!root.getElementById(id) : !!root.querySelector(`[id="${escAttrValue(id)}"]`); } catch (_) { return false; } }; customVisitedMap.forEach((_, key) => { if (!key.startsWith('id:')) return; const id = key.slice(3); if (checkIdInRoot(document, id)) { keys.add(key); return; } let found = false; shadowStyleMap.forEach((data, sroot) => { if (found) return; if (sroot.host && sroot.host.isConnected && checkIdInRoot(sroot, id)) found = true; }); if (found) keys.add(key); }); return { urls, keys }; } function clearCurrentPageRecords(dryRun = false) { mergeVisitedFromStorage(); 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; } const snapshot = takeFullSnapshot(); for (const url of urls) if (visitedLinksMap.delete(url)) removed++; for (const key of keys) if (customVisitedMap.delete(key)) removed++; if (removed > 0) { snapshot.removedCount = removed; undoSnapshot = snapshot; const newToken = bumpCleanToken(); lastSeenCleanToken = newToken; lastCleanInfo = { time: newToken, count: removed }; cleanInfoDirty = true; forceSave(true); gmSet('lastCleanToken', newToken); removeUnmatchedVisitedClasses(); updateStatsInPanel(); } return removed; } function applyAutoClean() { if (!isTopFrame) return; try { if (!autoCleanEnabled) return; mergeVisitedFromStorage(); const threshold = Date.now() - autoCleanDays * 86400000; let removed = 0; for (const [url, data] of visitedLinksMap) if (data.time < threshold) { visitedLinksMap.delete(url); removed++; } for (const [key, data] of customVisitedMap) if (data.time < threshold) { customVisitedMap.delete(key); removed++; } if (removed > 0) { const newToken = bumpCleanToken(); lastSeenCleanToken = newToken; lastCleanInfo = { time: newToken, count: removed }; cleanInfoDirty = true; forceSave(true); gmSet('lastCleanToken', newToken); removeUnmatchedVisitedClasses(); } enforceLimits(); updateStatsInPanelDebounced(); } catch (e) { console.warn('[已访问链接样式] 自动清理出错', e); } } function enforceLimits(allowBump = false) { const total = visitedLinksMap.size + customVisitedMap.size; const excess = total - MAX_TOTAL_RECORDS; if (excess <= 0) return; if (excess > 100) { const all = []; visitedLinksMap.forEach((d, k) => all.push({ v: true, k, t: d.time })); customVisitedMap.forEach((d, k) => all.push({ v: false, k, t: d.time })); all.sort((a, b) => a.t - b.t); for (let i = 0; i < excess; i++) { const item = all[i]; if (item.v) visitedLinksMap.delete(item.k); else customVisitedMap.delete(item.k); } } else { let cur = total; while (cur > MAX_TOTAL_RECORDS) { let t = null, k = null, ti = Infinity; visitedLinksMap.forEach((d, u) => { if (d.time < ti) { ti = d.time; t = 'v'; k = u; } }); customVisitedMap.forEach((d, u) => { if (d.time < ti) { ti = d.time; t = 'c'; k = u; } }); if (!t) break; if (t === 'v') visitedLinksMap.delete(k); else customVisitedMap.delete(k); cur--; } } if (allowBump && isTopFrame) pendingTokenBump = true; } // ---- 元素 ---- function getElementHref(el) { if (!el || el.nodeType !== 1) return null; if (el.namespaceURI === 'http://www.w3.org/2000/svg') { return el.getAttribute('href') || el.getAttribute('xlink:href') || null; } return el.href || null; } 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; return el.hasAttribute('onclick') || el.hasAttribute('download') || el.getAttribute('role') === 'button' || el.getAttribute('role') === 'link' || el.hasAttribute('data-href') || el.hasAttribute('data-url'); } function markElementVisited(el) { if (el && el.classList && !el.classList.contains('visited-link')) el.classList.add('visited-link'); } function addVisitedLink(url) { if (!url) return; const now = monotonicNow(); const ex = visitedLinksMap.get(url); if (ex) { ex.time = now; visitedLinksMap.delete(url); visitedLinksMap.set(url, ex); } else visitedLinksMap.set(url, { time: now }); enforceLimits(true); } function addCustomKey(key) { if (!key) return; const now = monotonicNow(); const ex = customVisitedMap.get(key); if (ex) { ex.time = now; customVisitedMap.delete(key); customVisitedMap.set(key, ex); } else customVisitedMap.set(key, { time: now }); enforceLimits(true); } // ---- 匹配扫描 ---- 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 }); 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 (_) {} }; if (typeof requestAnimationFrame === 'function' && document.visibilityState === 'visible') requestAnimationFrame(run); else setTimeout(run, 0); } function doMatchScan(key, isUrl) { let selector, matchFn; if (isUrl) { selector = 'a[href], a[xlink\\:href]'; matchFn = (el) => getElementHref(el) === 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); shadowStyleMap.forEach((data, sroot) => { if (sroot.host && sroot.host.isConnected) scan(sroot); }); } function markLinkAsVisited(link) { if (!link) return; const isAnchor = link.tagName && link.tagName.toUpperCase() === 'A'; const href = isAnchor ? getElementHref(link) : null; if (href) { addVisitedLink(href); markElementVisited(link); markMatchingElements(href, true); saveVisitedLinksDebounced(); return; } const key = getVisitedKeyFromElement(link); if (key) { addCustomKey(key); markElementVisited(link); markMatchingElements(key, false); saveVisitedLinksDebounced(); } } 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; } // ---- 样式(分离 document / shadow 缓存)---- function buildStyleText(isShadow) { const line = settings.underline === 'none' ? 'none' : 'underline'; const ls = settings.underline === 'none' ? 'none' : settings.underline; const c = settings.color; const prefix = isShadow ? '' : 'html body '; return `${prefix}a.visited-link[href],${prefix}button.visited-link,${prefix}.visited-link{color:${c} !important;-webkit-text-fill-color:${c} !important;text-decoration-line:${line} !important;text-decoration-style:${ls} !important;text-decoration-color:${c} !important;font-weight:${settings.weight} !important;forced-color-adjust:none !important;-webkit-forced-color-adjust:none !important;mix-blend-mode:normal !important;isolation:isolate !important;text-shadow:none !important;-webkit-text-stroke:0 !important;color-scheme:light only !important;opacity:1 !important}`; } function getStyleText(isShadow = false) { if (!highlightEnabled) return ''; const key = settings.color + '|' + settings.underline + '|' + settings.weight; if (isShadow) { if (key === styleCacheShadowKey && styleCacheShadow !== null) return styleCacheShadow; styleCacheShadow = buildStyleText(true); styleCacheShadowKey = key; return styleCacheShadow; } if (key === styleCacheDocKey && styleCacheDoc !== null) return styleCacheDoc; styleCacheDoc = buildStyleText(false); styleCacheDocKey = key; return styleCacheDoc; } function updateDocumentStyle() { const parent = document.head || document.documentElement; if (!parent) return; const css = getStyleText(false); if (!styleElement || !styleElement.isConnected) { if (styleElement) styleElement.remove(); try { document.querySelectorAll('#visited-links-style').forEach(el => el.remove()); } catch (_) {} styleElement = document.createElement('style'); styleElement.id = 'visited-links-style'; parent.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(true); shadowRoot.appendChild(style); const shadowObserver = new MutationObserver((mutations) => { const nodes = []; let hasAttr = false; for (const m of mutations) { if (m.type === 'childList') m.addedNodes.forEach(n => nodes.push(n)); else if (m.type === 'attributes') hasAttr = true; } if (nodes.length) applyStylesToNewNodes(nodes); if (hasAttr && highlightEnabled) try { shadowRoot.querySelectorAll(CANDIDATE_SELECTOR).forEach(el => { try { processElement(el); } catch (_) {} }); } catch (_) {} }); shadowObserver.observe(shadowRoot, { childList: true, subtree: true, attributes: true, attributeFilter: ['href', 'data-href', 'data-url', 'id'] }); 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(true); 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 updatePreviewLink() { if (!panelOverlay || !panelOverlay.isConnected) return; const el = panelOverlay.querySelector('.vlx-preview-text'); if (!el) return; const st = el.style; if (highlightEnabled) { const c = settings.color; const line = settings.underline === 'none' ? 'none' : 'underline'; const ls = settings.underline === 'none' ? 'none' : settings.underline; st.color = c; st.setProperty('-webkit-text-fill-color', c); st.textDecorationLine = line; st.textDecorationStyle = ls; st.textDecorationColor = c; st.fontWeight = settings.weight; } else { ['color', '-webkit-text-fill-color', 'text-decoration-line', 'text-decoration-style', 'text-decoration-color', 'font-weight'] .forEach(p => { if (st.getPropertyValue(p)) st.removeProperty(p); }); } } function applyHighlightState() { updateStylesDebounced(); if (highlightEnabled) try { applyVisitedStyles(); } catch (_) {} else try { removeAllVisitedClasses(); } catch (_) {} updatePreviewLink(); } // ---- 值变化监听 ---- function setupCrossContextSync() { if (typeof GM_addValueChangeListener !== 'function') return; const reg = (name, handler) => { try { GM_addValueChangeListener(name, (n, o, v, remote) => { if (remote === false) return; try { handler(v); } catch (_) {} }); } catch (_) {} }; reg('highlightEnabled', (v) => { if (v === highlightEnabled) return; highlightEnabled = v; applyHighlightState(); updateStatsInPanel(); if (panelOverlay && panelOverlay.isConnected) { const sw = panelOverlay.querySelector('#highlight-switch'); if (sw) sw.checked = highlightEnabled; } }); reg('linkColor', (v) => { if (v === settings.color) return; settings.color = v; updateStylesDebounced(); updatePreviewLink(); if (panelOverlay && panelOverlay.isConnected) { const hi = panelOverlay.querySelector('#hex-input'); if (hi && document.activeElement !== hi) hi.value = v; const pb = panelOverlay.querySelector('#color-preview-box'); if (pb) pb.style.backgroundColor = v; if (document.activeElement !== hi) { try { panelOverlay.refreshPicker?.(); } catch (_) {} } } }); reg('underlineType', (v) => { if (v === settings.underline) return; settings.underline = v; updateStylesDebounced(); updatePreviewLink(); if (panelOverlay && panelOverlay.isConnected) { const l = panelOverlay.querySelector('#underline-label'); if (l && underlineLabels[v]) l.textContent = `${underlineLabels[v]} (${v})`; } }); reg('linkWeight', (v) => { if (v === settings.weight) return; settings.weight = v; updateStylesDebounced(); updatePreviewLink(); if (panelOverlay && panelOverlay.isConnected) { const s = panelOverlay.querySelector('#weight-slider'); const d = panelOverlay.querySelector('#weight-display'); if (s) s.value = v; if (d) d.textContent = v; } }); reg('glassAlpha', (v) => { const a = parseFloat(v); if (isNaN(a) || a === settings.glassAlpha) return; settings.glassAlpha = a; if (document.documentElement) document.documentElement.style.setProperty('--vlx-alpha', String(a)); if (panelOverlay && panelOverlay.isConnected) { const s = panelOverlay.querySelector('#alpha-slider'); const d = panelOverlay.querySelector('#alpha-display'); if (s) s.value = a; if (d) d.textContent = a.toFixed(2); } }); reg('blurEnabled', (v) => { if (v === settings.blurEnabled) return; settings.blurEnabled = !!v; if (document.documentElement) document.documentElement.style.setProperty('--vlx-blur', settings.blurEnabled ? '12px' : '0px'); if (panelOverlay && panelOverlay.isConnected) { const sw = panelOverlay.querySelector('#blur-switch'); if (sw) sw.checked = settings.blurEnabled; } }); reg('autoCleanEnabled', (v) => { const val = !!v; if (val === autoCleanEnabled) return; autoCleanEnabled = val; if (panelOverlay && panelOverlay.isConnected) { const sw = panelOverlay.querySelector('#auto-clean-switch'); if (sw) sw.checked = autoCleanEnabled; } if (autoCleanEnabled) applyAutoClean(); }); reg('autoCleanDays', (v) => { const d = parseInt(v, 10); if (isNaN(d) || d === autoCleanDays) return; autoCleanDays = d; if (panelOverlay && panelOverlay.isConnected) { const b = panelOverlay.querySelector('#auto-clean-days-btn'); if (b) b.textContent = formatDayLabel(autoCleanDays); } }); const onStorage = () => { mergeVisitedFromStorage(); try { applyVisitedStyles(); } catch (_) {} updateStatsInPanelDebounced(); }; reg('visitedLinks', onStorage); reg('customVisitedKeys', onStorage); reg('lastCleanToken', onStorage); reg('lastCleanTime', onStorage); reg('lastCleanCount', onStorage); } // ---- 应用样式 ---- function hasValidBody() { return !!(document.body && document.body.childNodes.length > 0); } function processElement(el) { if (!el || el.nodeType !== 1 || !highlightEnabled) return; if (el.closest && el.closest(SCRIPT_UI_SELECTOR)) return; if (el.tagName && el.tagName.toUpperCase() === 'A') { const href = getElementHref(el); if (href) { if (visitedLinksMap.has(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 (!highlightEnabled || !hasValidBody()) return; if (visitedLinksMap.size === 0 && customVisitedMap.size === 0) { removeAllVisitedClasses(); return; } const processRoot = (root) => { let all; try { all = root.querySelectorAll(CANDIDATE_SELECTOR); } catch (_) { return; } if (!all || !all.length) return; let idx = 0; const next = () => { if (!highlightEnabled) return; const end = Math.min(idx + CHUNK_SIZE, all.length); for (; idx < end; idx++) try { processElement(all[idx]); } catch (_) {} if (idx < all.length) requestIdleCallback(next, { timeout: 100 }); }; requestIdleCallback(next, { timeout: 100 }); }; processRoot(document); shadowStyleMap.forEach((_, sroot) => { if (sroot.host && sroot.host.isConnected) processRoot(sroot); }); } function schedulePendingFlush() { if (pendingFlushScheduled) return; pendingFlushScheduled = true; requestIdleCallback(() => { pendingFlushScheduled = false; flushPendingNodes(); }, { timeout: 300 }); } function flushPendingNodes() { if (!pendingNodes.length) return; if (!highlightEnabled) { 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) { if (!highlightEnabled) return; try { const collected = []; for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; if (node.nodeType !== 1 || processedNodeSet.has(node)) continue; if (node.closest && node.closest(SCRIPT_UI_SELECTOR)) 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; if (el.closest && el.closest(SCRIPT_UI_SELECTOR)) 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 path = null; if (evt && typeof evt.composedPath === 'function') try { path = evt.composedPath(); } catch (_) { path = null; } if (!path || !path.length) { if (target.nodeType === 1) path = [target]; else return; } const uiCheck = (el) => el && el.nodeType === 1 && el.closest && el.closest(SCRIPT_UI_SELECTOR); let linkFound = null, clickableFound = null; for (let i = 0; i < path.length; i++) { const n = path[i]; if (!n || n.nodeType !== 1 || uiCheck(n)) continue; if (n.tagName && n.tagName.toUpperCase() === 'A' && getElementHref(n)) { linkFound = n; break; } if (!clickableFound && n.matches && n.matches(INTERACTIVE_SELECTOR) && isClickableElement(n)) clickableFound = n; } const t = linkFound || clickableFound; if (t) markLinkAsVisited(t); } // ---- 颜色转换 ---- function hsvToHex(h, s, v) { h = ((h % 360) + 360) % 360; 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 = p; 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(Math.min(1, Math.max(0, 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 *= 60; if (h < 0) h += 360; } return { h, s, v }; } // ---- 玻璃边框常量 ---- const BTN_GLASS_BORDER = '1px solid rgba(255,255,255,0.4)'; const BTN_GLASS_SHADOW = 'inset 0 1px 0.5px rgba(255,255,255,0.85), inset 0 -1px 0.5px rgba(255,255,255,0.25), inset 1px 0 0.5px rgba(255,255,255,0.5), inset -1px 0 0.5px rgba(255,255,255,0.5), 0 2px 6px rgba(0,0,0,0.06)'; const BTN_GLASS_SHADOW_HOVER = 'inset 0 1px 0.5px rgba(255,255,255,0.95), inset 0 -1px 0.5px rgba(255,255,255,0.35), inset 1px 0 0.5px rgba(255,255,255,0.65), inset -1px 0 0.5px rgba(255,255,255,0.65), 0 3px 8px rgba(0,0,0,0.08)'; function injectGlobalDialogStyles() { const parent = document.head || document.documentElement; if (!parent) return; const css = ` .vlx{color-scheme:light !important;} .vlx *{forced-color-adjust:none !important;-webkit-print-color-adjust:exact !important;print-color-adjust:exact !important;} .vlx.glass-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:radial-gradient(ellipse at center,rgba(0,0,0,0.20) 0%,rgba(0,0,0,0.38) 100%);backdrop-filter:blur(0px) saturate(140%);-webkit-backdrop-filter:blur(0px) saturate(140%);display:flex;align-items:center;justify-content:center;z-index:2147483647;opacity:0;visibility:hidden;pointer-events:none;transition:opacity 0.3s ease,backdrop-filter 0.35s ease,-webkit-backdrop-filter 0.35s ease,visibility 0s linear 0.35s;} .vlx.glass-overlay.show{opacity:1;visibility:visible;pointer-events:auto;backdrop-filter:blur(var(--vlx-blur,12px)) saturate(160%);-webkit-backdrop-filter:blur(var(--vlx-blur,12px)) saturate(160%);transition:opacity 0.3s ease,backdrop-filter 0.35s ease,-webkit-backdrop-filter 0.35s ease,visibility 0s;} .vlx .glass-card{background:rgba(255,255,255,calc(0.05 + var(--vlx-alpha,0.7) * 0.6)) !important;border-radius:24px;border:1px solid rgba(255,255,255,0.4);backdrop-filter:blur(calc(22px + var(--vlx-alpha,0.7) * 18px)) saturate(240%) brightness(1.15) contrast(1.08) !important;-webkit-backdrop-filter:blur(calc(22px + var(--vlx-alpha,0.7) * 18px)) saturate(240%) brightness(1.15) contrast(1.08) !important;box-shadow:0 30px 60px -12px rgba(0,0,0,0.22),0 18px 36px -18px rgba(0,0,0,0.16),inset 0 1px 0.5px rgba(255,255,255,0.85),inset 0 -1px 0.5px rgba(255,255,255,0.25),inset 1px 0 0.5px rgba(255,255,255,0.5),inset -1px 0 0.5px rgba(255,255,255,0.5) !important;padding:28px 26px;width:440px;max-width:94vw;max-height:85vh;overflow-y:auto;position:relative;color:var(--vlx-text-primary,#2c2c2c);font-family:system-ui,-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;transform:scale(0.92);transition:transform 0.4s cubic-bezier(0.34,1.56,0.64,1),box-shadow 0.6s ease,background 0.6s ease,color 0.35s ease !important;box-sizing:border-box;user-select:none;color-scheme:light !important;filter:none !important;-webkit-filter:none !important;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;scrollbar-width:none;-ms-overflow-style:none;} .vlx .glass-card::-webkit-scrollbar{display:none;width:0;height:0;} .vlx.glass-overlay.show .glass-card{transform:scale(1);} .vlx.glass-overlay.vlx-scrolling .glass-card{backdrop-filter:blur(calc(22px + var(--vlx-alpha,0.7) * 18px + var(--vlx-scroll-tick,0) * 5px)) saturate(calc(240% + var(--vlx-scroll-tick,0) * 40%)) brightness(1.15) contrast(1.08) !important;-webkit-backdrop-filter:blur(calc(22px + var(--vlx-alpha,0.7) * 18px + var(--vlx-scroll-tick,0) * 5px)) saturate(calc(240% + var(--vlx-scroll-tick,0) * 40%)) brightness(1.15) contrast(1.08) !important;} .vlx input,.vlx button,.vlx select,.vlx textarea{color-scheme:light !important;color:var(--vlx-text-secondary,#333);transition:color 0.35s ease,background-color 0.3s ease,border-color 0.3s ease;} .vlx .card-header{display:flex;align-items:center;gap:10px;margin-bottom:8px;padding-bottom:10px;border-bottom:1.5px solid var(--vlx-divider,rgba(255,255,255,0.8));cursor:move;transition:border-color 0.35s ease;} .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:var(--vlx-text-primary,#1a1a1a);pointer-events:none;transition:color 0.35s ease;} .vlx .close-btn{position:absolute;top:14px;right:14px;width:30px;height:30px;padding:0;margin:0;border:none;border-radius:50%;cursor:pointer;z-index:2;display:flex;align-items:center;justify-content:center;color:var(--vlx-text-primary,#1a1a1a) !important;-webkit-text-fill-color:var(--vlx-text-primary,#1a1a1a) !important;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,Roboto,sans-serif;font-size:18px;font-weight:300;line-height:1;font-variant-emoji:text;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-align:center;background:var(--vlx-btn-circle-bg,linear-gradient(180deg,rgba(255,255,255,0.55) 0%,rgba(255,255,255,0.10) 42%,rgba(255,255,255,0.02) 60%,transparent 100%),linear-gradient(0deg,rgba(255,255,255,0.18) 0%,rgba(255,255,255,0.02) 35%,transparent 55%),rgba(255,255,255,0.42));box-shadow:var(--vlx-btn-circle-shadow,0 0 0 1px rgba(255,255,255,0.55),0 2px 5px rgba(0,0,0,0.08),0 4px 12px rgba(0,0,0,0.06),inset 0 1px 0.5px rgba(255,255,255,0.95),inset 0 -1px 0.5px rgba(255,255,255,0.35),inset 1px 0 0.5px rgba(255,255,255,0.6),inset -1px 0 0.5px rgba(255,255,255,0.6));transition:color 0.35s ease,background 0.35s ease,box-shadow 0.35s ease,transform 0.18s cubic-bezier(0.34,1.56,0.64,1);-webkit-tap-highlight-color:transparent;user-select:none;} .vlx .close-btn:hover{background:var(--vlx-btn-circle-bg-hover,linear-gradient(180deg,rgba(255,255,255,0.70) 0%,rgba(255,255,255,0.16) 42%,rgba(255,255,255,0.04) 60%,transparent 100%),linear-gradient(0deg,rgba(255,255,255,0.25) 0%,rgba(255,255,255,0.04) 35%,transparent 55%),rgba(255,255,255,0.55));box-shadow:var(--vlx-btn-circle-shadow-hover,0 0 0 1px rgba(255,255,255,0.7),0 3px 8px rgba(0,0,0,0.1),0 6px 16px rgba(0,0,0,0.08),inset 0 1px 0.5px rgba(255,255,255,1),inset 0 -1px 0.5px rgba(255,255,255,0.45),inset 1px 0 0.5px rgba(255,255,255,0.7),inset -1px 0 0.5px rgba(255,255,255,0.7));} .vlx .close-btn:active{transform:scale(0.9);background:var(--vlx-btn-circle-bg-active,linear-gradient(180deg,rgba(255,255,255,0.35) 0%,rgba(255,255,255,0.08) 50%,transparent 100%),rgba(255,255,255,0.30));box-shadow:var(--vlx-btn-circle-shadow-active,0 0 0 1px rgba(255,255,255,0.4),0 1px 3px rgba(0,0,0,0.1),inset 0 1px 2px rgba(0,0,0,0.08),inset 0 -1px 0.5px rgba(255,255,255,0.15));} .vlx .close-btn:focus-visible{outline:2px solid #4a90e2;outline-offset:2px;} .vlx .setting-row{display:flex;flex-direction:column;align-items:stretch;gap:6px;margin-bottom:15px;width:100%;} .vlx .setting-row > label:not(.switch){min-width:auto;margin-bottom:0;font-size:14px;color:var(--vlx-text-secondary,#444);font-weight:500;transition:color 0.35s ease;} .vlx .setting-pair{display:flex;gap:12px;align-items:stretch;margin-bottom:15px;} .vlx .setting-pair > .setting-row{flex:1 1 0;min-width:0;margin-bottom:0;} .vlx .setting-row.slider-row{display:grid;grid-template-columns:auto auto 1fr;grid-template-areas:"label value ." "slider slider slider";gap:6px 6px;align-items:center;} .vlx .setting-row.slider-row > label:not(.switch){grid-area:label;margin-bottom:0;} .vlx .setting-row.slider-row > .weight-row{display:contents;} .vlx .setting-row.slider-row > .weight-row > input[type="range"]{grid-area:slider;width:100%;min-width:0;} .vlx .setting-row.slider-row > .weight-row > .weight-value{grid-area:value;text-align:left;min-width:auto;font-size:14px;padding-left:0;} .vlx .preview-btn{flex:1;display:flex;align-items:center;justify-content:center;gap:10px;padding:10px 16px;background:var(--vlx-btn-bg,rgba(255,255,255,0.5));border:${BTN_GLASS_BORDER} !important;border-radius:12px;cursor:pointer;font-size:14px;color:var(--vlx-text-secondary,#333);transition:all 0.25s ease,color 0.35s ease;width:100%;box-sizing:border-box;box-shadow:${BTN_GLASS_SHADOW} !important;} .vlx .preview-btn:hover{background:var(--vlx-btn-bg-hover,rgba(255,255,255,0.8));border-color:#4a90e2 !important;box-shadow:${BTN_GLASS_SHADOW_HOVER} !important;} .vlx .preview-btn:focus-visible{outline:2px solid #4a90e2;outline-offset:2px;} .vlx .preview-btn > span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%;} .vlx .weight-row{display:flex;align-items:center;gap:10px;flex:1;} .vlx input[type="range"]{flex:1;height:8px;-webkit-appearance:none !important;appearance:none !important;background:rgba(255,255,255,0.45) !important;background-color:rgba(255,255,255,0.45) !important;background-image:none !important;border-radius:8px;outline:none;border:1px solid rgba(255,255,255,0.6);transition:background 0.2s,border-color 0.2s,box-shadow 0.2s;forced-color-adjust:none !important;-webkit-forced-color-adjust:none !important;color-scheme:light only !important;accent-color:#ffffff !important;filter:none !important;-webkit-filter:none !important;mix-blend-mode:normal !important;} .vlx input[type="range"]:hover,.vlx input[type="range"]:active,.vlx input[type="range"]:focus-visible,.vlx input[type="range"].vlx-range-active{background:rgba(255,255,255,0.9) !important;background-color:rgba(255,255,255,0.9) !important;border-color:#4a90e2 !important;box-shadow:0 0 0 2px rgba(74,144,226,0.35) !important;} .vlx input[type="range"]::-webkit-slider-runnable-track{-webkit-appearance:none !important;appearance:none !important;background:transparent !important;background-color:transparent !important;background-image:none !important;border:none !important;height:8px;} .vlx input[type="range"]::-webkit-slider-thumb{-webkit-appearance:none !important;appearance:none !important;width:26px !important;height:26px !important;background:#ffffff !important;background-color:#ffffff !important;background-image:none !important;border:0 !important;border-radius:50% !important;box-shadow:0 0 0 1000px #ffffff inset,0 2px 10px rgba(0,0,0,0.15),0 0 0 2px rgba(255,255,255,0.5) !important;cursor:pointer !important;transition:transform 0.15s,box-shadow 0.15s;forced-color-adjust:none !important;-webkit-forced-color-adjust:none !important;color-scheme:light only !important;accent-color:#ffffff !important;color:#ffffff !important;fill:#ffffff !important;stroke:none !important;filter:none !important;-webkit-filter:none !important;opacity:1 !important;mix-blend-mode:normal !important;margin-top:-9px;} .vlx input[type="range"]:hover::-webkit-slider-thumb,.vlx input[type="range"]:active::-webkit-slider-thumb,.vlx input[type="range"]:focus-visible::-webkit-slider-thumb,.vlx input[type="range"].vlx-range-active::-webkit-slider-thumb{transform:scale(1.18);box-shadow:0 0 0 1000px #ffffff inset,0 2px 8px rgba(0,0,0,0.2),0 0 0 3px rgba(74,144,226,0.65) !important;} .vlx input[type="range"]::-moz-range-track{background:transparent !important;background-color:transparent !important;background-image:none !important;border:none !important;height:8px;} .vlx input[type="range"]::-moz-range-thumb{width:22px !important;height:22px !important;background:#ffffff !important;background-color:#ffffff !important;background-image:none !important;border:0 !important;border-radius:50% !important;box-shadow:0 0 0 1000px #ffffff inset,0 2px 10px rgba(0,0,0,0.15) !important;cursor:pointer !important;transition:transform 0.15s,box-shadow 0.15s;forced-color-adjust:none !important;-webkit-forced-color-adjust:none !important;color-scheme:light only !important;accent-color:#ffffff !important;color:#ffffff !important;filter:none !important;-webkit-filter:none !important;opacity:1 !important;mix-blend-mode:normal !important;} .vlx input[type="range"]:hover::-moz-range-thumb,.vlx input[type="range"]:active::-moz-range-thumb,.vlx input[type="range"]:focus-visible::-moz-range-thumb,.vlx input[type="range"].vlx-range-active::-moz-range-thumb{transform:scale(1.18);box-shadow:0 0 0 1000px #ffffff inset,0 2px 8px rgba(0,0,0,0.2),0 0 0 3px rgba(74,144,226,0.65) !important;} .vlx .weight-value{min-width:45px;text-align:center;font-weight:700;font-size:16px;color:var(--vlx-text-primary,#333);transition:color 0.35s ease;} .vlx .danger-btn{background:var(--vlx-danger-bg,rgba(255,255,255,0.55));border:${BTN_GLASS_BORDER} !important;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;box-shadow:${BTN_GLASS_SHADOW} !important;} .vlx .danger-btn:hover{background:#d63031;color:white;border-color:rgba(255,255,255,0.5) !important;box-shadow:inset 0 1px 0.5px rgba(255,255,255,0.5),inset 0 -1px 0.5px rgba(255,255,255,0.15),0 3px 8px rgba(0,0,0,0.15) !important;} .vlx .danger-btn:focus-visible{outline:2px solid #d63031;outline-offset:2px;} .vlx .danger-btn.page-clear-btn{color:#e67e22;} .vlx .danger-btn.page-clear-btn:hover{background:#e67e22;color:white;border-color:rgba(255,255,255,0.5) !important;} .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;align-items:center;flex-wrap:wrap;} .vlx .confirm-buttons button{padding:12px 24px;border-radius:12px;font-size:15px;font-weight:500;cursor:pointer;border:${BTN_GLASS_BORDER} !important;background:var(--vlx-btn-bg,rgba(255,255,255,0.45));color:var(--vlx-text-secondary,#333);transition:all 0.25s ease,color 0.35s ease;box-shadow:${BTN_GLASS_SHADOW} !important;} .vlx .confirm-buttons button:hover{background:var(--vlx-btn-bg-hover,rgba(255,255,255,0.8));box-shadow:${BTN_GLASS_SHADOW_HOVER} !important;} .vlx .confirm-buttons button.confirm-yes{background:#d63031;color:white;border-color:rgba(255,255,255,0.4) !important;box-shadow:inset 0 1px 0.5px rgba(255,255,255,0.5),inset 0 -1px 0.5px rgba(255,255,255,0.15),0 2px 8px rgba(214,48,49,0.3) !important;} .vlx .confirm-buttons button.confirm-yes:hover{background:#c0262c;box-shadow:inset 0 1px 0.5px rgba(255,255,255,0.5),inset 0 -1px 0.5px rgba(255,255,255,0.15),0 3px 10px rgba(214,48,49,0.4) !important;} .vlx .confirm-buttons button.undo-last-btn{background:rgba(74,144,226,0.9);color:#fff !important;-webkit-text-fill-color:#fff !important;border-color:rgba(255,255,255,0.35) !important;box-shadow:inset 0 1px 0.5px rgba(255,255,255,0.5),inset 0 -1px 0.5px rgba(255,255,255,0.15),0 2px 8px rgba(74,144,226,0.35) !important;} .vlx .confirm-buttons button.undo-last-btn:hover{background:#3a7bc8;transform:translateY(-1px);box-shadow:inset 0 1px 0.5px rgba(255,255,255,0.5),inset 0 -1px 0.5px rgba(255,255,255,0.15),0 4px 12px rgba(74,144,226,0.45) !important;} .vlx .confirm-buttons.has-undo{display:grid;grid-template-columns:1fr 1fr;gap:10px;justify-content:initial;} .vlx .confirm-buttons.has-undo .undo-last-btn{grid-column:1 / -1;width:100%;} .vlx .confirm-buttons.has-undo .cancel-btn,.vlx .confirm-buttons.has-undo .confirm-yes{width:100%;} .vlx .cancel-custom,.vlx .confirm-custom{border:${BTN_GLASS_BORDER} !important;box-shadow:${BTN_GLASS_SHADOW} !important;} .vlx .confirm-custom.confirm-yes{box-shadow:inset 0 1px 0.5px rgba(255,255,255,0.5),inset 0 -1px 0.5px rgba(255,255,255,0.15),0 2px 8px rgba(214,48,49,0.3) !important;} .vlx .picker-area{display:flex;flex-direction:column;gap:10px;margin:6px 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);background:#ff0000;filter:none !important;-webkit-filter:none !important;mix-blend-mode:normal !important;forced-color-adjust:none !important;color-scheme:light only !important;will-change:background-color;transform:translateZ(0);isolation:isolate;} .vlx .sv-panel .sv-bg{position:absolute;top:0;left:0;width:100%;height:100%;background-image:linear-gradient(to top,#000000 0%,rgba(0,0,0,0) 100%),linear-gradient(to right,#ffffff 0%,rgba(255,255,255,0) 100%) !important;filter:none !important;-webkit-filter:none !important;mix-blend-mode:normal !important;opacity:1 !important;forced-color-adjust:none !important;will-change:background-color;} .vlx .sv-panel .sv-cursor{position:absolute;width:16px;height:16px;border:2px solid #ffffff;border-radius:50%;box-shadow:0 0 0 1px rgba(0,0,0,0.3);pointer-events:none;left:0;top:0;transform:translate3d(0,0,0) translate(-50%,-50%);background:transparent !important;filter:none !important;mix-blend-mode:normal !important;will-change:left,top;} .vlx .hue-slider{position:relative;width:100%;height:18px;border-radius:9px;background-image:linear-gradient(to right,#ff0000 0%,#ffff00 16.6667%,#00ff00 33.3333%,#00ffff 50%,#0000ff 66.6667%,#ff00ff 83.3333%,#ff0000 100%) !important;background-color:#ff0000 !important;cursor:pointer;border:none !important;box-shadow:0 0 0 1px rgba(255,255,255,0.85),0 2px 6px rgba(0,0,0,0.06) !important;transition:box-shadow 0.2s;filter:none !important;-webkit-filter:none !important;mix-blend-mode:normal !important;opacity:1 !important;forced-color-adjust:none !important;color-scheme:light only !important;background-clip:border-box !important;box-sizing:border-box;} .vlx .hue-slider:hover,.vlx .hue-slider:active,.vlx .hue-slider.vlx-hue-active{box-shadow:0 0 0 1px rgba(74,144,226,1),0 0 0 3px rgba(74,144,226,0.4) !important;} .vlx .hue-slider .hue-thumb{position:absolute;top:-5px;left:0;width:28px;height:28px;background:#ffffff !important;background-color:#ffffff !important;background-image:none !important;border-radius:50%;box-shadow:0 0 0 1000px #ffffff inset,0 2px 8px rgba(0,0,0,0.3) !important;pointer-events:none;transform:translate3d(0,0,0) translateX(-50%);transition:transform 0.15s ease,box-shadow 0.15s;will-change:left;forced-color-adjust:none !important;accent-color:#ffffff !important;color:#ffffff !important;filter:none !important;-webkit-filter:none !important;opacity:1 !important;mix-blend-mode:normal !important;} .vlx .hue-slider:hover .hue-thumb,.vlx .hue-slider:active .hue-thumb,.vlx .hue-slider.vlx-hue-active .hue-thumb{box-shadow:0 0 0 1000px #ffffff inset,0 2px 8px rgba(0,0,0,0.2),0 0 0 3px rgba(74,144,226,0.65) !important;} .vlx .color-preview-block{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;width:80px;isolation:isolate;color-scheme:light only !important;} .vlx .color-preview-block .preview-box{width:100%;height:36px;border-radius:9px;border:none !important;box-shadow:inset 0 0 0 1px rgba(0,0,0,0.08),0 2px 6px rgba(0,0,0,0.06) !important;isolation:isolate;background-clip:border-box !important;color-scheme:light only !important;filter:none !important;-webkit-filter:none !important;mix-blend-mode:normal !important;opacity:1 !important;} .vlx .color-preview-block input[type="text"]{width:100%;padding:6px 4px;background:var(--vlx-input-bg,rgba(255,255,255,0.45));border:${BTN_GLASS_BORDER} !important;border-radius:8px;font-size:12px;text-align:center;outline:none;box-sizing:border-box;color:var(--vlx-text-secondary,#333);transition:color 0.35s ease,background-color 0.3s ease,border-color 0.3s ease;box-shadow:${BTN_GLASS_SHADOW} !important;} .vlx .import-export-btns{display:flex;flex-direction:column;gap:6px;width:100%;} .vlx .mini-btn{padding:4px 6px;font-size:11px;background:var(--vlx-btn-bg,rgba(255,255,255,0.5));border:${BTN_GLASS_BORDER} !important;border-radius:6px;cursor:pointer;color:var(--vlx-text-secondary,#333);transition:all 0.2s ease,color 0.35s ease;box-shadow:${BTN_GLASS_SHADOW} !important;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-align:center;font-family:inherit;line-height:1.4;} .vlx .mini-btn:hover{background:var(--vlx-btn-bg-hover,rgba(255,255,255,0.8));border-color:#4a90e2 !important;box-shadow:${BTN_GLASS_SHADOW_HOVER} !important;} .vlx .mini-btn:focus-visible{outline:2px solid #4a90e2;outline-offset:2px;} .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:var(--vlx-btn-bg,rgba(255,255,255,0.5));border:${BTN_GLASS_BORDER} !important;border-radius:10px;cursor:pointer;font-size:14px;color:var(--vlx-text-secondary,#333);transition:all 0.2s ease,color 0.35s ease;box-shadow:${BTN_GLASS_SHADOW} !important;} .vlx .underline-option:hover{background:var(--vlx-btn-bg-hover,rgba(255,255,255,0.8));border-color:#4a90e2 !important;box-shadow:${BTN_GLASS_SHADOW_HOVER} !important;} .vlx .underline-option.active{background:#4a90e2;color:white;border-color:rgba(255,255,255,0.4) !important;font-weight:600;box-shadow:inset 0 1px 0.5px rgba(255,255,255,0.5),inset 0 -1px 0.5px rgba(255,255,255,0.15),0 2px 8px rgba(74,144,226,0.3) !important;} .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);forced-color-adjust:none !important;filter:none !important;mix-blend-mode:normal !important;} .vlx .slider:before{position:absolute;content:"";height:20px;width:20px;left:2px;bottom:2px;background:#ffffff !important;background-color:#ffffff !important;background-image:none !important;transition:.3s;border-radius:50%;box-shadow:0 0 0 1000px #ffffff inset,0 1px 4px rgba(0,0,0,0.2) !important;forced-color-adjust:none !important;accent-color:#ffffff !important;filter:none !important;opacity:1 !important;mix-blend-mode:normal !important;} .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:var(--vlx-hint-bg,rgba(255,255,255,0.5));border-radius:10px;padding:5px 12px;margin-bottom:6px;font-size:13px;color:var(--vlx-text-secondary,#333);transition:color 0.35s ease,background-color 0.3s ease;} .vlx .clean-info{font-size:12px;color:var(--vlx-text-tertiary,#555);margin-top:2px;margin-bottom:0;min-height:14px;line-height:1.3;transition:color 0.35s ease;} .vlx .preview-link-box{padding:10px 10px;background:var(--vlx-hint-bg,rgba(255,255,255,0.5));border:${BTN_GLASS_BORDER} !important;border-radius:12px;flex:1;text-align:center;font-size:14px;box-sizing:border-box;isolation:isolate;color-scheme:light only !important;transition:background-color 0.3s ease,border-color 0.3s ease;box-shadow:${BTN_GLASS_SHADOW} !important;} .vlx .preview-link-box .vlx-preview-text{-webkit-text-fill-color:currentColor !important;text-shadow:none !important;color-scheme:light only !important;filter:none !important;-webkit-filter:none !important;mix-blend-mode:normal !important;isolation:isolate;} .vlx .toggles-row{display:grid;grid-template-columns:minmax(0,1fr) auto;grid-template-areas:"blur highlight" "clean days";gap:10px 12px;align-items:center;margin-bottom:15px;width:100%;box-sizing:border-box;} .vlx .toggle-item{display:flex;align-items:center;gap:8px;min-width:0;} .vlx .toggle-item.toggle-blur{grid-area:blur;justify-content:flex-start;} .vlx .toggle-item.toggle-clean{grid-area:clean;justify-content:flex-start;} .vlx .toggle-item.toggle-highlight{grid-area:highlight;justify-content:flex-end;} .vlx .toggle-item > label:not(.switch){font-size:14px;color:var(--vlx-text-secondary,#444);font-weight:500;white-space:nowrap;min-width:auto;margin-bottom:0;transition:color 0.35s ease;} .vlx .toggles-row .day-btn{grid-area:days;justify-self:stretch;width:auto;min-width:100px;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding:6px 10px;font-size:13px;box-sizing:border-box;border-radius:10px;} @media (max-width:480px){.vlx .glass-card{padding:18px 14px;width:96vw;max-height:90vh;}.vlx .card-header h2{font-size:18px;}.vlx .preview-btn{font-size:13px;padding:10px 8px;}.vlx .preview-link-box{font-size:13px;padding:10px 8px;word-break:break-all;}.vlx input[type="range"]::-webkit-slider-thumb{width:22px;height:22px;margin-top:-7px;}.vlx input[type="range"]::-moz-range-thumb{width:20px;height:20px;}.vlx .picker-area{margin:6px 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;flex-shrink:0;}.vlx .color-preview-block input[type="text"]{flex:1;width:auto;}.vlx .import-export-btns{flex-direction:row;gap:6px;width:auto;flex-shrink:0;}.vlx .mini-btn{padding:6px 10px;font-size:12px;}.vlx .hue-slider{margin-top:4px;}.vlx .underline-option{flex:0 0 calc(50% - 4px);width:auto;box-sizing:border-box;}.vlx .danger-row .danger-btn{font-size:14px;padding:11px 6px;}.vlx .toggles-row .day-btn{min-width:90px;font-size:12px;padding:5px 8px;}.vlx .confirm-buttons{gap:8px;}.vlx .confirm-buttons button{padding:10px 16px;font-size:14px;}} `; let style = document.getElementById('glass-dialog-styles'); if (!style) { style = document.createElement('style'); style.id = 'glass-dialog-styles'; parent.appendChild(style); } style.textContent = css; } // ---- 主题变量 ---- const THEME_LIGHT = { '--vlx-text-primary': '#ffffff', '--vlx-text-secondary': '#ffffff', '--vlx-text-tertiary': '#ffffff', '--vlx-btn-bg': 'rgba(255,255,255,0.16)', '--vlx-btn-bg-hover': 'rgba(255,255,255,0.28)', '--vlx-btn-border': 'rgba(255,255,255,0.24)', '--vlx-divider': 'rgba(255,255,255,0.2)', '--vlx-input-bg': 'rgba(255,255,255,0.14)', '--vlx-hint-bg': 'rgba(255,255,255,0.12)', '--vlx-btn-circle-bg': 'linear-gradient(180deg, rgba(255,255,255,0.30) 0%, rgba(255,255,255,0.06) 42%, rgba(255,255,255,0.01) 60%, transparent 100%), ' + 'linear-gradient(0deg, rgba(255,255,255,0.12) 0%, rgba(255,255,255,0.02) 35%, transparent 55%), ' + 'rgba(0,0,0,0.38)', '--vlx-btn-circle-shadow': '0 0 0 1px rgba(255,255,255,0.30), 0 2px 5px rgba(0,0,0,0.25), 0 4px 12px rgba(0,0,0,0.20), ' + 'inset 0 1px 0.5px rgba(255,255,255,0.55), inset 0 -1px 0.5px rgba(255,255,255,0.15), ' + 'inset 1px 0 0.5px rgba(255,255,255,0.30), inset -1px 0 0.5px rgba(255,255,255,0.30)', '--vlx-btn-circle-bg-hover': 'linear-gradient(180deg, rgba(255,255,255,0.42) 0%, rgba(255,255,255,0.10) 42%, rgba(255,255,255,0.02) 60%, transparent 100%), ' + 'linear-gradient(0deg, rgba(255,255,255,0.18) 0%, rgba(255,255,255,0.02) 35%, transparent 55%), ' + 'rgba(0,0,0,0.50)', '--vlx-btn-circle-shadow-hover': '0 0 0 1px rgba(255,255,255,0.45), 0 3px 8px rgba(0,0,0,0.30), 0 6px 16px rgba(0,0,0,0.25), ' + 'inset 0 1px 0.5px rgba(255,255,255,0.65), inset 0 -1px 0.5px rgba(255,255,255,0.20), ' + 'inset 1px 0 0.5px rgba(255,255,255,0.40), inset -1px 0 0.5px rgba(255,255,255,0.40)', '--vlx-btn-circle-bg-active': 'linear-gradient(180deg, rgba(255,255,255,0.18) 0%, rgba(255,255,255,0.05) 50%, transparent 100%), ' + 'rgba(0,0,0,0.45)', '--vlx-btn-circle-shadow-active': '0 0 0 1px rgba(255,255,255,0.25), 0 1px 3px rgba(0,0,0,0.25), ' + 'inset 0 1px 2px rgba(0,0,0,0.25), inset 0 -1px 0.5px rgba(255,255,255,0.10)', '--vlx-danger-bg': 'rgba(255,255,255,0.16)' }; // ✅ 修复:把 --vlx-alpha / --vlx-blur 纳入主题变量统一重设,防 SPA 重置 html.style function applyThemeVariables() { const root = document.documentElement; if (!root) return; try { for (const k in THEME_LIGHT) root.style.setProperty(k, THEME_LIGHT[k]); root.style.setProperty('--vlx-alpha', String(settings.glassAlpha)); root.style.setProperty('--vlx-blur', settings.blurEnabled ? '12px' : '0px'); currentTheme = true; } catch (_) { currentTheme = null; } } let vlxScrollRaf = null, vlxScrollIdleTimer = null, vlxScrollTick = 0; function startVlxScrollRepaint() { if (vlxScrollRaf !== null) return; const tick = () => { vlxScrollTick = (vlxScrollTick + 1) % 100; if (panelOverlay && panelOverlay.classList.contains('show')) { const card = panelOverlay.querySelector('.glass-card'); if (card) card.style.setProperty('--vlx-scroll-tick', String(vlxScrollTick / 100)); } vlxScrollRaf = requestAnimationFrame(tick); }; vlxScrollRaf = requestAnimationFrame(tick); } function stopVlxScrollRepaint() { if (vlxScrollRaf !== null) { cancelAnimationFrame(vlxScrollRaf); vlxScrollRaf = null; } vlxScrollTick = 0; if (panelOverlay) { panelOverlay.classList.remove('vlx-scrolling'); const card = panelOverlay.querySelector('.glass-card'); if (card) card.style.removeProperty('--vlx-scroll-tick'); } } function handleVlxScroll() { if (!panelOverlay || !panelOverlay.classList.contains('show')) return; panelOverlay.classList.add('vlx-scrolling'); startVlxScrollRepaint(); clearTimeout(vlxScrollIdleTimer); vlxScrollIdleTimer = setTimeout(() => { stopVlxScrollRepaint(); }, 180); } // ---- 拖拽卡片 ---- function makeDraggable(card, headerSelector = '.card-header') { const header = card.querySelector(headerSelector); if (!header) return; let startX, startY, startLeft, startTop, dragging = false, hasDragged = 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 cx = e.touches ? e.touches[0].clientX : e.clientX; const cy = e.touches ? e.touches[0].clientY : e.clientY; const dx = cx - startX, dy = cy - startY; if (!dragging && Math.abs(dx) < 5 && Math.abs(dy) < 5) return; dragging = true; hasDragged = true; e.preventDefault(); card.style.left = (startLeft + dx) + 'px'; card.style.top = (startTop + dy) + 'px'; }; const onEnd = () => { if (!dragging && !hasDragged) { card.style.position = ''; card.style.left = ''; card.style.top = ''; card.style.margin = ''; card.style.transform = ''; } startX = undefined; 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) { const displayTime = lastCleanInfo.time ? Math.min(lastCleanInfo.time, Date.now()) : 0; const cleanTxt = displayTime ? `上次清理:${new Date(displayTime).toLocaleString()},移除 ${lastCleanInfo.count} 条` : '尚未执行清理'; refs.cleanInfo.textContent = (!highlightEnabled ? '🔕 高亮已关闭。' : '') + cleanTxt; } } function updateStatsInPanelDebounced() { clearTimeout(statsUpdateTimer); statsUpdateTimer = setTimeout(updateStatsInPanel, 200); } function bindDialogOverlay(overlay) { const close = () => { stopVlxScrollRepaint(); overlay.classList.remove('show'); setTimeout(() => { try { overlay.remove(); } catch (_) {} }, 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 presets = [1, 7, 30]; const isCustom = !presets.includes(currentDays); overlay.innerHTML = `
💡 上次清理移除了 ${undoCount} 条记录,可点击下方「撤销上次清理」恢复
` : `💡 本次操作完成后,仍可通过再次打开清理弹窗找到「撤销上次清理」入口
`; overlay.innerHTML = `
${escapeHtml(message)}
${undoHintHtml}