// ==UserScript== // @name 阅读位置标记 // @namespace your.namespace // @version 1.4 // @description 在安卓移动端(包括 Via 浏览器)上,选中文字并标记阅读位置,增强兼容性处理脚本冲突。 // @match *://*/* // @grant none // @run-at document-idle // @downloadURL none // ==/UserScript== (function() { 'use strict'; // 防止重复执行 if (window.readingPositionMarkerActive) { return; } window.readingPositionMarkerActive = true; const SCRIPT_ID = 'reading-position-marker'; const styleId = SCRIPT_ID + '-style'; let currentHighlightOverlay = null; let savedSelection = null; let isScriptActive = true; // 兼容性检查和初始化 function initializeScript() { try { // 检查必要的API是否可用 if (!window.getSelection || !document.createRange || !document.evaluate) { console.warn('[阅读标记] 浏览器不支持必要的API'); return false; } // 注入CSS样式,使用更高的优先级 injectStyles(); // 设置事件监听器 setupEventListeners(); // 延迟加载保存的位置,确保页面完全加载 setTimeout(loadReadingPosition, 1000); console.log('[阅读标记] 脚本初始化成功'); return true; } catch (e) { console.error('[阅读标记] 初始化失败:', e); return false; } } // 注入样式 function injectStyles() { // 避免重复注入 if (document.getElementById(styleId)) { return; } const styleElement = document.createElement('style'); styleElement.id = styleId; styleElement.innerHTML = ` .${SCRIPT_ID}-overlay { position: absolute !important; background-color: rgba(255, 0, 0, 0.3) !important; pointer-events: none !important; z-index: 999999 !important; border-radius: 2px !important; transition: opacity 0.2s ease !important; box-shadow: 0 0 0 1px rgba(255, 0, 0, 0.2) !important; } /* 增强选中文本的视觉反馈 */ ::selection { background-color: rgba(255, 100, 100, 0.8) !important; color: inherit !important; } ::-moz-selection { background-color: rgba(255, 100, 100, 0.8) !important; color: inherit !important; } /* 确保在各种主题下都可见 */ .${SCRIPT_ID}-overlay.dark-mode { background-color: rgba(255, 100, 100, 0.4) !important; box-shadow: 0 0 0 1px rgba(255, 100, 100, 0.3) !important; } `; // 优先插入到head的最前面,提高优先级 if (document.head.firstChild) { document.head.insertBefore(styleElement, document.head.firstChild); } else { document.head.appendChild(styleElement); } } /** * 创建非侵入式的高亮覆盖层 */ function createHighlightOverlay(range) { if (!isScriptActive) return; try { // 移除之前的覆盖层 removeHighlightOverlay(); const rects = range.getClientRects(); if (rects.length === 0) return; // 检测是否为暗色主题 const isDarkMode = detectDarkMode(); // 为每个矩形区域创建覆盖层 const overlays = []; for (let i = 0; i < rects.length; i++) { const rect = rects[i]; if (rect.width === 0 || rect.height === 0) continue; const overlay = document.createElement('div'); overlay.className = `${SCRIPT_ID}-overlay` + (isDarkMode ? ' dark-mode' : ''); overlay.setAttribute('data-script', SCRIPT_ID); // 设置覆盖层位置和大小 overlay.style.cssText = ` position: absolute !important; left: ${rect.left + window.scrollX}px !important; top: ${rect.top + window.scrollY}px !important; width: ${rect.width}px !important; height: ${rect.height}px !important; background-color: ${isDarkMode ? 'rgba(255, 100, 100, 0.4)' : 'rgba(255, 0, 0, 0.3)'} !important; pointer-events: none !important; z-index: 999999 !important; border-radius: 2px !important; box-shadow: 0 0 0 1px ${isDarkMode ? 'rgba(255, 100, 100, 0.3)' : 'rgba(255, 0, 0, 0.2)'} !important; `; // 确保覆盖层不会被其他脚本意外移除 Object.defineProperty(overlay, 'remove', { value: function() { if (this.getAttribute('data-script') === SCRIPT_ID) { Element.prototype.remove.call(this); } } }); document.body.appendChild(overlay); overlays.push(overlay); } currentHighlightOverlay = overlays; return overlays; } catch (e) { console.error('[阅读标记] 创建覆盖层失败:', e); } } /** * 检测暗色主题 */ function detectDarkMode() { // 检查CSS媒体查询 if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { return true; } // 检查body背景色 const bodyStyle = window.getComputedStyle(document.body); const bgColor = bodyStyle.backgroundColor; if (bgColor && bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'transparent') { const rgb = bgColor.match(/\d+/g); if (rgb && rgb.length >= 3) { const brightness = (parseInt(rgb[0]) * 299 + parseInt(rgb[1]) * 587 + parseInt(rgb[2]) * 114) / 1000; return brightness < 128; } } // 检查常见的暗色主题类名 const darkModeClasses = ['dark', 'dark-mode', 'night-mode', 'theme-dark']; return darkModeClasses.some(className => document.documentElement.classList.contains(className) || document.body.classList.contains(className) ); } /** * 移除高亮覆盖层 */ function removeHighlightOverlay() { if (currentHighlightOverlay) { currentHighlightOverlay.forEach(overlay => { try { if (overlay.parentNode) { overlay.parentNode.removeChild(overlay); } } catch (e) { // 忽略移除失败的情况 } }); currentHighlightOverlay = null; } } /** * 保存阅读位置到localStorage */ function saveReadingPosition(selection) { if (!selection || selection.rangeCount === 0 || selection.isCollapsed) return; try { const range = selection.getRangeAt(0); const text = range.toString().trim(); if (text.length === 0) return; const selectionData = { anchorNodePath: getXPath(range.startContainer), anchorOffset: range.startOffset, focusNodePath: getXPath(range.endContainer), focusOffset: range.endOffset, text: text, url: window.location.href, timestamp: Date.now() }; const storageKey = `${SCRIPT_ID}_${window.location.href}`; localStorage.setItem(storageKey, JSON.stringify(selectionData)); console.log('[阅读标记] 位置已保存:', text.substring(0, 50) + '...'); } catch (e) { console.error('[阅读标记] 保存位置失败:', e); } } /** * 从localStorage加载并恢复阅读位置 */ function loadReadingPosition() { const storageKey = `${SCRIPT_ID}_${window.location.href}`; const savedPosition = localStorage.getItem(storageKey); if (!savedPosition) return; try { const selectionData = JSON.parse(savedPosition); // 检查数据完整性 if (!selectionData.anchorNodePath || !selectionData.focusNodePath) { console.warn('[阅读标记] 保存的数据不完整'); return; } const startNode = getElementByXPath(selectionData.anchorNodePath); const endNode = getElementByXPath(selectionData.focusNodePath); if (startNode && endNode) { const range = document.createRange(); range.setStart(startNode, selectionData.anchorOffset); range.setEnd(endNode, selectionData.focusOffset); // 验证恢复的内容是否匹配 const currentText = range.toString().trim(); if (currentText === selectionData.text) { // 创建持久化的高亮覆盖层 createHighlightOverlay(range); savedSelection = { range: range, data: selectionData }; // 自动滚动到高亮位置 scrollToHighlight(range); console.log('[阅读标记] 位置已恢复:', currentText.substring(0, 50) + '...'); } else { console.warn('[阅读标记] 文本内容已变更,清除过期数据'); localStorage.removeItem(storageKey); } } else { console.warn('[阅读标记] 无法找到保存的节点'); } } catch (e) { console.error('[阅读标记] 加载位置失败:', e); } } /** * 滚动到高亮位置 */ function scrollToHighlight(range) { try { const rect = range.getBoundingClientRect(); if (rect.top < 0 || rect.bottom > window.innerHeight) { const scrollTop = window.scrollY + rect.top - (window.innerHeight / 3); window.scrollTo({ top: Math.max(0, scrollTop), behavior: 'smooth' }); } } catch (e) { console.error('[阅读标记] 滚动失败:', e); } } /** * 获取DOM节点的XPath */ function getXPath(node) { if (!node || node.nodeType === Node.DOCUMENT_NODE) { return ''; } if (node.nodeType === Node.TEXT_NODE) { let index = 1; let sibling = node; while (sibling.previousSibling) { sibling = sibling.previousSibling; if (sibling.nodeType === Node.TEXT_NODE) { index++; } } return getXPath(node.parentNode) + `/text()[${index}]`; } const parts = []; let currentNode = node; while (currentNode && currentNode.nodeType !== Node.DOCUMENT_NODE) { let selector = currentNode.nodeName.toLowerCase(); // 优先使用ID if (currentNode.id) { selector += `[@id="${currentNode.id}"]`; parts.unshift(selector); break; // ID是唯一的,可以停止 } else { // 计算同名兄弟节点的位置 let sibling = currentNode; let nth = 1; while (sibling.previousSibling) { sibling = sibling.previousSibling; if (sibling.nodeType === Node.ELEMENT_NODE && sibling.nodeName.toLowerCase() === currentNode.nodeName.toLowerCase()) { nth++; } } if (nth > 1) { selector += `[${nth}]`; } } parts.unshift(selector); currentNode = currentNode.parentNode; } return parts.length ? '/' + parts.join('/') : ''; } /** * 根据XPath获取DOM节点 */ function getElementByXPath(path) { if (!path) return null; try { if (path.includes('/text()')) { const parts = path.split('/text()'); const elementPath = parts[0]; const textIndexMatch = parts[1].match(/\[(\d+)\]/); const textIndex = textIndexMatch ? parseInt(textIndexMatch[1]) : 1; const element = getElementByXPath(elementPath); if (element) { let textNodeCount = 0; for (let i = 0; i < element.childNodes.length; i++) { const child = element.childNodes[i]; if (child.nodeType === Node.TEXT_NODE) { textNodeCount++; if (textNodeCount === textIndex) { return child; } } } } return null; } else { const result = document.evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); return result.singleNodeValue; } } catch (e) { console.error('[阅读标记] XPath解析失败:', path, e); return null; } } /** * 更新覆盖层位置 */ function updateOverlayPositions() { if (!currentHighlightOverlay || !savedSelection || !isScriptActive) return; try { const range = savedSelection.range; const rects = range.getClientRects(); // 检查是否需要重新创建覆盖层 if (rects.length !== currentHighlightOverlay.length) { createHighlightOverlay(range); return; } currentHighlightOverlay.forEach((overlay, index) => { if (rects[index] && overlay.parentNode) { const rect = rects[index]; overlay.style.left = (rect.left + window.scrollX) + 'px'; overlay.style.top = (rect.top + window.scrollY) + 'px'; } }); } catch (e) { console.error('[阅读标记] 更新位置失败:', e); } } /** * 处理选区变化 */ function handleSelectionChange() { if (!isScriptActive) return; try { const selection = window.getSelection(); if (selection.rangeCount > 0 && !selection.isCollapsed) { const range = selection.getRangeAt(0); const text = range.toString().trim(); if (text.length > 0) { // 创建非侵入式高亮 createHighlightOverlay(range); saveReadingPosition(selection); // 保存当前选区信息 savedSelection = { range: range.cloneRange(), data: { text: text } }; } } } catch (e) { console.error('[阅读标记] 处理选区变化失败:', e); } } /** * 设置事件监听器 */ function setupEventListeners() { // 选区变化监听 let selectionTimeout; document.addEventListener('selectionchange', () => { clearTimeout(selectionTimeout); selectionTimeout = setTimeout(handleSelectionChange, 100); }, { passive: true }); // 滚动监听 - 使用节流 let scrollTimeout; let isScrolling = false; window.addEventListener('scroll', () => { if (!isScrolling) { window.requestAnimationFrame(() => { updateOverlayPositions(); isScrolling = false; }); isScrolling = true; } }, { passive: true }); // 窗口大小变化监听 window.addEventListener('resize', debounce(updateOverlayPositions, 250), { passive: true }); // 页面可见性变化监听 document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible' && currentHighlightOverlay) { setTimeout(updateOverlayPositions, 100); } }); // 双击清除高亮 document.addEventListener('dblclick', (e) => { if (currentHighlightOverlay && e.target.getAttribute('data-script') !== SCRIPT_ID) { removeHighlightOverlay(); savedSelection = null; const storageKey = `${SCRIPT_ID}_${window.location.href}`; localStorage.removeItem(storageKey); console.log('[阅读标记] 高亮已清除'); } }); // 页面卸载前清理 window.addEventListener('beforeunload', () => { isScriptActive = false; removeHighlightOverlay(); }); } /** * 防抖函数 */ function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // 初始化脚本 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeScript); } else { initializeScript(); } // 暴露清理函数供其他脚本调用 window.readingPositionMarker = { remove: removeHighlightOverlay, update: updateOverlayPositions, isActive: () => isScriptActive, disable: () => { isScriptActive = false; removeHighlightOverlay(); }, enable: () => { isScriptActive = true; } }; })();