通用阅读器

为所有站点增加进入阅读模式按钮,点击后如果匹配成功则自动转码成为通用的阅读器样式方便使用,并提供扩展语音阅读功能(需要浏览器支持,推荐最新版Firefox浏览器)

  1. // ==UserScript==
  2. // @name 通用阅读器
  3. // @version 0.4.5
  4. // @description 为所有站点增加进入阅读模式按钮,点击后如果匹配成功则自动转码成为通用的阅读器样式方便使用,并提供扩展语音阅读功能(需要浏览器支持,推荐最新版Firefox浏览器)
  5. // @author pppploi8
  6. // @match https://*/*
  7. // @match http://*/*
  8. // @grant none
  9. // @namespace https://gf.qytechs.cn/users/240492
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. var $ = function(selector){
  14. return document.querySelector(selector);
  15. }
  16.  
  17. var blackList = {"www.baidu.com": true, "www.sogou.com": true, "www.so.com": true, "www.bing.com":true, "cn.bing.com": true, "www.google.com": true, "m.sm.cn": true};
  18.  
  19. // 通用解析模板
  20. function parseContentAndTitle(){
  21. var mainDom = null;
  22.  
  23. function findMainDom(doms){
  24. var docSize = document.body.scrollHeight * document.body.scrollWidth;
  25. for(var i=0;i<doms.length;i++){
  26. var dom = doms[i];
  27. // 计算dom尺寸百分比并输出
  28. var size = (dom.scrollWidth * dom.scrollHeight) / docSize;
  29. if (size > 0.3 && dom.nodeName !== "CODE" && dom.nodeName !== "PRE"){ // pre和code是代码常用元素块,忽略不认为是正文内容
  30. // 如果innerText字数大于500,则视为正文区
  31. var text = dom.innerText;
  32. if (text.length >= 500){
  33. mainDom = dom;
  34. }
  35. }
  36. findMainDom(dom.children||[]);
  37. }
  38. }
  39. findMainDom(document.body.children);
  40.  
  41. if (mainDom){
  42. // 在mainDom同层查找标题
  43. // var titleList = [];
  44. // findTitleDom(mainDom.parentNode.children, titleList);
  45. // 查找标题逻辑bug较多,暂时改为查找网页title作为标题,后续重写逻辑进行优化
  46. return {content: mainDom.innerText, title: document.title};
  47. }
  48.  
  49. // function findTitleDom(doms, titleList){
  50. // for(var i=0;i<doms.length;i++){
  51. // var dom = doms[i];
  52. // if (dom.children && dom.children.length !== 0){
  53. // var title = findTitleDom(dom.children, titleList);
  54. // }else{
  55. // if (/\<h1|\<h2|title/.test(dom.outerHTML)){
  56. // var title = dom.innerText;
  57. // if (title && title.length >= 5 && title.length <= 200){
  58. // titleList.push(dom);
  59. // }
  60. // }
  61. // }
  62. // }
  63. // }
  64. }
  65.  
  66. function parsePageUp(){
  67. var as = document.querySelectorAll('a');
  68. var reg = /上一章|上一篇|上一页|navigation-prev/;
  69. for(var i=0;i<as.length;i++){
  70. var text = as[i].outerHTML;
  71. var href = (as[i].attributes.href && as[i].attributes.href.value) || (as[i].dataset && as[i].dataset.url);
  72. if (text && reg.test(text.trim()) && href && href != "#" && href.indexOf("javascript:") !== 0){
  73. return href;
  74. }
  75. }
  76. }
  77.  
  78. function parsePageDown(){
  79. var as = document.querySelectorAll('a');
  80. var reg = /下一章|下一篇|下一页|navigation-next/;
  81. for(var i=0;i<as.length;i++){
  82. var text = as[i].outerHTML;
  83. var href = (as[i].attributes.href && as[i].attributes.href.value) || (as[i].dataset && as[i].dataset.url);
  84. if (text && reg.test(text.trim()) && href && href != "#" && href.indexOf("javascript:") !== 0){
  85. return href;
  86. }
  87. }
  88. }
  89.  
  90. function parsePageIndex(){
  91. var as = document.querySelectorAll('a');
  92. var reg = /目录/;
  93. for(var i=0;i<as.length;i++){
  94. var text = as[i].innerText;
  95. var href = as[i].attributes.href && as[i].attributes.href.value;
  96. if (text && text.length <= 10 && reg.test(text.trim()) && href && href != "#" && href.indexOf("javascript:") !== 0){
  97. return href;
  98. }
  99. }
  100. }
  101.  
  102.  
  103. var fontsize = parseInt(localStorage["_er_fontsize"] || 0);
  104. var padding = parseInt(localStorage["_er_padding"] || 10);
  105. var autoplay = false;
  106. if (localStorage['_er-autoplay'] === 'true'){
  107. autoplay = true;
  108. }
  109. delete localStorage['_er-autoplay'];
  110.  
  111. if (top.window !== window) return; // iframe内的网页不展示按钮,也不支持进入阅读模式
  112. if (localStorage['_er-enable'] === 'true'){
  113. localStorage['_er-enable'] = 'false';
  114. checkAndCreateReader(true);
  115. } else if (blackList[location.host] !== true){
  116. // 创建阅读模式悬浮按钮
  117. $('body').children[0].insertAdjacentHTML('beforeBegin', '<button id="_er-entryReadMode" style="' +
  118. ' position: fixed;' +
  119. ' right: 50px;' +
  120. ' bottom: 50px;' +
  121. ' background-color: white;' +
  122. ' border: 1px solid black;' +
  123. ' border-radius: 10px;' +
  124. ' padding: 0 5px;' +
  125. ' height: 50px;' +
  126. ' overflow: auto;' +
  127. ' background-color: white;' +
  128. ' z-index: 201901272210;">进入阅读模式</button>');
  129. // 配置阅读模式按钮自动淡出效果
  130. var i = 1;
  131. var interval = setInterval(function(){
  132. i -= 0.03;
  133. var btn = $('#_er-entryReadMode');
  134. if (!btn) {
  135. clearInterval(interval);
  136. return;
  137. }
  138. $('#_er-entryReadMode').style.opacity = i;
  139. if (i <= 0) {
  140. btn.remove();
  141. clearInterval(interval);
  142. }
  143. }, 100);
  144. $('#_er-entryReadMode').onclick = checkAndCreateReader;
  145. $('#_er-NotShowReadMode').onclick = function(){
  146. localStorage['_er-disabled'] = 'true';
  147. $('#_er-entryReadMode').remove();
  148. $('#_er-NotShowReadMode').remove();
  149. }
  150. }
  151.  
  152. function checkAndCreateReader(notAlert){
  153. // 通过调用通用模板尝试是否能够成功匹配到阅读内容
  154. var content = parseContentAndTitle();
  155. if (content && content.content){
  156. content.pageup = parsePageUp();
  157. content.pagedown = parsePageDown();
  158. content.pageindex = parsePageIndex();
  159. createReader(content);
  160. }else{
  161. if (notAlert !== true){
  162. alert('当前页面解析失败,无法进入阅读模式!');
  163. }
  164. }
  165. }
  166.  
  167. function setTheme(theme) {
  168. switch(theme) {
  169. case 'black':
  170. $('._er').style.backgroundColor = 'black';
  171. $('._er-title').style.color = 'lightgrey';
  172. $('._er-content').style.color = 'lightgrey';
  173. break;
  174. case 'OliveDrab':
  175. $('._er').style.backgroundColor = '#D3E1D0';
  176. $('._er-title').style.color = 'black';
  177. $('._er-content').style.color = 'black';
  178. break;
  179. case 'Khaki':
  180. $('._er').style.backgroundColor = '#F6F2E7';
  181. $('._er-title').style.color = 'black';
  182. $('._er-content').style.color = 'black';
  183. break;
  184. case 'blue':
  185. $('._er').style.backgroundColor = '#D3E5F9';
  186. $('._er-title').style.color = 'black';
  187. $('._er-content').style.color = 'black';
  188. break;
  189. case 'white':
  190. $('._er').style.backgroundColor = 'white';
  191. $('._er-title').style.color = 'black';
  192. $('._er-content').style.color = 'black';
  193. break;
  194. }
  195. localStorage['_er-theme'] = theme;
  196. $('._er').dataset['theme'] = theme;
  197. }
  198.  
  199. // 创建阅读器
  200. function createReader(content){
  201. $('#_er-entryReadMode') && $('#_er-entryReadMode').remove();
  202. $('#_er-NotShowReadMode') && $('#_er-NotShowReadMode').remove();
  203. addClassAndDom();
  204. if (window.SpeechSynthesisUtterance){
  205. $('#_er-tts').style.display = 'block';
  206. }
  207. if (localStorage['_er-theme']) {
  208. setTheme(localStorage['_er-theme']);
  209. }
  210. $('._er-title').innerText = content.title;
  211. var contentArr = content.content.split('\n');
  212. var contentHtml = '';
  213. for(var i=0;i<contentArr.length;i++){
  214. var line = contentArr[i];
  215. if (line){
  216. contentHtml += '<span>' + line + '</span>';
  217. }
  218. contentHtml += '<br>';
  219. }
  220. $('._er-content').innerHTML = contentHtml;
  221. var spanNodes = document.querySelectorAll('._er-content span');
  222. for(var i=0;i<spanNodes.length;i++){
  223. spanNodes[i].onclick = function(){
  224. for(var j=0;j<spanNodes.length;j++){
  225. spanNodes[j].classList.remove('_er-current');
  226. }
  227. this.classList.add('_er-current');
  228. }
  229. }
  230. // 挂接键盘事件,实现键盘上下左右切换阅读功能
  231. $('body').onkeydown = function(e){
  232. e.stopPropagation();
  233. switch(e.keyCode || e.which || e.charCode){
  234. case 38: // up
  235. if (e.ctrlKey) {
  236. $('._er').scrollTop = $('._er').scrollTop - (document.documentElement.clientHeight - 24)
  237. } else {
  238. toPrevReadPos();
  239. updateReadPos();
  240. }
  241. break;
  242. case 40: // down
  243. if (e.ctrlKey) {
  244. $('._er').scrollTop = $('._er').scrollTop + (document.documentElement.clientHeight - 24);
  245. } else {
  246. toNextReadPos();
  247. updateReadPos();
  248. }
  249. break;
  250. case 37: // left
  251. if (e.ctrlKey) {
  252. toPrevPage();
  253. } else {
  254. $('._er').scrollTop = $('._er').scrollTop - (document.documentElement.clientHeight - 24);
  255. }
  256. break;
  257. case 39: // right
  258. if (e.ctrlKey) {
  259. toNextPage();
  260. } else {
  261. $('._er').scrollTop = $('._er').scrollTop + (document.documentElement.clientHeight - 24);
  262. }
  263. break;
  264. default:
  265. return true;
  266. }
  267. return false;
  268.  
  269. function toPrevPage(){
  270. if (content.pageup){
  271. localStorage['_er-enable'] = 'true';
  272. location.href = content.pageup;
  273. }else{
  274. alert('很抱歉,没有匹配到上一页!');
  275. }
  276. }
  277. function toNextPage(){
  278. if (content.pagedown){
  279. localStorage['_er-enable'] = 'true';
  280. location.href = content.pagedown;
  281. }else{
  282. alert('很抱歉,没有匹配到下一页!');
  283. }
  284. }
  285. };
  286. $('._er-content').onclick = function(e){ // 适用于墨水屏的左右点击无动画翻页
  287. var x = e.pageX;
  288. var width = document.documentElement.clientWidth;
  289. if (x <= width*0.1){ // 前翻一页
  290. $('._er').scrollTop = $('._er').scrollTop - (document.documentElement.clientHeight - 24)
  291. }else if(x >= width*0.9){ // 后翻一页
  292. $('._er').scrollTop = $('._er').scrollTop + (document.documentElement.clientHeight - 24);
  293. }
  294. }
  295. $('#_er-pageindex').onclick = function(){
  296. if (content.pageindex){
  297. location.href = content.pageindex;
  298. }else{
  299. alert('很抱歉,没有匹配到目录!');
  300. }
  301. };
  302. $('#_er-switch-theme').onclick = function(){
  303. var current = $('._er').dataset['theme'] || 'white';
  304. var themeList = ['white', 'Khaki', 'blue', 'OliveDrab', 'black'];
  305. var index = themeList.indexOf(current);
  306. if (index === -1) index = 0;
  307. index++;
  308. if (index >= themeList.length) {
  309. index = 0;
  310. }
  311. setTheme(themeList[index]);
  312. }
  313. $('#_er-pageup').onclick = function(){
  314. if (content.pageup){
  315. localStorage['_er-enable'] = 'true';
  316. location.href = content.pageup;
  317. }else{
  318. alert('很抱歉,没有匹配到上一页!');
  319. }
  320. };
  321. $('#_er-pagedown').onclick = function(){
  322. if (content.pagedown){
  323. localStorage['_er-enable'] = 'true';
  324. location.href = content.pagedown;
  325. }else{
  326. alert('很抱歉,没有匹配到下一页!');
  327. }
  328. };
  329. $('#_er-pagedown').dataset['nexturl'] = content.pagedown;
  330. setFontSize();
  331. setPadding();
  332.  
  333. // 按钮事件处理
  334. $('#_er-close').onclick = removeDom;
  335. $('#_er-font-plus').onclick = function(){
  336. fontsize += 2;
  337. setFontSize();
  338. };
  339. $('#_er-font-minus').onclick = function(){
  340. fontsize -= 2;
  341. setFontSize();
  342. };
  343. $('#_er-border').onclick= function() {
  344. padding = padding == 10 ? 5 : 10;
  345. setPadding();
  346. }
  347.  
  348. $('#_er-tts').onclick = function(){
  349. if (this.dataset['pause'] === 'true'){
  350. // 开始播放
  351. this.innerText = '停止';
  352. this.dataset['pause'] = 'false';
  353. playNextText();
  354. }else{
  355. this.innerText = '听书';
  356. this.dataset['pause'] = 'true';
  357. }
  358. };
  359.  
  360. if (autoplay){
  361. $('#_er-tts').innerText = '停止';
  362. $('#_er-tts').dataset['pause'] = 'false';
  363. playNextText();
  364. }else{
  365. $('#_er-tts').dataset['pause'] = 'true';
  366. }
  367. }
  368.  
  369. // 听书功能
  370. function playNextText(){
  371. updateReadPos();
  372. var current = $('._er-current');
  373. var playText = '';
  374. if (current){
  375. playText = current.innerText;
  376. }else{
  377. playText = $('._er-title').innerText;
  378. }
  379. if (playText){
  380. var utterThis = new SpeechSynthesisUtterance();
  381. utterThis.text = playText;
  382. utterThis.onerror = function(){
  383. $('#_er-tts').dataset['pause'] = 'true';
  384. alert("TTS语音转换文字出现异常,听书已停止运行!");
  385. };
  386. utterThis.onend = function(){
  387. toNextReadPos();
  388. if (!$('._er-current')){
  389. var nextUrl = $('#_er-pagedown').dataset['nexturl'];
  390. console.log(nextUrl);
  391. if (nextUrl){
  392. localStorage['_er-autoplay'] = 'true';
  393. localStorage['_er-enable'] = 'true';
  394. location.href = nextUrl;
  395. }
  396. return;
  397. }
  398. if ($('#_er-tts').dataset['pause'] === 'false'){
  399. playNextText();
  400. }
  401. };
  402. speechSynthesis.speak(utterThis);
  403. }else{
  404. toNextReadPos();
  405. playNextText();
  406. }
  407. }
  408.  
  409. function toNextReadPos(){
  410. var current = $('._er-current');
  411. var nextSpan = null;
  412. if (current){
  413. nextSpan = current.nextElementSibling;
  414. while(nextSpan && nextSpan.nodeName !== 'SPAN'){
  415. nextSpan = nextSpan.nextElementSibling;
  416. }
  417. }else{
  418. nextSpan = $('._er-content span');
  419. }
  420. if (current) current.classList.remove('_er-current');
  421. if (nextSpan) nextSpan.classList.add('_er-current');
  422. }
  423.  
  424. function toPrevReadPos(){
  425. var current = $('._er-current');
  426. var prevSpan = null;
  427. if (current){
  428. prevSpan = current.previousElementSibling;
  429. while(prevSpan && prevSpan.nodeName !== 'SPAN'){
  430. prevSpan = prevSpan.previousElementSibling;
  431. }
  432. }
  433. if (current) current.classList.remove('_er-current');
  434. if (prevSpan) prevSpan.classList.add('_er-current');
  435. }
  436.  
  437. function updateReadPos(){
  438. if ($('._er-current'))
  439. $('._er').scrollTop = $('._er-current').offsetTop - (document.documentElement.clientHeight / 2);
  440. }
  441.  
  442. function setFontSize(){
  443. localStorage["_er_fontsize"] = fontsize;
  444. $('._er-title').style.fontSize = (20+fontsize) + 'px';
  445. $('._er-title').style.lineHeight = ((20+fontsize)*1.5) + 'px';
  446. $('._er-content').style.fontSize = (14+fontsize) + 'px';
  447. $('._er-content').style.lineHeight = ((14+fontsize)*1.5) + 'px';
  448. }
  449.  
  450. function setPadding() {
  451. localStorage["_er_padding"] = padding;
  452. $('._er-content').style.padding = '10px ' + padding + '%';
  453. }
  454.  
  455. var oldOverflow = '';
  456. var oldOnKeyDown = $('body').onkeydown;
  457.  
  458. function removeDom(){
  459. $('._er').remove();
  460. $('body').style.overflow = oldOverflow;
  461. $('body').onkeydown = oldOnKeyDown;
  462. }
  463.  
  464. function addClassAndDom(){
  465. oldOverflow = $('body').style.overflow;
  466. $('body').style.overflow = 'hidden';
  467.  
  468. $('body').children[0].insertAdjacentHTML('beforeBegin',
  469. '<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no"><div class="_er">' +
  470. ' <div class="_er-tts">' +
  471. ' <button type="button" id="_er-tts">听书</button>' +
  472. ' </div>' +
  473. ' <div class="_er-tools">' +
  474. ' <button type="button" id="_er-pageindex">目录</button>' +
  475. ' <button type="button" id="_er-switch-theme">切换主题</button>' +
  476. ' <button type="button" id="_er-font-plus">字号+</button>' +
  477. ' <button type="button" id="_er-font-minus">字号-</button>' +
  478. ' <button type="button" id="_er-border">边距</button>' +
  479. ' <button type="button" id="_er-close">返回原网页</button>' +
  480. ' </div>' +
  481. ' <div class="_er-title"></div>' +
  482. ' <div class="_er-content">' +
  483. ' </div>' +
  484. ' <div class="_er-tools">' +
  485. ' <button type="button" id="_er-pageup">上一页</button>' +
  486. ' <button type="button" id="_er-pagedown">下一页</button>' +
  487. ' </div>' +
  488. '</div>');
  489. $('body').children[0].insertAdjacentHTML('beforeBegin',
  490. '<style>' +
  491. '._er{' +
  492. ' position: fixed;' +
  493. ' left: 0;' +
  494. ' right: 0;' +
  495. ' top: 0;' +
  496. ' bottom: 0;' +
  497. ' overflow: auto;' +
  498. ' background-color: white;' +
  499. ' z-index: 201901272211;' +
  500. '}' +
  501. '._er-title{' +
  502. ' text-align: center;' +
  503. ' font-size: 20px;' +
  504. ' line-height: 30px;' +
  505. ' font-weight: 900;' +
  506. ' padding: 10px 10%;' +
  507. ' color: black;' +
  508. '}' +
  509. '._er-content{' +
  510. ' padding: 10px 10%;' +
  511. ' font-size: 14px;' +
  512. ' line-height: 21px;' +
  513. ' color: black;' +
  514. '}' +
  515. '._er-tools{' +
  516. ' margin-top: 10px;' +
  517. ' margin-bottom: 10px;' +
  518. ' text-align: center;' +
  519. '}' +
  520. '._er-tools button{' +
  521. ' cursor: pointer;' +
  522. ' color: black;' +
  523. ' background-color: #908E90;' +
  524. ' border: 1px solid black;' +
  525. ' padding: 5px;' +
  526. ' border-radius: 10px;' +
  527. '}' +
  528. '._er-tts button{' +
  529. ' width: 50px;' +
  530. ' height: 50px;' +
  531. ' position: fixed;' +
  532. ' right: 15px;' +
  533. ' bottom: 15px;' +
  534. ' z-index: 201901272212;' +
  535. ' color: black;' +
  536. ' border: 1px solid black;' +
  537. ' opacity: 0.5;' +
  538. ' cursor: pointer;' +
  539. ' border-radius: 25px;' +
  540. ' display: none;' +
  541. '}' +
  542. '._er-current{' +
  543. ' background-color: yellow;' +
  544. ' color: black;' +
  545. '}' +
  546. '</style>');
  547. }
  548. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址