// ==UserScript==
// @name Reading Ruler 阅读标尺
// @namespace http://tampermonkey.net/
// @version 0.3
// @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
// ==/UserScript==
(function() {
'use strict';
// 检查全局标识,防止重复初始化
if (window._readingRulerInitialized) {
console.log('Reading Ruler already initialized');
return;
}
// 检查已存在的元素
if (document.querySelector('.reading-ruler') || document.querySelector('.ruler-control')) {
console.log('Reading Ruler elements already exist, 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', {})
};
// 确保只在根文档中添加样式和元素
if (window.self !== window.top) {
console.log('Skip initialization in iframe');
return;
}
// 创建样式
const style = document.createElement('style');
style.textContent = `
.reading-ruler {
position: fixed;
left: 0;
width: 100%;
height: ${settings.height}px;
pointer-events: none;
z-index: 2147483646;
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: 2147483647;
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);
// 创建标尺和控制元素函数
function createRulerElements() {
// 创建标尺元素并添加到根元素
const ruler = document.createElement('div');
ruler.className = 'reading-ruler';
document.documentElement.appendChild(ruler);
// 创建控制面板并添加到根元素
const control = document.createElement('div');
control.className = 'ruler-control';
control.innerHTML = `
<button class="ruler-toggle" id="toggleRuler">📏</button>
<div class="ruler-settings">
<label>
高度 (px):
<input type="range" id="rulerHeight" min="10" max="100" value="${settings.height}">
<span id="heightValue">${settings.height}</span>px
</label>
<label>
颜色:
<input type="color" id="rulerColor" value="${settings.color}">
</label>
<label>
透明度:
<input type="range" id="rulerOpacity" min="0" max="100" value="${settings.opacity * 100}">
<span id="opacityValue">${Math.round(settings.opacity * 100)}</span>%
</label>
<div class="mode-switch">
<span>反色模式</span>
<label class="mode-switch-toggle">
<input type="checkbox" id="toggleMode" ${settings.isInverted ? 'checked' : ''}>
<span class="mode-switch-slider"></span>
</label>
</div>
</div>
`;
document.documentElement.appendChild(control);
return { ruler, control };
}
// 创建元素
const { ruler, control } = createRulerElements();
// 获取所有需要的元素
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: 2147483647;
font-size: 14px;
`;
notification.textContent = message;
document.documentElement.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
}
// 拖动相关函数
function dragStart(e) {
if (!e.target.closest('.ruler-toggle')) return;
e.preventDefault();
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 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;
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() {
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();
});
function throttle(func, limit) {
let lastFunc;
let lastRan;
return function(...args) {
if (!lastRan) {
func.apply(this, args);
lastRan = Date.now();
} else {
clearTimeout(lastFunc);
lastFunc = setTimeout(() => {
if ((Date.now() - lastRan) >= limit) {
func.apply(this, args);
lastRan = Date.now();
}
}, limit - (Date.now() - lastRan));
}
};
}
// 鼠标移动时更新标尺位置
document.addEventListener('mousemove', throttle((e) => {
if (settings.isEnabled) {
const y = e.clientY - (settings.height / 2);
ruler.style.top = `${y}px`;
}
}, 16)); // 16ms 大约相当于 60fps
// 监听窗口大小变化,调整设置面板位置
window.addEventListener('resize', () => {
if (settingsPanel.classList.contains('visible')) {
adjustSettingsPanelPosition();
}
});
// 添加 MutationObserver 以检测 DOM 变化
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
// 检查是否有重复的标尺元素
const rulers = document.querySelectorAll('.reading-ruler');
const controls = document.querySelectorAll('.ruler-control');
if (rulers.length > 1 || controls.length > 1) {
// 移除多余的元素
Array.from(rulers).slice(1).forEach(el => el.remove());
Array.from(controls).slice(1).forEach(el => el.remove());
}
}
});
});
// 观察整个文档的变化
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
// 初始化显示状态
if (settings.isEnabled) {
toggleButton.classList.add('active');
updateDisplayMode();
}
})();