浏览器文本阅读

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

  1. // ==UserScript==
  2. // @name 浏览器文本阅读
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.5
  5. // @description 选中文本进行朗读,支持拖动、最小化、倍速、音调调整及重复播放(可自定义次数)
  6. // @author Songmile
  7. // @match *://*/*
  8. // @grant none
  9. // @license MIT
  10. // ==/UserScript==
  11.  
  12. (function () {
  13. 'use strict';
  14.  
  15. const panel = document.createElement('div');
  16. panel.style.position = 'fixed';
  17. panel.style.bottom = '20px';
  18. panel.style.right = '20px';
  19. panel.style.background = 'rgba(30, 30, 30, 0.95)';
  20. panel.style.color = 'white';
  21. panel.style.padding = '15px';
  22. panel.style.borderRadius = '10px';
  23. panel.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.3)';
  24. panel.style.zIndex = '9999';
  25. panel.style.fontFamily = 'Arial, sans-serif';
  26. panel.style.width = '300px';
  27. panel.style.cursor = 'move';
  28. panel.innerHTML = `
  29. <div style="display: flex; justify-content: space-between; align-items: center;">
  30. <strong>文本朗读控制面板</strong>
  31. <button id="minimizeBtn" style="background: transparent; border: none; color: white; font-size: 18px; cursor: pointer;">&#x2212;</button>
  32. </div>
  33. <div id="controls" style="margin-top: 10px;">
  34. <button id="playBtn" style="margin: 5px; padding: 5px 10px; background-color: #28a745; border: none; border-radius: 5px; color: white; cursor: pointer;">播放</button>
  35. <button id="pauseBtn" style="margin: 5px; padding: 5px 10px; background-color: #ffc107; border: none; border-radius: 5px; color: white; cursor: pointer;">暂停</button>
  36. <button id="stopBtn" style="margin: 5px; padding: 5px 10px; background-color: #dc3545; border: none; border-radius: 5px; color: white; cursor: pointer;">停止</button>
  37. <button id="replayBtn" style="margin: 5px; padding: 5px 10px; background-color: #17a2b8; border: none; border-radius: 5px; color: white; cursor: pointer;">重复播放</button>
  38. <div style="margin-top: 10px;">
  39. <label for="repeatCount">重复次数: </label>
  40. <input type="number" id="repeatCount" min="1" max="100" value="1" style="width: 60px;">
  41. </div>
  42. <br>
  43. <div style="margin-top: 10px;">
  44. <label for="rate">语速: </label>
  45. <input type="range" id="rate" min="0.5" max="2" step="0.1" value="1">
  46. <span id="rateValue">1</span>x
  47. </div>
  48. <div style="margin-top: 10px;">
  49. <label for="pitch">音调: </label>
  50. <input type="range" id="pitch" min="0" max="2" step="0.1" value="1">
  51. <span id="pitchValue">1</span>
  52. </div>
  53. <br>
  54. <small>选中文本后点击播放或重复播放</small>
  55. </div>
  56. `;
  57. document.body.appendChild(panel);
  58.  
  59.  
  60. const playBtn = document.getElementById('playBtn');
  61. const pauseBtn = document.getElementById('pauseBtn');
  62. const stopBtn = document.getElementById('stopBtn');
  63. const replayBtn = document.getElementById('replayBtn');
  64. const minimizeBtn = document.getElementById('minimizeBtn');
  65. const controlsDiv = document.getElementById('controls');
  66. const rateSlider = document.getElementById('rate');
  67. const pitchSlider = document.getElementById('pitch');
  68. const rateValue = document.getElementById('rateValue');
  69. const pitchValue = document.getElementById('pitchValue');
  70. const repeatCountInput = document.getElementById('repeatCount');
  71.  
  72.  
  73. let utterance = null;
  74. let isPaused = false;
  75. let lastText = '';
  76. let repeatTimes = 1;
  77. let remainingRepeats = 1;
  78.  
  79.  
  80. rateSlider.addEventListener('input', () => {
  81. rateValue.textContent = rateSlider.value;
  82. if (utterance) {
  83. utterance.rate = parseFloat(rateSlider.value);
  84. }
  85. });
  86.  
  87.  
  88. pitchSlider.addEventListener('input', () => {
  89. pitchValue.textContent = pitchSlider.value;
  90. if (utterance) {
  91. utterance.pitch = parseFloat(pitchSlider.value);
  92. }
  93. });
  94.  
  95.  
  96. repeatCountInput.addEventListener('input', () => {
  97. let value = parseInt(repeatCountInput.value, 10);
  98. if (isNaN(value) || value < 1) {
  99. repeatCountInput.value = 1;
  100. value = 1;
  101. } else if (value > 100) {
  102. repeatCountInput.value = 100;
  103. value = 100;
  104. }
  105. repeatTimes = value;
  106. });
  107.  
  108. playBtn.addEventListener('click', () => {
  109. const selectedText = window.getSelection().toString().trim();
  110. if (selectedText) {
  111. lastText = selectedText;
  112. remainingRepeats = repeatTimes;
  113. startReading(selectedText);
  114. } else if (lastText) {
  115. // 如果没有选中文本,但有上一次播放的文本,则播放上一次的文本
  116. remainingRepeats = repeatTimes;
  117. startReading(lastText);
  118. } else {
  119. alert('请先选中文本再播放!');
  120. }
  121. });
  122.  
  123. // 重复播放
  124. replayBtn.addEventListener('click', () => {
  125. if (lastText) {
  126. remainingRepeats = repeatTimes;
  127. startReading(lastText);
  128. } else {
  129. alert('没有可重复播放的文本!');
  130. }
  131. });
  132.  
  133. // 暂停朗读
  134. pauseBtn.addEventListener('click', () => {
  135. if (speechSynthesis.speaking && !speechSynthesis.paused) {
  136. speechSynthesis.pause();
  137. isPaused = true;
  138. } else if (speechSynthesis.paused) {
  139. speechSynthesis.resume();
  140. isPaused = false;
  141. }
  142. });
  143.  
  144. // 停止朗读
  145. stopBtn.addEventListener('click', () => {
  146. if (speechSynthesis.speaking) {
  147. speechSynthesis.cancel();
  148. isPaused = false;
  149. utterance = null;
  150. }
  151. });
  152.  
  153. // 最小化面板
  154. minimizeBtn.addEventListener('click', () => {
  155. if (controlsDiv.style.display === 'none') {
  156. controlsDiv.style.display = 'block';
  157. minimizeBtn.innerHTML = '&#x2212;'; // -符号
  158. } else {
  159. controlsDiv.style.display = 'none';
  160. minimizeBtn.innerHTML = '&#x2b;'; // +符号
  161. }
  162. });
  163.  
  164. // 使面板可拖动
  165. let isDragging = false;
  166. let offsetX, offsetY;
  167.  
  168. panel.addEventListener('mousedown', (e) => {
  169. if (
  170. e.target.id === 'minimizeBtn' ||
  171. e.target.tagName === 'BUTTON' ||
  172. e.target.tagName === 'INPUT' ||
  173. e.target.tagName === 'LABEL' ||
  174. e.target.tagName === 'SPAN'
  175. ) {
  176. return; // 不触发拖动事件
  177. }
  178. isDragging = true;
  179. offsetX = e.clientX - panel.offsetLeft;
  180. offsetY = e.clientY - panel.offsetTop;
  181. panel.style.cursor = 'grabbing';
  182. });
  183.  
  184. document.addEventListener('mousemove', (e) => {
  185. if (isDragging) {
  186. panel.style.left = `${e.clientX - offsetX}px`;
  187. panel.style.top = `${e.clientY - offsetY}px`;
  188. panel.style.right = 'auto';
  189. panel.style.bottom = 'auto';
  190. }
  191. });
  192.  
  193. document.addEventListener('mouseup', () => {
  194. if (isDragging) {
  195. isDragging = false;
  196. panel.style.cursor = 'move';
  197. }
  198. });
  199.  
  200. // 开始朗读文本
  201. function startReading(text) {
  202. // 如果正在朗读,先停止
  203. if (speechSynthesis.speaking) {
  204. speechSynthesis.cancel();
  205. }
  206.  
  207. // 开始新的朗读
  208. utterance = new SpeechSynthesisUtterance(text);
  209. utterance.lang = 'zh-CN'; // 设置语言,可以改为 'en-US'
  210. utterance.rate = parseFloat(rateSlider.value); // 语速
  211. utterance.pitch = parseFloat(pitchSlider.value); // 音调
  212.  
  213. utterance.onend = function () {
  214. remainingRepeats--;
  215. if (remainingRepeats > 0) {
  216. // 使用 setTimeout 确保前一个朗读结束后再开始新的
  217. setTimeout(() => {
  218. startReading(text);
  219. }, 500);
  220. }
  221. };
  222.  
  223. speechSynthesis.speak(utterance);
  224. }
  225.  
  226.  
  227.  
  228. rateSlider.addEventListener('change', () => {
  229. if (speechSynthesis.speaking && !speechSynthesis.paused) {
  230. restartUtterance();
  231. }
  232. });
  233.  
  234. pitchSlider.addEventListener('change', () => {
  235. if (speechSynthesis.speaking && !speechSynthesis.paused) {
  236. restartUtterance();
  237. }
  238. });
  239.  
  240. function restartUtterance() {
  241. if (utterance) {
  242. const currentText = utterance.text;
  243. speechSynthesis.cancel();
  244. utterance = null;
  245. startReading(currentText);
  246. }
  247. }
  248.  
  249. })();