选中文本进行朗读,支持拖动、最小化、倍速、音调调整及重复播放(可自定义次数)
// ==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;">−</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 = '−'; // -符号
} else {
controlsDiv.style.display = 'none';
minimizeBtn.innerHTML = '+'; // +符号
}
});
// 使面板可拖动
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);
}
}
})();