Greasy Fork

Greasy Fork is available in English.

通用阅读器

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

当前为 2019-06-23 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         通用阅读器
// @version      0.3.4
// @description  为所有站点增加进入阅读模式按钮,点击后如果匹配成功则自动转码成为通用的阅读器样式方便使用,并提供扩展语音阅读功能(需要浏览器支持,推荐最新版Firefox浏览器)
// @author       pppploi8
// @match        https://*/*
// @match        http://*/*
// @grant        none
// @namespace http://greasyfork.icu/users/240492
// ==/UserScript==

(function() {
    var $ = function(selector){
        return document.querySelector(selector);
    }

    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};

    // 通用解析模板
    function parseContentAndTitle(){
        var mainDom = null;
        var minSize = 100;

        function findMainDom(doms){
            var docSize = document.body.scrollHeight * document.body.scrollWidth;
            for(var i=0;i<doms.length;i++){
                var dom = doms[i];
                // 计算dom尺寸百分比并输出
                var size = (dom.scrollWidth * dom.scrollHeight) / docSize;
                if (size > 0.3){
                    // 如果innerText字数大于500,且size最小,则视为正文区
                    var text = dom.innerText;
                    if (text.length >= 500 && size < minSize){
                        mainDom = dom;
                    }
                }
                findMainDom(dom.children||[]);
            }
        }
        findMainDom(document.body.children);

        if (mainDom){
            // 在mainDom同层查找标题
            // var titleList = [];
            // findTitleDom(mainDom.parentNode.children, titleList);
            // 查找标题逻辑bug较多,暂时改为查找网页title作为标题,后续重写逻辑进行优化
            return {content: mainDom.innerText, title: document.title};
        }

        // function findTitleDom(doms, titleList){
        //     for(var i=0;i<doms.length;i++){
        //         var dom = doms[i];
        //         if (dom.children && dom.children.length !== 0){
        //             var title = findTitleDom(dom.children, titleList);
        //         }else{
        //             if (/\<h1|\<h2|title/.test(dom.outerHTML)){
        //                 var title = dom.innerText;
        //                 if (title && title.length >= 5 && title.length <= 200){
        //                     titleList.push(dom);
        //                 }
        //             }
        //         }
        //     }
        // }
    }

    function parsePageUp(){
        var as = document.querySelectorAll('a');
        var reg = /上一章|上一页/;
        for(var i=0;i<as.length;i++){
            var text = as[i].outerHTML;
            var href = as[i].attributes.href && as[i].attributes.href.value;
            if (text && reg.test(text.trim()) && href && href != "#" && href.indexOf("javascript:") !== 0){
                return href;
            }
        }
    }

    function parsePageDown(){
        var as = document.querySelectorAll('a');
        var reg = /下一章|下一页/;
        for(var i=0;i<as.length;i++){
            var text = as[i].outerHTML;
            var href = as[i].attributes.href && as[i].attributes.href.value;
            if (text && reg.test(text.trim()) && href && href != "#" && href.indexOf("javascript:") !== 0){
                return href;
            }
        }
    }

    function parsePageIndex(){
        var as = document.querySelectorAll('a');
        var reg = /目录/;
        for(var i=0;i<as.length;i++){
            var text = as[i].innerText;
            var href = as[i].attributes.href && as[i].attributes.href.value;
            if (text && text.length <= 10 && reg.test(text.trim()) && href && href != "#" && href.indexOf("javascript:") !== 0){
                return href;
            }
        }
    }


    var fontsize = parseInt(localStorage["_er_fontsize"]||0);
    var autoplay = false;
    if (localStorage['_er-autoplay'] === 'true'){
        autoplay = true;
    }
    delete localStorage['_er-autoplay'];

    if (top.window !== window) return; // iframe内的网页不展示按钮,也不支持进入阅读模式
    if (localStorage['_er-enable'] === 'true'){
        localStorage['_er-enable'] = 'false';
        checkAndCreateReader(true);
    } else if (localStorage['_er-disabled'] !== 'true' && blackList[location.host] !== true){
        // 创建阅读模式悬浮按钮
        $('body').children[0].insertAdjacentHTML('beforeBegin', '<button id="_er-entryReadMode" style="' +
            '    position: fixed;' +
            '    right: 50px;' +
            '    bottom: 105px;' +
            '    background-color: white;' +
            '    border: 1px solid green;' +
            '    border-radius: 10px;' +
            '    padding: 0 5px;' +
            '    opacity: 0.5;' +
            '    height: 50px;' +
            '    overflow: auto;' +
            '    background-color: white;' +
            '    z-index: 201901272210;">进入阅读模式</button>'+
            '<button id="_er-NotShowReadMode" style="' +
            '    position: fixed;' +
            '    right: 50px;' +
            '    height: 50px;' +
            '    bottom: 50px;' +
            '    background-color: white;' +
            '    border: 1px solid green;' +
            '    border-radius: 10px;' +
            '    padding: 0 5px;' +
            '    opacity: 0.5;' +
            '    overflow: auto;' +
            '    background-color: white;' +
            '    z-index: 201901272210;">对本站关闭阅读器</button>');
        $('#_er-entryReadMode').onclick = checkAndCreateReader;
        $('#_er-NotShowReadMode').onclick = function(){
            localStorage['_er-disabled'] = 'true';
            $('#_er-entryReadMode').remove();
            $('#_er-NotShowReadMode').remove();
        }
    }

    function checkAndCreateReader(notAlert){
        // 通过调用通用模板尝试是否能够成功匹配到阅读内容
        var content = parseContentAndTitle();
        if (content && content.content){
            content.pageup = parsePageUp();
            content.pagedown = parsePageDown();
            content.pageindex = parsePageIndex();
            createReader(content);
        }else{
            if (notAlert !== true){
                alert('当前页面解析失败,无法进入阅读模式!');
            }
        }
    }

    // 创建阅读器
    function createReader(content){
        $('#_er-entryReadMode') && $('#_er-entryReadMode').remove();
        $('#_er-NotShowReadMode') && $('#_er-NotShowReadMode').remove();
        addClassAndDom();
        if (window.SpeechSynthesisUtterance){
            $('#_er-tts').style.display = 'block';
        }
        $('._er-title').innerText = content.title;
        var contentArr = content.content.split('\n');
        var contentHtml = '';
        for(var i=0;i<contentArr.length;i++){
            var line = contentArr[i];
            if (line){
                contentHtml += '<span>' + line + '</span>';
            }
            contentHtml += '<br>';
        }
        $('._er-content').innerHTML = contentHtml;
        var spanNodes = document.querySelectorAll('._er-content span');
        for(var i=0;i<spanNodes.length;i++){
            spanNodes[i].onclick = function(){
                for(var j=0;j<spanNodes.length;j++){
                    spanNodes[j].classList.remove('_er-current');
                }
                this.classList.add('_er-current');
            }
        }
        // 挂接键盘事件,实现键盘上下左右切换阅读功能
        $('body').onkeydown = function(e){
            switch(e.keyCode){
                case 38: // up
                    toPrevReadPos();
                    updateReadPos();
                    break;
                case 40: // down
                    toNextReadPos();
                    updateReadPos();
                    break;
                case 37: // left
                    $('._er').scrollTop = $('._er').scrollTop - (document.documentElement.clientHeight - 24)
                    break;
                case 39: // right
                    $('._er').scrollTop = $('._er').scrollTop + (document.documentElement.clientHeight - 24);
                    break;
                default:
                    return true;
            }
            return false;
        };
        $('._er-content').onclick = function(e){ // 适用于墨水屏的左右点击无动画翻页
            var x = e.pageX;
            var width = document.documentElement.clientWidth;
            if (x <= width*0.1){ // 前翻一页
                $('._er').scrollTop = $('._er').scrollTop - (document.documentElement.clientHeight - 24)
            }else if(x >= width*0.9){ // 后翻一页
                $('._er').scrollTop = $('._er').scrollTop + (document.documentElement.clientHeight - 24);
            }
        }
        $('#_er-pageindex').onclick = function(){
            if (content.pageindex){
                location.href = content.pageindex;
            }else{
                alert('很抱歉,没有匹配到目录!');
            }
        };
        $('#_er-pageup').onclick = function(){
            if (content.pageindex){
                localStorage['_er-enable'] = 'true';
                location.href = content.pageup;
            }else{
                alert('很抱歉,没有匹配到上一页!');
            }
        };
        $('#_er-pagedown').onclick = function(){
            if (content.pageindex){
                localStorage['_er-enable'] = 'true';
                location.href = content.pagedown;
            }else{
                alert('很抱歉,没有匹配到下一页!');
            }
        };
        $('#_er-pagedown').dataset['nexturl'] = content.pagedown;
        setFontSize();

        // 按钮事件处理
        $('#_er-close').onclick = removeDom;
        $('#_er-font-plus').onclick = function(){
            fontsize += 2;
            setFontSize();
        };
        $('#_er-font-minus').onclick = function(){
            fontsize -= 2;
            setFontSize();
        };

        $('#_er-tts').onclick = function(){
            if (this.dataset['pause'] === 'true'){
                // 开始播放
                this.innerText = '停止';
                this.dataset['pause'] = 'false';
                playNextText();
            }else{
                this.innerText = '听书';
                this.dataset['pause'] = 'true';
            }
        };

        if (autoplay){
            $('#_er-tts').innerText = '停止';
            $('#_er-tts').dataset['pause'] = 'false';
            playNextText();
        }else{
            $('#_er-tts').dataset['pause'] = 'true';
        }
    }

    // 听书功能
    function playNextText(){
        updateReadPos();
        var current = $('._er-current');
        var playText = '';
        if (current){
            playText = current.innerText;
        }else{
            playText = $('._er-title').innerText;
        }
        if (playText){
            var utterThis = new SpeechSynthesisUtterance();
            utterThis.text = playText;
            utterThis.onerror = function(){
                $('#_er-tts').dataset['pause'] = 'true';
                alert("TTS语音转换文字出现异常,听书已停止运行!");
            };
            utterThis.onend = function(){
                toNextReadPos();
                if (!$('._er-current')){
                    var nextUrl = $('#_er-pagedown').dataset['nexturl'];
                    console.log(nextUrl);
                    if (nextUrl){
                        localStorage['_er-autoplay'] = 'true';
                        localStorage['_er-enable'] = 'true';
                        location.href = nextUrl;
                    }
                    return;
                }
                if ($('#_er-tts').dataset['pause'] === 'false'){
                    playNextText();
                }
            };
            speechSynthesis.speak(utterThis);
        }else{
            toNextReadPos();
            playNextText();
        }
    }

    function toNextReadPos(){
        var current = $('._er-current');
        var nextSpan = null;
         if (current){
            nextSpan = current.nextElementSibling;
            while(nextSpan && nextSpan.nodeName !== 'SPAN'){
                nextSpan = nextSpan.nextElementSibling;
            }
        }else{
            nextSpan = $('._er-content span');
        }
        if (current) current.classList.remove('_er-current');
        if (nextSpan) nextSpan.classList.add('_er-current');
    }

    function toPrevReadPos(){
        var current = $('._er-current');
        var prevSpan = null;
        if (current){
            prevSpan = current.previousElementSibling;
            while(prevSpan && prevSpan.nodeName !== 'SPAN'){
                prevSpan = prevSpan.previousElementSibling;
            }
        }
        if (current) current.classList.remove('_er-current');
        if (prevSpan) prevSpan.classList.add('_er-current');
    }

    function updateReadPos(){
        if ($('._er-current'))
            $('._er').scrollTop =  $('._er-current').offsetTop - (document.documentElement.clientHeight / 2);
    }

    function setFontSize(){
        localStorage["_er_fontsize"] = fontsize;
        $('._er-title').style.fontSize = (20+fontsize) + 'px';
        $('._er-content').style.fontSize = (14+fontsize) + 'px';
    }

    var oldOverflow = '';
    var oldOnKeyDown = $('body').onkeydown;

    function removeDom(){
        $('._er').remove();
        $('body').style.overflow = oldOverflow;
        $('body').onkeydown = oldOnKeyDown;
    }

    function addClassAndDom(){
        oldOverflow = $('body').style.overflow;
        $('body').style.overflow = 'hidden';

        $('body').children[0].insertAdjacentHTML('beforeBegin',
            '<div class="_er">' +
            '    <div class="_er-tts">' +
            '        <button type="button" id="_er-tts">听书</button>' +
            '    </div>' +
            '    <div class="_er-tools">' +
            '        <button type="button" id="_er-pageindex">目录</button>' +
            '        <button type="button" id="_er-font-plus">字体+</button>' +
            '        <button type="button" id="_er-font-minus">字体-</button>' +
            '        <button type="button" id="_er-close">返回原网页</button>' +
            '    </div>' +
            '    <div class="_er-title"></div>' +
            '    <div class="_er-content">' +
            '    </div>' +
            '    <div class="_er-tools">' +
            '        <button type="button" id="_er-pageup">上一页</button>' +
            '        <button type="button" id="_er-pagedown">下一页</button>' +
            '    </div>' +
            '</div>');
        $('body').children[0].insertAdjacentHTML('beforeBegin',
            '<style>' +
            '._er{' +
            '    position: fixed;' +
            '    left: 0;' +
            '    right: 0;' +
            '    top: 0;' +
            '    bottom: 0;' +
            '    overflow: auto;' +
            '    background-color: white;' +
            '    z-index: 201901272211;' +
            '}' +
            '._er-title{' +
            '    text-align: center;' +
            '    font-size: 20px;' +
            '    font-weight: 900;' +
            '    padding: 10px 10%;' +
            '    color: black;' +
            '}' +
            '._er-content{' +
            '    padding: 10px 10%;' +
            '    font-size: 14px;' +
            '    color: black;' +
            '}' +
            '._er-tools{' +
            '    margin-top: 10px;' +
            '    margin-bottom: 10px;' +
            '    text-align: center;' +
            '}' +
            '._er-tools button{' +
            '    cursor: pointer;' +
            '    color: green;' +
            '    border: 1px solid black;' +
            '    padding: 5px;' +
            '    border-radius: 10px;' +
            '}' +
            '._er-tts button{' +
            '    width: 50px;' +
            '    height: 50px;' +
            '    position: fixed;' +
            '    right: 15px;' +
            '    bottom: 15px;' +
            '    z-index: 201901272212;' +
            '    color: green;' +
            '    border: 1px solid black;' +
            '    opacity: 0.5;' +
            '    cursor: pointer;' +
            '    border-radius: 25px;' +
            '    display: none;' +
            '}' +
            '._er-current{' +
            '    background-color: yellow;' +
            '}' +
            '</style>');
    }
})();