Greasy Fork

Greasy Fork is available in English.

Reading Ruler 阅读标尺

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

当前为 2024-12-21 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

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