起点听书/www.qidian.com

阅读界面右侧功能栏增加“听书”按钮、“语音”按钮。点击“听书”开始朗读:Esc-结束朗读;空格-暂定/继续;后台静默复制文章内容到剪贴板。点击“语音”按钮,打开设置页,可以调整语速。

  1. // ==UserScript==
  2. // @name 起点听书/www.qidian.com
  3. // @namespace yoursatan
  4. // @version 0.4
  5. // @description 阅读界面右侧功能栏增加“听书”按钮、“语音”按钮。点击“听书”开始朗读:Esc-结束朗读;空格-暂定/继续;后台静默复制文章内容到剪贴板。点击“语音”按钮,打开设置页,可以调整语速。
  6. // @author yorusatan
  7. // @include https://www.qidian.com/chapter*
  8. // @include https://read.qidian.com/chapter*
  9. // @grant none
  10. // @require https://code.jquery.com/jquery-2.1.4.min.js
  11. // @license MIT License
  12. // @require https://scriptcat.org/lib/513/2.0.1/ElementGetter.js#sha256=V0EUYIfbOrr63nT8+W7BP1xEmWcumTLWu2PXFJHh5dg=
  13. // ==/UserScript==
  14. // v0.4 增加“语音(设置)”按钮,可调整语速。
  15. // v0.3.1 小调整。
  16. // v0.3 修复功能,优化代码。
  17. // v0.2 修复一些使用中发现的bug。
  18. // v0.1 在阅读界面右侧功能栏添加“听书”按钮,点击“听书”开始朗读:Esc-结束朗读;空格-暂定/继续;后台静默复制文章内容到剪贴板。
  19. (async function () {
  20. "use strict";
  21. // 获取文章内容
  22. const str = await elmGetter.get("main");
  23. const storyContent = $(str).children("p");
  24. const storyTextArr = Array.from(storyContent).map((el) => el.textContent);
  25. const newStoryArr = [];
  26.  
  27. window.speechSynthesis.getVoices();
  28.  
  29. // 侧边栏添加 听书 按钮
  30. const rMenu = await elmGetter.get("#r-menu");
  31. const btnRead = `<div data-v-6cdbc58a data-v-47ffe1e class="tooltip-wrapper relative flex" style="margin-bottom:10px">
  32. <a id ="btnRead" data-v-47ffe1ec target="#" href="javascript">
  33. <button data-v-47ffe1ec class="w-64px h-64px flex flex-col items-center justify-center rounded-8px bg-sheet-b-gray-50 text-s-gray-900 noise-bg group hover:bg-sheet-b-bw-white hover:text-primary-red-500 hover:bg-none">
  34. <span class="icon-audio text-24px"></span>
  35. <span class="text-bo4 text-s-gray-500 mt-2px group-hover:text-primary-red-500" style="font-weight:600;">听书</span>
  36. </button></a><!----></div>`;
  37. const btnReadSet = `<div data-v-6cdbc58a data-v-47ffe1e class="tooltip-wrapper relative flex" style="margin-bottom:10px">
  38. <a id ="btnReadSet" data-v-47ffe1ec target="#" href="javascript">
  39. <button data-v-47ffe1ec class="w-64px h-64px flex flex-col items-center justify-center rounded-8px bg-sheet-b-gray-50 text-s-gray-900 noise-bg group hover:bg-sheet-b-bw-white hover:text-primary-red-500 hover:bg-none">
  40. <span class="icon-setting-bold text-24px"></span>
  41. <span class="text-bo4 text-s-gray-500 mt-2px group-hover:text-primary-red-500" style="font-weight:600;">语音</span>
  42. </button></a><!----></div>`;
  43. const readSetPage = `
  44. <section id="readSetPage" class="bg-sheet-b-bw-white shadow-sd16 w-480px pl-32px pt-42px pb-44px absolute right-full top-64px" hidden>
  45. <button class="bg-s-gray-100 w-28px h-28px rounded-1 flex items-center justify-center hover-24 active-10 p-0 absolute right-10px top-10px">
  46. <span class="icon-close text-20px text-s-gray-400"></span>
  47. </button>
  48. <div class="text-rh4 font-medium text-s-gray-900">设置</div>
  49. <div class="w-359px">
  50. <div class="flex items-center mt-32px"><span class="text-s4 font-medium text-s-gray-500 sm:pr-12px mr-16px flex-shrink-0">语速调整</span>
  51. <div class="flex flex-grow">
  52. <span class="text-s4 font-medium text-s-gray-500 ">0.8 </span>
  53. <input id="slider" type="range" value="1.25" min="0.8" max="2" step="0.1" style="width:270px;margin:0 5px">
  54. <span class="text-s4 font-medium text-s-gray-500 "> 2.0</span>
  55. </div>
  56. </div>
  57. <div hidden>
  58. <div class="flex items-center mt-20px"><span class="text-s4 font-medium text-s-gray-500 sm:pr-12px mr-16px flex-shrink-0" >选择语音</span>
  59. <div class="flex flex-grow" >
  60. <select aria-label="选择语音" class="text-s4 w-320px" id="voiceList" >
  61. </select>
  62. </div>
  63. </div>
  64. </div>
  65. </div>
  66. </section>
  67. `;
  68.  
  69. $(rMenu).prepend($(readSetPage)).prepend($(btnReadSet)).prepend($(btnRead));
  70.  
  71. setTimeout(() => {
  72. const voicesArr = window.speechSynthesis.getVoices();
  73. const voices = voicesArr.filter((item) => item.lang.includes("zh-"));
  74. let options = ``;
  75. for (i = 0; i < voices.length; i++) {
  76. options += `<option value="${voices[i].voiceURI}">${voices[i].name
  77. .replace("Microsoft ", "")
  78. .replace(" Chinese ", "")}</option>`;
  79. }
  80. $("#voiceList").append(options);
  81. $("#voiceList option:first").prop("selected", true);
  82. }, 100);
  83. let voice = "";
  84. $("#voiceList").change(function () {
  85. voice = $("#voiceList option:selected").prop("value");
  86. });
  87.  
  88. $("#btnReadSet").click(function () {
  89. event.preventDefault();
  90. $("#readSetPage").toggle();
  91. });
  92.  
  93. $(".icon-close").click(() => {
  94. $("#readSetPage").toggle();
  95. });
  96. $("#r-menu")
  97. .children()
  98. .first()
  99. .click(function () {
  100. event.preventDefault();
  101. });
  102. // 移除数组空项(文本空行)
  103. const countPara = storyTextArr.length;
  104. for (var i = 0; i < countPara; i++) {
  105. storyTextArr[i] = storyTextArr[i].replace(/\s+/g, " ").trim();
  106. if (storyTextArr[i] != "") {
  107. newStoryArr.push(storyTextArr[i]);
  108. }
  109. }
  110. const newCountPara = newStoryArr.length;
  111.  
  112. // 用于逐段朗读
  113. var flag = 0;
  114.  
  115. // 朗读
  116.  
  117. $("#btnRead").click(function () {
  118. event.preventDefault();
  119. // 朗读文字数组
  120. var storyAllRead = newStoryArr;
  121.  
  122. // 用于文字选中效果
  123. var range = document.createRange();
  124. var selection = window.getSelection();
  125.  
  126. if (window.speechSynthesis.speaking) {
  127. window.speechSynthesis.pause();
  128. }
  129. if (window.speechSynthesis.paused) {
  130. window.speechSynthesis.resume();
  131. }
  132. // 朗读
  133.  
  134. var readStory = function () {
  135. var speaker = new window.SpeechSynthesisUtterance();
  136.  
  137. speaker.rate = $("#slider").val();
  138. $("#slider").on("input", function () {
  139. speaker.rate = $(this).val();
  140. });
  141. speaker.lang = "zh-CN";
  142. speaker.pitch = 1.24;
  143. speaker.voiceURI =
  144. voice === "" ? "Microsoft Huihui - Chinese (Simplified, PRC)" : voice;
  145.  
  146. var reading = setInterval(function () {
  147. if (!window.speechSynthesis.speaking && flag <= newCountPara) {
  148. speaker.text = storyAllRead[flag];
  149. window.speechSynthesis.speak(speaker);
  150. flag += 1;
  151.  
  152. // 朗读段落文字选中效果
  153. var referenceNode = document
  154. .getElementsByTagName("main p")
  155. .item(flag - 1);
  156. // 起点网朗读效果,当前朗读段落文字变红
  157. $("main p")
  158. .eq(flag - 1)
  159. .css("color", "red");
  160. $("html,body").animate(
  161. {
  162. scrollTop:
  163. $("main p")
  164. .eq(flag - 1)
  165. .offset().top -
  166. document.documentElement.clientHeight * 0.382
  167. },
  168. 300 /*scroll实现定位滚动*/
  169. ); //代码参考,感谢:https://blog.csdn.net/qq_30109365/article/details/86592336
  170. if (flag - 1) {
  171. $("main p")
  172. .eq(flag - 2)
  173. .css("color", "black");
  174. }
  175. } else if (flag > newCountPara) {
  176. // 朗读结束
  177.  
  178. window.speechSynthesis.cancel();
  179. clearInterval(reading);
  180.  
  181. selection.removeAllRanges();
  182. flag = 0;
  183.  
  184. $("main p")
  185. .eq(flag - 1)
  186. .css("color", "black");
  187. $("main p").eq(flag).css("color", "black");
  188. alert("本章已读完。");
  189. }
  190. }, 300);
  191.  
  192. // 后台复制文章内容到剪贴板
  193. var copyStory = document.createElement("textarea"); //创建textarea对象
  194. copyStory.id = "copyArea";
  195. $("main").prepend(copyStory); //添加元素
  196.  
  197. var storyTitle = $(".title")
  198. .contents()
  199. .filter(function () {
  200. return this.nodeType === 3; // 过滤掉非文本节点
  201. })
  202. .text();
  203.  
  204. copyStory.value = storyTitle + "\n" + newStoryArr.join("\n"); // 组合文章标题
  205. copyStory.focus();
  206. if (copyStory.setSelectionRange) {
  207. copyStory.setSelectionRange(0, copyStory.value.length); //获取光标起始位置到结束位置
  208. } else {
  209. copyStory.select();
  210. }
  211. document.execCommand("Copy", "false", null); //执行复制
  212. if (document.execCommand("Copy", "false", null)) {
  213. console.log(
  214. "已复制文章到剪贴板!Success,The story has been copied to clipboard!--yoursatan"
  215. );
  216. }
  217. $("#copyArea").remove(); //删除元素
  218.  
  219. // 监听键盘:Esc/F5
  220. $(document).keyup(function (event) {
  221. if (event.keyCode == 27 || event.keyCode == 116) {
  222. window.speechSynthesis.cancel();
  223. clearInterval(reading);
  224. selection.removeAllRanges();
  225. if (
  226. // https://read.qidian.com/chapter/ 网站支持
  227. window.location.href.indexOf("https://read.qidian.com/chapter/") >
  228. -1 ||
  229. window.location.href.indexOf("https://www.qidian.com/chapter/") > -1
  230. ) {
  231. $("main p")
  232. .eq(flag - 1)
  233. .css("color", "black");
  234. }
  235. flag = 0;
  236. }
  237. });
  238.  
  239. // 监听键盘:空格键
  240. $(document).keypress(function (event) {
  241. event.preventDefault();
  242. if (event.keyCode == 32) {
  243. if (window.speechSynthesis.speaking) {
  244. window.speechSynthesis.pause();
  245. }
  246. if (window.speechSynthesis.paused) {
  247. window.speechSynthesis.resume();
  248. }
  249. }
  250. });
  251.  
  252. // 监听标签关闭事件
  253. window.onbeforeunload = function (e) {
  254. clearInterval(reading);
  255. window.speechSynthesis.cancel();
  256. };
  257. };
  258. readStory();
  259. });
  260. })();