Reading Ruler 阅读标尺

A reading ruler tool to help focus while reading, with duplicate prevention

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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();
    }
})();