// ==UserScript== // @name Reading Ruler 阅读标尺 // @namespace http://tampermonkey.net/ // @version 0.2 // @description A reading ruler tool to help focus while reading, with duplicate prevention // @author lumos momo // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @license GPL-3.0-or-later // @downloadURL none // ==/UserScript== (function() { 'use strict'; // 检查是否已经存在阅读标尺实例 if (document.querySelector('.reading-ruler') || document.querySelector('.ruler-control')) { console.log('Reading Ruler already exists, preventing duplicate initialization'); return; } // 添加一个标识,表明此实例已经运行 window._readingRulerInitialized = true; // 默认设置 const defaultSettings = { height: 30, // 标尺高度 color: '#ffeb3b', // 标尺颜色 opacity: 0.3, // 标尺透明度 isEnabled: false, // 是否启用标尺 isInverted: false, // 是否使用反色模式 position: { x: 20, y: '50%' } // 控制按钮位置 }; // 从存储中获取保存的设置,如果没有则使用默认设置 let settings = { ...defaultSettings, ...GM_getValue('rulerSettings', {}) }; // 创建样式 const style = document.createElement('style'); style.textContent = ` .reading-ruler { position: fixed; left: 0; width: 100%; height: ${settings.height}px; pointer-events: none; z-index: 9999; transition: top 0.1s ease; display: none; } .reading-ruler.normal { background-color: ${settings.color}; opacity: ${settings.opacity}; } .reading-ruler.inverted { background-color: transparent; box-shadow: 0 0 0 100vh ${settings.color}; position: fixed; left: 0; right: 0; width: 100%; } .ruler-control { position: fixed; left: ${settings.position.x}px; top: ${settings.position.y}; transform: translateY(-50%); z-index: 10000; cursor: move; user-select: none; } .ruler-toggle { width: 48px; height: 48px; border-radius: 50%; background: white; border: none; box-shadow: 0 2px 5px rgba(0,0,0,0.2); cursor: pointer; display: flex; align-items: center; justify-content: center; transition: background-color 0.3s; font-size: 20px; font-weight: bold; color: #666; } .ruler-toggle:hover { background-color: #f5f5f5; } .ruler-toggle.active { background-color: #e3f2fd; color: #2196f3; } .ruler-settings { position: absolute; background: white; border-radius: 4px; padding: 15px; box-shadow: 0 2px 5px rgba(0,0,0,0.2); display: none; width: 200px; max-height: 90vh; overflow-y: auto; } .ruler-settings.visible { display: block; } .ruler-settings.right { left: 100%; margin-left: 10px; } .ruler-settings.left { right: 100%; margin-right: 10px; } .ruler-settings.top { bottom: 100%; margin-bottom: 10px; } .ruler-settings.bottom { top: 100%; margin-top: 10px; } .ruler-settings label { display: block; margin: 10px 0; font-size: 14px; } .ruler-settings input { width: 100%; margin-top: 5px; } .ruler-settings .mode-switch { display: flex; align-items: center; margin: 10px 0; padding: 8px 0; border-top: 1px solid #eee; } .ruler-settings .mode-switch span { flex-grow: 1; font-size: 14px; } .mode-switch-toggle { position: relative; display: inline-block; width: 40px; height: 20px; } .mode-switch-toggle input { opacity: 0; width: 0; height: 0; } .mode-switch-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 20px; } .mode-switch-slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 2px; bottom: 2px; background-color: white; transition: .4s; border-radius: 50%; } .mode-switch-toggle input:checked + .mode-switch-slider { background-color: #2196F3; } .mode-switch-toggle input:checked + .mode-switch-slider:before { transform: translateX(20px); } `; document.head.appendChild(style); // 创建标尺元素 const ruler = document.createElement('div'); ruler.className = 'reading-ruler'; document.body.appendChild(ruler); // 创建控制面板 const control = document.createElement('div'); control.className = 'ruler-control'; control.innerHTML = `
反色模式
`; document.body.appendChild(control); // 获取所有需要的元素 const toggleButton = document.getElementById('toggleRuler'); const modeSwitch = document.getElementById('toggleMode'); const settingsPanel = control.querySelector('.ruler-settings'); // 设置面板位置调整函数 function adjustSettingsPanelPosition() { const controlRect = control.getBoundingClientRect(); const settingsRect = settingsPanel.getBoundingClientRect(); settingsPanel.classList.remove('right', 'left', 'top', 'bottom'); if (controlRect.right + settingsRect.width + 10 <= window.innerWidth) { settingsPanel.classList.add('right'); } else if (controlRect.left - settingsRect.width - 10 >= 0) { settingsPanel.classList.add('left'); } else if (controlRect.bottom + settingsRect.height + 10 <= window.innerHeight) { settingsPanel.classList.add('bottom'); } else { settingsPanel.classList.add('top'); } } // 拖拽状态管理 let dragState = { isDragging: false, startX: 0, startY: 0, startPosX: 0, startPosY: 0 }; // 显示通知提示 function showNotification(message) { const notification = document.createElement('div'); notification.style.cssText = ` position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background: rgba(0, 0, 0, 0.8); color: white; padding: 10px 20px; border-radius: 4px; z-index: 10001; font-size: 14px; `; notification.textContent = message; document.body.appendChild(notification); setTimeout(() => { notification.remove(); }, 3000); } // 拖动相关函数 function dragStart(e) { if (!e.target.closest('.ruler-toggle')) return; e.preventDefault(); const control = document.querySelector('.ruler-control'); const rect = control.getBoundingClientRect(); dragState.isDragging = true; dragState.startX = e.clientX; dragState.startY = e.clientY; dragState.startPosX = rect.left; dragState.startPosY = rect.top; control.style.transition = 'none'; control.style.transform = 'none'; settingsPanel.classList.remove('visible'); } function drag(e) { if (!dragState.isDragging) return; e.preventDefault(); const control = document.querySelector('.ruler-control'); const deltaX = e.clientX - dragState.startX; const deltaY = e.clientY - dragState.startY; let newX = Math.max(0, Math.min(window.innerWidth - control.offsetWidth, dragState.startPosX + deltaX)); let newY = Math.max(0, Math.min(window.innerHeight - control.offsetHeight, dragState.startPosY + deltaY)); control.style.left = `${newX}px`; control.style.top = `${newY}px`; } function dragEnd(e) { if (!dragState.isDragging) return; const control = document.querySelector('.ruler-control'); dragState.isDragging = false; settings.position = { x: parseInt(control.style.left), y: control.style.top }; saveSettings(); control.style.transition = ''; } // 设置相关函数 function updateSettingsDisplay() { document.getElementById('heightValue').textContent = settings.height; document.getElementById('opacityValue').textContent = Math.round(settings.opacity * 100); ruler.style.height = `${settings.height}px`; updateRulerMode(); } function updateRulerMode() { ruler.className = 'reading-ruler ' + (settings.isInverted ? 'inverted' : 'normal'); if (!settings.isInverted) { ruler.style.backgroundColor = settings.color; ruler.style.opacity = settings.opacity; ruler.style.boxShadow = ''; } else { ruler.style.backgroundColor = 'transparent'; ruler.style.boxShadow = `0 0 0 100vh ${settings.color}`; ruler.style.opacity = settings.opacity; } } function saveSettings() { GM_setValue('rulerSettings', settings); } function updateDisplayMode() { ruler.style.display = settings.isEnabled ? 'block' : 'none'; updateRulerMode(); } function resetControlPosition() { const control = document.querySelector('.ruler-control'); if (control) { control.style.left = defaultSettings.position.x + 'px'; control.style.top = defaultSettings.position.y; control.style.transform = 'translateY(-50%)'; settings.position = { x: defaultSettings.position.x, y: defaultSettings.position.y }; saveSettings(); showNotification('按钮位置已重置'); } } // 注册油猴脚本菜单命令 GM_registerMenuCommand("打开设置面板", () => { settingsPanel.classList.add('visible'); adjustSettingsPanelPosition(); }); GM_registerMenuCommand("重置按钮位置", resetControlPosition); // 事件监听器设置 toggleButton.addEventListener('click', () => { settings.isEnabled = !settings.isEnabled; toggleButton.classList.toggle('active', settings.isEnabled); updateDisplayMode(); saveSettings(); }); modeSwitch.addEventListener('change', (e) => { settings.isInverted = e.target.checked; updateDisplayMode(); saveSettings(); }); toggleButton.addEventListener('contextmenu', (e) => { e.preventDefault(); settingsPanel.classList.toggle('visible'); if (settingsPanel.classList.contains('visible')) { adjustSettingsPanelPosition(); } }); document.addEventListener('click', (e) => { if (!e.target.closest('.ruler-settings') && !e.target.closest('.ruler-toggle')) { settingsPanel.classList.remove('visible'); } }); // 拖动事件监听 control.addEventListener("mousedown", dragStart); document.addEventListener("mousemove", drag); document.addEventListener("mouseup", dragEnd); // 防止拖动时选中文本 control.addEventListener('selectstart', (e) => { if (dragState.isDragging) { e.preventDefault(); } }); // 设置面板事件监听 document.getElementById('rulerHeight').addEventListener('input', (e) => { settings.height = parseInt(e.target.value); updateSettingsDisplay(); saveSettings(); }); document.getElementById('rulerColor').addEventListener('input', (e) => { settings.color = e.target.value; updateSettingsDisplay(); saveSettings(); }); document.getElementById('rulerOpacity').addEventListener('input', (e) => { settings.opacity = parseInt(e.target.value) / 100; updateSettingsDisplay(); saveSettings(); }); // 鼠标移动时更新标尺位置 document.addEventListener('mousemove', (e) => { if (settings.isEnabled) { const y = e.clientY - (settings.height / 2); ruler.style.top = `${y}px`; } }); // 监听窗口大小变化,调整设置面板位置 window.addEventListener('resize', () => { if (settingsPanel.classList.contains('visible')) { adjustSettingsPanelPosition(); } }); // 初始化显示状态 if (settings.isEnabled) { toggleButton.classList.add('active'); updateDisplayMode(); } })();