Greasy Fork

Greasy Fork is available in English.

浏览器文本阅读

选中文本进行朗读,支持拖动、最小化、倍速、音调调整及重复播放(可自定义次数)

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

You will need to install an extension such as Tampermonkey to install this script.

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         浏览器文本阅读
// @namespace    http://tampermonkey.net/
// @version      2.5
// @description  选中文本进行朗读,支持拖动、最小化、倍速、音调调整及重复播放(可自定义次数)
// @author       Songmile
// @match        *://*/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

 
    const panel = document.createElement('div');
    panel.style.position = 'fixed';
    panel.style.bottom = '20px';
    panel.style.right = '20px';
    panel.style.background = 'rgba(30, 30, 30, 0.95)';
    panel.style.color = 'white';
    panel.style.padding = '15px';
    panel.style.borderRadius = '10px';
    panel.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.3)';
    panel.style.zIndex = '9999';
    panel.style.fontFamily = 'Arial, sans-serif';
    panel.style.width = '300px';
    panel.style.cursor = 'move'; 
    panel.innerHTML = `
        <div style="display: flex; justify-content: space-between; align-items: center;">
            <strong>文本朗读控制面板</strong>
            <button id="minimizeBtn" style="background: transparent; border: none; color: white; font-size: 18px; cursor: pointer;">&#x2212;</button>
        </div>
        <div id="controls" style="margin-top: 10px;">
            <button id="playBtn" style="margin: 5px; padding: 5px 10px; background-color: #28a745; border: none; border-radius: 5px; color: white; cursor: pointer;">播放</button>
            <button id="pauseBtn" style="margin: 5px; padding: 5px 10px; background-color: #ffc107; border: none; border-radius: 5px; color: white; cursor: pointer;">暂停</button>
            <button id="stopBtn" style="margin: 5px; padding: 5px 10px; background-color: #dc3545; border: none; border-radius: 5px; color: white; cursor: pointer;">停止</button>
            <button id="replayBtn" style="margin: 5px; padding: 5px 10px; background-color: #17a2b8; border: none; border-radius: 5px; color: white; cursor: pointer;">重复播放</button>
            <div style="margin-top: 10px;">
                <label for="repeatCount">重复次数: </label>
                <input type="number" id="repeatCount" min="1" max="100" value="1" style="width: 60px;">
            </div>
            <br>
            <div style="margin-top: 10px;">
                <label for="rate">语速: </label>
                <input type="range" id="rate" min="0.5" max="2" step="0.1" value="1">
                <span id="rateValue">1</span>x
            </div>
            <div style="margin-top: 10px;">
                <label for="pitch">音调: </label>
                <input type="range" id="pitch" min="0" max="2" step="0.1" value="1">
                <span id="pitchValue">1</span>
            </div>
            <br>
            <small>选中文本后点击播放或重复播放</small>
        </div>
    `;
    document.body.appendChild(panel);


    const playBtn = document.getElementById('playBtn');
    const pauseBtn = document.getElementById('pauseBtn');
    const stopBtn = document.getElementById('stopBtn');
    const replayBtn = document.getElementById('replayBtn');
    const minimizeBtn = document.getElementById('minimizeBtn');
    const controlsDiv = document.getElementById('controls');
    const rateSlider = document.getElementById('rate');
    const pitchSlider = document.getElementById('pitch');
    const rateValue = document.getElementById('rateValue');
    const pitchValue = document.getElementById('pitchValue');
    const repeatCountInput = document.getElementById('repeatCount');


    let utterance = null;
    let isPaused = false;
    let lastText = ''; 
    let repeatTimes = 1;
    let remainingRepeats = 1;


    rateSlider.addEventListener('input', () => {
        rateValue.textContent = rateSlider.value;
        if (utterance) {
            utterance.rate = parseFloat(rateSlider.value);
        }
    });


    pitchSlider.addEventListener('input', () => {
        pitchValue.textContent = pitchSlider.value;
        if (utterance) {
            utterance.pitch = parseFloat(pitchSlider.value);
        }
    });


    repeatCountInput.addEventListener('input', () => {
        let value = parseInt(repeatCountInput.value, 10);
        if (isNaN(value) || value < 1) {
            repeatCountInput.value = 1;
            value = 1;
        } else if (value > 100) { 
            repeatCountInput.value = 100;
            value = 100;
        }
        repeatTimes = value;
    });

 
    playBtn.addEventListener('click', () => {
        const selectedText = window.getSelection().toString().trim();
        if (selectedText) {
            lastText = selectedText; 
            remainingRepeats = repeatTimes;
            startReading(selectedText);
        } else if (lastText) {
            // 如果没有选中文本,但有上一次播放的文本,则播放上一次的文本
            remainingRepeats = repeatTimes;
            startReading(lastText);
        } else {
            alert('请先选中文本再播放!');
        }
    });

    // 重复播放
    replayBtn.addEventListener('click', () => {
        if (lastText) {
            remainingRepeats = repeatTimes;
            startReading(lastText);
        } else {
            alert('没有可重复播放的文本!');
        }
    });

    // 暂停朗读
    pauseBtn.addEventListener('click', () => {
        if (speechSynthesis.speaking && !speechSynthesis.paused) {
            speechSynthesis.pause();
            isPaused = true;
        } else if (speechSynthesis.paused) {
            speechSynthesis.resume();
            isPaused = false;
        }
    });

    // 停止朗读
    stopBtn.addEventListener('click', () => {
        if (speechSynthesis.speaking) {
            speechSynthesis.cancel();
            isPaused = false;
            utterance = null;
        }
    });

    // 最小化面板
    minimizeBtn.addEventListener('click', () => {
        if (controlsDiv.style.display === 'none') {
            controlsDiv.style.display = 'block';
            minimizeBtn.innerHTML = '&#x2212;'; // -符号
        } else {
            controlsDiv.style.display = 'none';
            minimizeBtn.innerHTML = '&#x2b;'; // +符号
        }
    });

    // 使面板可拖动
    let isDragging = false;
    let offsetX, offsetY;

    panel.addEventListener('mousedown', (e) => {
        if (
            e.target.id === 'minimizeBtn' ||
            e.target.tagName === 'BUTTON' ||
            e.target.tagName === 'INPUT' ||
            e.target.tagName === 'LABEL' ||
            e.target.tagName === 'SPAN'
        ) {
            return; // 不触发拖动事件
        }
        isDragging = true;
        offsetX = e.clientX - panel.offsetLeft;
        offsetY = e.clientY - panel.offsetTop;
        panel.style.cursor = 'grabbing';
    });

    document.addEventListener('mousemove', (e) => {
        if (isDragging) {
            panel.style.left = `${e.clientX - offsetX}px`;
            panel.style.top = `${e.clientY - offsetY}px`;
            panel.style.right = 'auto';
            panel.style.bottom = 'auto';
        }
    });

    document.addEventListener('mouseup', () => {
        if (isDragging) {
            isDragging = false;
            panel.style.cursor = 'move';
        }
    });

    // 开始朗读文本
    function startReading(text) {
        // 如果正在朗读,先停止
        if (speechSynthesis.speaking) {
            speechSynthesis.cancel();
        }

        // 开始新的朗读
        utterance = new SpeechSynthesisUtterance(text);
        utterance.lang = 'zh-CN'; // 设置语言,可以改为 'en-US'
        utterance.rate = parseFloat(rateSlider.value); // 语速
        utterance.pitch = parseFloat(pitchSlider.value); // 音调

        utterance.onend = function () {
            remainingRepeats--;
            if (remainingRepeats > 0) {
                // 使用 setTimeout 确保前一个朗读结束后再开始新的
                setTimeout(() => {
                    startReading(text);
                }, 500);
            }
        };

        speechSynthesis.speak(utterance);
    }



    rateSlider.addEventListener('change', () => {
        if (speechSynthesis.speaking && !speechSynthesis.paused) {
            restartUtterance();
        }
    });

    pitchSlider.addEventListener('change', () => {
        if (speechSynthesis.speaking && !speechSynthesis.paused) {
            restartUtterance();
        }
    });

    function restartUtterance() {
        if (utterance) {
            const currentText = utterance.text;
            speechSynthesis.cancel();
            utterance = null;
            startReading(currentText);
        }
    }

})();