// ==UserScript== // @name 阅读位置标记 // @namespace your.namespace // @version 1.3 // @description 在安卓移动端(包括 Via 浏览器)上,选中文字并标记阅读位置,使用非侵入式高亮保持菜单可用。 // @match *://*/* // @grant none // @run-at document-idle // @downloadURL none // ==/UserScript== (function() { 'use strict'; const styleId = 'reading-highlight-style'; let currentHighlightOverlay = null; let savedSelection = null; // 注入CSS样式 if (!document.getElementById(styleId)) { const styleElement = document.createElement('style'); styleElement.id = styleId; styleElement.innerHTML = ` .reading-highlight-overlay { position: absolute; background-color: rgba(255, 0, 0, 0.3); pointer-events: none; z-index: 9999; border-radius: 2px; transition: opacity 0.2s ease; } /* 为选中文本添加额外的视觉反馈,不破坏DOM结构 */ ::selection { background-color: rgba(255, 100, 100, 0.8) !important; } ::-moz-selection { background-color: rgba(255, 100, 100, 0.8) !important; } `; document.head.appendChild(styleElement); } /** * 创建非侵入式的高亮覆盖层 */ function createHighlightOverlay(range) { // 移除之前的覆盖层 removeHighlightOverlay(); const rects = range.getClientRects(); if (rects.length === 0) return; // 为每个矩形区域创建覆盖层 const overlays = []; for (let i = 0; i < rects.length; i++) { const rect = rects[i]; const overlay = document.createElement('div'); overlay.className = 'reading-highlight-overlay'; // 设置覆盖层位置和大小 overlay.style.left = (rect.left + window.scrollX) + 'px'; overlay.style.top = (rect.top + window.scrollY) + 'px'; overlay.style.width = rect.width + 'px'; overlay.style.height = rect.height + 'px'; document.body.appendChild(overlay); overlays.push(overlay); } currentHighlightOverlay = overlays; return overlays; } /** * 移除高亮覆盖层 */ function removeHighlightOverlay() { if (currentHighlightOverlay) { currentHighlightOverlay.forEach(overlay => { if (overlay.parentNode) { overlay.parentNode.removeChild(overlay); } }); currentHighlightOverlay = null; } } /** * 保存阅读位置到localStorage */ function saveReadingPosition(selection) { if (!selection || selection.rangeCount === 0 || selection.isCollapsed) return; const range = selection.getRangeAt(0); const selectionData = { anchorNodePath: getXPath(range.startContainer), anchorOffset: range.startOffset, focusNodePath: getXPath(range.endContainer), focusOffset: range.endOffset, text: range.toString() // 保存文本内容用于验证 }; localStorage.setItem('readingPosition_' + window.location.href, JSON.stringify(selectionData)); console.log("阅读位置已保存:", selectionData); } /** * 从localStorage加载并恢复阅读位置 */ function loadReadingPosition() { const savedPosition = localStorage.getItem('readingPosition_' + window.location.href); if (savedPosition) { try { const selectionData = JSON.parse(savedPosition); const startNode = getElementByXPath(selectionData.anchorNodePath); const endNode = getElementByXPath(selectionData.focusNodePath); if (startNode && endNode) { try { const range = document.createRange(); range.setStart(startNode, selectionData.anchorOffset); range.setEnd(endNode, selectionData.focusOffset); // 验证恢复的内容是否匹配 if (range.toString() === selectionData.text) { // 创建持久化的高亮覆盖层(不依赖选区) createHighlightOverlay(range); savedSelection = { range: range, data: selectionData }; // 自动滚动到高亮位置 const rect = range.getBoundingClientRect(); if (rect.top < 0 || rect.bottom > window.innerHeight) { window.scrollBy({ top: rect.top - (window.innerHeight / 3), behavior: 'smooth' }); } console.log("阅读位置已加载并恢复。"); } else { console.warn("保存的文本内容已变更,跳过恢复。"); } } catch (e) { console.error("无法恢复选区:", e); } } else { console.warn("无法找到保存的节点,阅读位置可能已失效。"); } } 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(); if (currentNode.id) { selector += `[@id="${currentNode.id}"]`; } else { let sibling = currentNode; let nth = 1; while (sibling.previousSibling) { sibling = sibling.previousSibling; if (sibling.nodeName.toLowerCase() === selector) { 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) { const range = savedSelection.range; const rects = range.getClientRects(); currentHighlightOverlay.forEach((overlay, index) => { if (rects[index]) { const rect = rects[index]; overlay.style.left = (rect.left + window.scrollX) + 'px'; overlay.style.top = (rect.top + window.scrollY) + 'px'; } }); } } // 监听选区变化 let selectionTimeout; document.addEventListener('selectionchange', () => { clearTimeout(selectionTimeout); const selection = window.getSelection(); if (selection.rangeCount > 0 && !selection.isCollapsed) { // 短延迟确保选择完成 selectionTimeout = setTimeout(() => { const currentSelection = window.getSelection(); if (currentSelection.rangeCount > 0 && !currentSelection.isCollapsed) { const range = currentSelection.getRangeAt(0); // 创建非侵入式高亮,不影响选区 createHighlightOverlay(range); saveReadingPosition(currentSelection); // 保存当前选区信息 savedSelection = { range: range.cloneRange(), data: { text: range.toString() } }; } }, 100); // 很短的延迟,几乎感觉不到 } }); // 滚动时更新覆盖层位置 let scrollTimeout; window.addEventListener('scroll', () => { if (currentHighlightOverlay) { clearTimeout(scrollTimeout); scrollTimeout = setTimeout(updateOverlayPositions, 16); // ~60fps } }); // 窗口大小改变时更新覆盖层 window.addEventListener('resize', () => { if (currentHighlightOverlay && savedSelection) { updateOverlayPositions(); } }); // 页面加载完成后尝试恢复阅读位置 window.addEventListener('load', loadReadingPosition); // 点击其他地方时清除高亮(可选) document.addEventListener('click', (e) => { const selection = window.getSelection(); if (selection.isCollapsed && !e.target.closest('.reading-highlight-overlay')) { // 可以选择是否在点击时清除高亮 // removeHighlightOverlay(); // 取消注释可在点击时清除高亮 // savedSelection = null; } }); // 双击高亮区域可以清除高亮 document.addEventListener('dblclick', (e) => { if (currentHighlightOverlay) { removeHighlightOverlay(); savedSelection = null; // 同时清除localStorage中的记录 localStorage.removeItem('readingPosition_' + window.location.href); console.log("高亮已清除"); } }); })();