轻小说文库+

章节批量下载,版权限制小说TXT简繁全本下载,书名/作者名双击复制,Ctrl+Enter快捷键发表书评,单章节下载,小说JPEG插图下载,下载线路点击切换,书评帖子全贴下载保存

目前为 2021-04-28 提交的版本。查看 最新版本

// ==UserScript==
// @name         轻小说文库+
// @namespace    Wenku8+
// @version      0.9.2
// @description  章节批量下载,版权限制小说TXT简繁全本下载,书名/作者名双击复制,Ctrl+Enter快捷键发表书评,单章节下载,小说JPEG插图下载,下载线路点击切换,书评帖子全贴下载保存
// @author       PY-DNG
// @match        http*://www.wenku8.net/*
// @connect      wenku8.com
// @connect      wenku8.net
// @grant        GM_download
// @grant        GM_xmlhttpRequest
// @grant        GM_info
// @noframes
// ==/UserScript==

// 记录
// 阅读API:http://dl.wenku8.com/pack.php?aid=2478&vid=92914
// 回帖API:https://www.wenku8.net/modules/article/reviewshow.php?rid=209631&aid=2751
(function() {
    'use strict';

    // CONSTS
    const HTML_DOWNLOAD_CONTENER = '<div id="dctn" style=\"margin:0px auto;overflow:hidden;\">\n<fieldset style=\"width:820px;height:35px;margin:0px auto;padding:0px;\">\n<legend><b>《{BOOKNAME}》小说TXT简繁全本下载</b></legend>\n</fieldset>\n</div>';
    const HTML_DOWNLOAD_LINKS = '<div class="even">\n<span>简体(G)(<a class="dlink" href="http://dl.wenku8.com/down.php?type=txt&amp;id={BOOKID}" target="_black">载点一</a> \n<a class="dlink" href="http://dl.wenku8.com/down.php?type=txt&amp;id={BOOKID}&amp;fname=%B0%B2%B4%EF%D3%EB%B5%BA%B4%E5" target="_black">载点二</a>)</span>\n\n<span>简体(U)(<a class="dlink" href="http://dl.wenku8.com/down.php?type=utf8&amp;id={BOOKID}" target="_black">载点一</a> \n<a class="dlink" href="http://dl.wenku8.com/down.php?type=utf8&amp;id={BOOKID}&amp;fname=%B0%B2%B4%EF%D3%EB%B5%BA%B4%E5" target="_black">载点二</a>)</span>\n\n<span>繁体(U)(<a class="dlink" href="http://dl.wenku8.com/down.php?type=big5&amp;id={BOOKID}" target="_black">载点一</a> \n<a class="dlink" href="http://dl.wenku8.com/down.php?type=big5&amp;id={BOOKID}&amp;fname=%B0%B2%B4%EF%D3%EB%B5%BA%B4%E5" target="_black">载点二</a>)</span>\n  </div>';
    const HTML_DOWNLOAD_BOARD = '[轻小说文库+] 为您提供《{BOOKNAME}》的TXT简繁全本下载!</br>由此产生的一切法律及其他问题均由脚本用户承担</br>—— PY-DNG';
    const CSS_DOWNLOAD = '.even {display: grid; grid-template-columns: repeat(3, 1fr); text-align: center;} .dlink {text-align: center;}';
    const CSS_COMMON = '.plusbtn {color: rgb(0, 160, 0);} .plusbtn:hover {color: rgb(0, 100, 0);} .plusbtn:focus {color: rgb(0, 100, 0);}';
    const CSS_COLOR_BTN_NORMAL = 'rgb(0, 160, 0)', CSS_COLOR_BTN_HOVER = 'rgb(0, 100, 0)';

    const CLASSNAME_BUTTON = 'plusbtn';

    const TEXT_TIP_COPY = '双击复制';
    const TEXT_TIP_SERVERCHANGE = '点击切换线路';
    const TEXT_GUI_DOWNLOAD_IMAGE = '下载图片';
    const TEXT_GUI_DOWNLOAD_TEXT = '下载本章';
    const TEXT_GUI_DOWNLOAD_REVIEW = '[下载本帖(共A页)]';
    const TEXT_GUI_DOWNLOADING_REVIEW = '[下载中...(C/A)]';
    const TEXT_GUI_DOWNLOADFINISH_REVIEW = '[下载完毕]';
    const TEXT_GUI_DOWNLOADING = ' 下载中...'; const REG_GUI_DOWNLOADING = new RegExp(TEXT_GUI_DOWNLOADING + '$');
    const TEXT_GUI_DOWNLOADED = ' (下载完毕)'; const REG_GUI_DOWNLOADED = new RegExp(TEXT_GUI_DOWNLOADED.replaceAll(/([\(\)])+/g, '\\$1') + '$');
    const TEXT_GUI_DOWNLOADING_ALL = '下载中...(C/A)';
    const TEXT_GUI_DOWNLOADED_ALL = '下载图片(已完成)';

    /* \t
    ┌┬┐┌─┐┏┳┓┏━┓╭─╮
    ├┼┤│┼│┣╋┫┃╋┃│╳│
    └┴┘└─┘┗┻┛┗━┛╰─╯
    ╲╱╭╮
    ╱╲╰╯
    */
    /* **output format: Review Name.txt**
    ** 轻小说文库-帖子 [ID: reviewid]
    ** title
    ** 保存自: reviewlink
    ** 保存时间: savetime
    ** By scriptname Ver. version, author authorname
    **
    ** ──────────────────────────────
    ** [用户: username userid]
    ** 用户名: username
    ** 用户ID: userid
    ** 加入日期: 1970-01-01
    ** 用户链接: userlink
    ** 最早出现: 1楼
    ** ──────────────────────────────
    ** ...
    ** ──────────────────────────────
    ** [#1 2021-04-26 17:53:49] [username userid]
    ** ──────────────────────────────
    ** content - line 1
    ** content - line 2
    ** content - line 3
    ** ──────────────────────────────
    **
    ** ──────────────────────────────
    ** [#2 2021-04-26 19:28:08] [username userid]
    ** ──────────────────────────────
    ** content - line 1
    ** content - line 2
    ** content - line 3
    ** ──────────────────────────────
    **
    ** ...
    **
    **
    ** [THE END]
    */
    const TEXT_SPLIT_LINE_CHAR = '━'; const TEXT_SPLIT_LINE = TEXT_SPLIT_LINE_CHAR.repeat(20)
    const TEXT_OUTPUT_REVIEW_HEAD =
          '轻小说文库-帖子 [ID: {RWID}]\n{RWTT}\n保存自: {RWLK}\n保存时间: {SVTM}\nBy {SCNM} Ver. {VRSN}, author {ATNM}'
    const TEXT_OUTPUT_REVIEW_USER =
          '{LNSPLT}\n[用户: {USERNM} {USERID}]\n用户名: {USERNM}\n用户ID: {USERID}\n加入日期: {USERJT}\n用户链接: {USERLK}\n最早出现: {USERFL}楼\n{LNSPLT}'
    const TEXT_OUTPUT_REVIEW_FLOOR =
          '{LNSPLT}\n[#{RPNUMB} {RPTIME}] [{USERNM} {USERID}]\n{LNSPLT}\n{RPTEXT}\n{LNSPLT}';
    const TEXT_OUTPUT_REVIEW_END = '\n[THE END]';

    /** DoLog相关函数改自 Ocrosoft 的 Pixiv Previewer
     *  [GitHub]     Ocrosoft: https://github.com/Ocrosoft/
     *  [GreasyFork] Ocrosoft: https://greasyfork.org/zh-CN/users/63073
     *  [GreasyFork] Pixiv Previewer: https://greasyfork.org/zh-CN/scripts/30766
     *  [GitHub]     Pixiv Previewer: https://github.com/Ocrosoft/PixivPreviewer
     **/
    let LogLevel = {
        None: 0,
        Error: 1,
        Success: 2,
        Warning: 3,
        Info: 4,
        Elements: 5,
    };
    let g_logCount = 0;
    let g_logLevel = LogLevel.Success;

    function DoLog(level, msgOrElement, isElement=false) {
        if (level <= g_logLevel) {
            let prefix = '%c';
            let param = '';

            if (level == LogLevel.Error) {
                prefix += '[Error]';
                param = 'color:#ff0000';
            } else if (level == LogLevel.Success) {
                prefix += '[Success]';
                param = 'color:#00aa00';
            } else if (level == LogLevel.Warning) {
                prefix += '[Warning]';
                param = 'color:#ffa500';
            } else if (level == LogLevel.Info) {
                prefix += '[Info]';
                param = 'color:#888888';
            } else if (level == LogLevel.Elements) {
                prefix += 'Elements';
                param = 'color:#000000';
            }

            if (level != LogLevel.Elements && !isElement) {
                console.log(prefix + msgOrElement, param);
            } else {
                console.log(msgOrElement);
            }

            if (++g_logCount > 512) {
                console.clear();
                g_logCount = 0;
            }
        }
    }

    // Common actions
    addStyle(CSS_COMMON);

    // Get tab url api part
    const API = window.location.href.replace(/https?:\/\/www\.wenku8\.net\//, '').replace(/\?.*/, '')
                .replace(/^book\/\d+\.html?/, 'book').replace(/novel\/(\d+\/?)+\.html?$/, 'novel');
    switch (API) {
        // Dwonload page
        case 'modules/article/packshow.php':
            pageDownload();
            break;
        case 'modules/article/reviews.php':
        case 'modules/article/reviewshow.php':
            pageReview();
            break;
        // Index page
        case 'index.php':
            pageIndex();
            break;
        // Book page
        case 'book':
            pageBook();
            break;
        // Novel page
        case 'novel':
            pageNovel();
            break;
        // Other pages
        default:
            DoLog(LogLevel.Info, API);
    }

    // Book page add-on
    function pageBook() {
        const bookIdText = location.href.match(/\/(\d+)\.htm/)[1];
        const bookNameElement = document.querySelector('#content > div:nth-child(1) > table:nth-child(1) > tbody:nth-child(1) > tr:nth-child(1) > td:nth-child(1) > table:nth-child(1) > tbody:nth-child(1) > tr:nth-child(1) > td:nth-child(1) > span:nth-child(1) > b:nth-child(1)');
        const bookName = bookNameElement.innerText;
        const authorNameElement = document.querySelector('#content > div:nth-child(1) > table:nth-child(1) > tbody:nth-child(1) > tr:nth-child(2) > td:nth-child(2)');
        const authorName = authorNameElement.innerText.substr(authorNameElement.innerText.indexOf(':') + 1);
        const downloadEnabled = document.querySelector('#content > div:nth-child(1) > div > fieldset:nth-child(1) > legend:nth-child(1) > b:nth-child(1)') !== null;
        const commentArea = document.querySelector('#pcontent');
        const commentForm = document.querySelector('form[action^="https://www.wenku8.net/modules/article/reviews.php"]');
        const commentSbmt = document.querySelector('td > input[name="Submit"]');

        // Ctrl+Enter comment submit
        commentSbmt.value = '发表书评(Ctrl+Enter)';
        commentSbmt.style.padding = '0.3em 0.4em 0.3em 0.4em';
        commentSbmt.style.height= 'auto';
        commentArea.addEventListener('keydown', function() {
            let keycode = event.keyCode;
            if (keycode === 13 && event.ctrlKey && !event.altKey) {
                commentForm.submit();
            }
        })

        // Provide book & author name doubleclick copy
        if (tipshow && tiphide) {
            // tipshow and tiphide is coded inside wenku8 itself, its function is to show a text tip besides the mouse
            bookNameElement.addEventListener('mouseover', function() {tipshow(TEXT_TIP_COPY);});
            bookNameElement.addEventListener('mouseout' , tiphide);
            authorNameElement.addEventListener('mouseover', function() {tipshow(TEXT_TIP_COPY);});
            authorNameElement.addEventListener('mouseout' , tiphide);
        } else {
            bookNameElement.title = TEXT_TIP_COPY;
            authorNameElement.title = TEXT_TIP_COPY;
        }
        bookNameElement.addEventListener('dblclick', function() {copyText(bookName);});
        authorNameElement.addEventListener('dblclick', function() {copyText(authorName);});

        // Provide txtfull download for book which download is disabled
        if (!downloadEnabled) {
            // Append download html model
            const modelContainer = document.createElement('div');
            document.querySelector('#content div').appendChild(modelContainer);
            modelContainer.outerHTML = HTML_DOWNLOAD_CONTENER.replaceAll('{BOOKNAME}', bookName);
            //document.querySelector('#content div').innerHTML += HTML_DOWNLOAD_CONTENER.replaceAll('{BOOKNAME}', bookName);
            document.querySelector('#content div').lastChild.querySelector('fieldset').innerHTML += HTML_DOWNLOAD_LINKS.replaceAll('{BOOKID}', bookIdText);
            // Append CSS
            addStyle(CSS_DOWNLOAD);
            // Write textboard
            let textBoard = document.querySelector('#content > div:nth-child(1) > table:nth-child(4) > tbody:nth-child(1) > tr:nth-child(1) > td:nth-child(2) > span:nth-child(1) > b:nth-child(2)');
            textBoard.innerHTML = HTML_DOWNLOAD_BOARD.replaceAll('{BOOKNAME}', bookName);
            textBoard.style.color = 'green';
        }
    }

    // Review page add-on
    function pageReview() {
        // Release title area first
        if (document.querySelector('td > input[name="Submit"]')) {
            const table = document.querySelector('form>table');
            const titleText = table.innerHTML.match(/<!--[\s\S]+id="ptitle"[\s\S]+-->/)[0];
            const titleHTML = titleText.replace(/^<!--\s*/, '').replace(/\s*-->$/, '');
            table.innerHTML = table.innerHTML.replace(titleText, titleHTML);
        }

        const commentArea = document.querySelector('#pcontent');
        const commentForm = document.querySelector('form[action^="https://www.wenku8.net/modules/article/review"]');
        const commentSbmt = document.querySelector('td > input[name="Submit"]');
        const commenttitl = document.querySelector('#ptitle');

        // Ctrl+Enter comment submit
        if (commentSbmt) {
            commentSbmt.value = '发表书评(Ctrl+Enter)';
            commentSbmt.style.padding = '0.3em 0.4em 0.3em 0.4em';
            commentSbmt.style.height= 'auto';
            commentArea.addEventListener('keydown', hotkeyReply);
            commenttitl.addEventListener('keydown', hotkeyReply);
        }

        function hotkeyReply() {
            let keycode = event.keyCode;
            if (keycode === 13 && event.ctrlKey && !event.altKey) {
                commentForm.submit();
            }
        }

        // ## Save whole post ##
        // GUI
        const pageCountText = document.querySelector('#pagelink>.last').href.match(/page=(\d+)/)[1];

        const main = document.querySelector('#content');
        const headBars = main.querySelectorAll('tr>td[align]');
        headBars[0].width = '80%';
        headBars[1].width = '20%';

        const saveBtn = document.createElement('span');
        saveBtn.innerText = TEXT_GUI_DOWNLOAD_REVIEW.replaceAll('A', pageCountText);
        saveBtn.classList.add(CLASSNAME_BUTTON);
        saveBtn.addEventListener('click', downloadWholePost);
        headBars[1].appendChild(saveBtn);

        /*
        // Testing
        getAllPages(function(data) {
            const txt = joinTXT(data);
            DoLog(LogLevel.Success, txt);
        });
        */


        // ## Function: Get data from page document or join it into the given data variable ##
        function getDataFromPage(document, data) {
            let i;

            // Get Floors; avatars uses for element locating
            const main = document.querySelector('#content');
            const avatars = main.querySelectorAll('table div img.avatar');

            // init data, floors and users if need
            let floors = {}, users = {};
            if (data) {
                floors = data.floors;
                users = data.users;
            } else {
                data = {};
                initData(data, floors, users);
            }
            for (i = 0; i < avatars.length; i++) {
                const floor = newFloor(floors, avatars, i);
                const elements = getFloorElements(floor);
                const reply = getFloorReply(floor);
                const user = getFloorUser(floor);
                appendFloor(floors, floor);
            }
            return data;

            function initData(data, floors, users) {
                // data vars
                data.floors = floors; floors.data = data;
                data.users = users; users.data = data;

                // review info
                data.link = location.href;
                data.id = location.href.match(/rid=(\d+)/) ? Number(location.href.match(/rid=(\d+)/)[1]) : 0;
                data.page = location.href.match(/page=(\d+)/) ? Number(location.href.match(/page=(\d+)/)[1]) : 1;
                data.title = main.querySelector('th strong').innerText;
                return data;
            }

            function newFloor(floors, avatars, i) {
                const floor = {};
                floor.avatar = avatars[i];
                floor.floors = floors;
                return floor;
            }

            function getFloorElements(floor) {
                const elements = {}; floor.elements = elements;
                elements.avatar = floor.avatar;
                elements.table = elements.avatar.parentElement.parentElement.parentElement.parentElement.parentElement;
                elements.tr = elements.table.querySelector('tr');
                elements.tdUser = elements.table.querySelector('td.odd');
                elements.tdReply = elements.table.querySelector('td.even');
                elements.divUser = elements.tdUser.querySelector('div');
                elements.aUser = elements.divUser.querySelector('a');
                elements.attr = elements.tdReply.querySelector('div a').parentElement;
                elements.time = elements.attr.childNodes[0];
                elements.number = elements.attr.childNodes[1];
                elements.title = elements.tdReply.querySelector('div>strong');
                elements.content = elements.tdReply.querySelector('hr+div');
                return elements;
            }

            function getFloorReply(floor) {
                const elements = floor.elements;
                const reply = {}; floor.reply = reply;
                reply.time = elements.time.nodeValue.match(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/)[0];
                reply.number = Number(elements.number.innerText.match(/\d+/)[0]);
                reply.value = elements.content.innerText;
                reply.title = elements.title.innerText;
                return reply;
            }

            function getFloorUser(floor) {
                const elements = floor.elements;
                const user = {}; floor.user = user;
                user.id = elements.aUser.href.match(/uid=(\d+)/)[1];
                user.name = elements.aUser.innerText;
                user.avatar = elements.avatar.src;
                user.link = elements.aUser.href;
                user.jointime = elements.divUser.innerText.match(/\d{4}-\d{2}-\d{2}/)[0];

                const data = floor.floors.data; const users = data.users;
                if (!users.hasOwnProperty(user.id)) {
                    users[user.id] = user;
                    user.floors = [floor];
                } else {
                    const uFloors = users[user.id].floors;
                    uFloors.push(floor);
                    sortUserFloors(uFloors);
                }
                return user;
            }

            function sortUserFloors(uFloors) {
                uFloors.sort(function(F1, F2) {
                    return F1.reply.number > F2.reply.number;
                })
            }

            function appendFloor(floors, floor) {
                floors[floor.reply.number-1] = floor;
            }
        }

        // ## Function: Get pages and parse each pages to a data, returns data ##
        // callback(data, gotcount, finished) is called when xhr and parsing completed
        function getAllPages(callback) {
            let i, data, gotcount = 0;
            const ridMatcher = /rid=(\d+)/, pageMatcher = /page=(\d+)/;
            const lastpageUrl = document.querySelector('#pagelink>.last').href;
            const rid = Number(lastpageUrl.match(ridMatcher)[1]);
            const pageCount = Number(lastpageUrl.match(pageMatcher)[1]);
            const curPageNum = location.href.match(pageMatcher) ? Number(location.href.match(pageMatcher)[1]) : 1;

            for (i = 1; i <= pageCount; i++) {
                const url = lastpageUrl.replace(pageMatcher, 'page='+String(i));
                DoLog(LogLevel.Info, 'getting page ' + String(i) + ', url=\'' + url + '\'');
                getDocument(url, joinPageData, callback);
            }

            function joinPageData(pageDocument, callback) {
                data = getDataFromPage(pageDocument, data);
                gotcount++;

                // log
                DoLog(LogLevel.Info, 'got ' + String(gotcount) + ' pages.');
                if (gotcount === pageCount) {
                    DoLog(LogLevel.Success, 'All pages xhr and parsing completed.');
                    DoLog(LogLevel.Success, data, true);
                }

                // callback
                if (callback) {callback(data, gotcount, gotcount === pageCount);};
            }
        }

        // Function output
        function joinTXT(data, noSpliter=true) {
            const floors = data.floors; const users = data.users;

            // HEAD META DATA
            const saveTime = getTime();
            const head = TEXT_OUTPUT_REVIEW_HEAD
                .replaceAll('{RWID}', data.id).replaceAll('{RWTT}', data.title).replaceAll('{RWLK}', data.link)
                .replaceAll('{SVTM}', saveTime).replaceAll('{SCNM}', GM_info.script.name)
                .replaceAll('{VRSN}', GM_info.script.version).replaceAll('{ATNM}', GM_info.script.author);

            // join userinfos
            let userText = '';
            for (const [pname, user] of Object.entries(users)) {
                if (!isNumeric(pname)) {continue;};
                userText += TEXT_OUTPUT_REVIEW_USER
                    .replaceAll('{LNSPLT}', noSpliter ? '' : TEXT_SPLIT_LINE).replaceAll('{USERNM}', user.name)
                    .replaceAll('{USERID}', user.id).replaceAll('{USERJT}', user.jointime)
                    .replaceAll('{USERLK}', user.link).replaceAll('{USERFL}', user.floors[0].reply.number);
                userText += '\n'.repeat(2);
            }

            // join floors
            let floorText = '';
            for (const [pname, floor] of Object.entries(floors)) {
                if (!isNumeric(pname)) {continue;};
                const avatar = floor.avatar; const elements = floor.elements; const user = floor.user; const reply = floor.reply;
                floorText += TEXT_OUTPUT_REVIEW_FLOOR
                    .replaceAll('{LNSPLT}', noSpliter ? '' : TEXT_SPLIT_LINE).replaceAll('{RPNUMB}', String(reply.number))
                    .replaceAll('{RPTIME}', reply.time).replaceAll('{USERNM}', user.name)
                    .replaceAll('{USERID}', user.id).replaceAll('{RPTEXT}', reply.value);
                floorText += '\n'.repeat(2);
            }

            // End
            const foot = TEXT_OUTPUT_REVIEW_END;

            // return
            const txt = head + '\n'.repeat(2) + userText + '\n'.repeat(2) + floorText + '\n'.repeat(2) + foot;
            return txt;
        }

        // ## Function: Download the whole post ##
        function downloadWholePost() {
            // Continues only if not working
            if (downloadWholePost.working) {return;};
            downloadWholePost.working = true;

            // GUI
            saveBtn.innerText = TEXT_GUI_DOWNLOADING_REVIEW
                .replaceAll('C', '0').replaceAll('A', pageCountText);

            // go work!
            getAllPages(function(data, gotCount, finished) {
                // GUI
                saveBtn.innerText = TEXT_GUI_DOWNLOADING_REVIEW
                    .replaceAll('C', String(gotCount)).replaceAll('A', pageCountText);

                // Stop here if not completed
                if (!finished) {return;};

                // Join text
                const TXT = joinTXT(data);

                // Download
                const blob = new Blob([TXT],{type:"text/plain;charset=utf-8"});
                const url = URL.createObjectURL(blob);
                const name = '文库贴 - ' + String(data.id) + '.txt';

                const a = document.createElement('a');
                a.href = url;
                a.download = name;
                a.click();

                // GUI
                saveBtn.innerText = TEXT_GUI_DOWNLOADFINISH_REVIEW;

                // Work finish
                downloadWholePost.working = false;
            })
        }
    }

    // Novel page add-on
    function pageNovel() {
        const title = document.querySelector('#title').textContent;
        const isImagePage = title.includes('插图') || title.includes('插圖');
        const rightButtonDiv = document.querySelector('#linkright');
        const rightButtons = rightButtonDiv.childNodes;

        let dlCompleted = 0; // number of completed download tasks
        let dlAllCount = 0; // number of all download tasks
        let dlAllRunning = false; // whether there is downloadAllImages running

        // append control buttons
        let i;
        let spliter, button = rightButtonDiv.querySelector('a').cloneNode();
        for (i = 0; i < rightButtons.length; i++) {
            if (rightButtons[i].textContent.includes('|')) {
                spliter = rightButtons[i].cloneNode();
            }
        }

        // Attributes & Display config
        let allImages, buttonText;
        let clickFunc;
        if (isImagePage) {
            buttonText = TEXT_GUI_DOWNLOAD_IMAGE;
            clickFunc = function() {downloadAllImages();};
        } else {
            buttonText = TEXT_GUI_DOWNLOAD_TEXT;
            clickFunc = function() {downloadText();};
        }

        button.href = 'javascript:void(0);';
        button.target = '';
        button.innerText = buttonText;
        button.style.color = '#00BB00';
        button.addEventListener('click', clickFunc);
        rightButtonDiv.insertBefore(spliter, rightButtonDiv.lastChild);
        rightButtonDiv.insertBefore(button, rightButtonDiv.lastChild);
        rightButtonDiv.style.width = '500px';

        // Prevent URL.revokeObjectURL in script 轻小说文库下载
        const Ori_revokeObjectURL = URL.revokeObjectURL;
        URL.revokeObjectURL = function(arg) {
            if (typeof(arg) === 'string' && arg.substr(0, 5) === 'blob:') {return false;};
            return Ori_revokeObjectURL(arg);
        }

        function downloadText() {
            const contentEle = document.querySelector('#content');
            let content = contentEle.innerText//.replaceAll('\n', '\r\n');

            if (content.length === 0) {
                return false;
            }

            // Clear spaces
            content = content.split('\n');
            for (let i = 0; i < content.length; i++) {
                content[i] = content[i].trim();
            }
            content = content.join('\r\n');

            // Download
            const blob = new Blob([content],{type:"text/plain;charset=utf-8"});
            const url = URL.createObjectURL(blob);
            const name = title + '.txt';

            const a = document.createElement('a');
            a.style.display = 'none';
            a.href = url;
            a.download = name;
            a.click();
        }

        function downloadAllImages() {
            if (dlAllRunning) {
                return false;
            }
            allImages = document.querySelectorAll('#content > div.divimage img');
            dlAllCount = allImages.length;
            dlCompleted = 0;
            dlAllRunning = true;
            // Display
            button.innerText = TEXT_GUI_DOWNLOADING_ALL.replace('C', '0').replace('A', String(dlAllCount));
            rightButtonDiv.style.width = '550px';
            // Download
            const numLen = String(dlAllCount).length;
            for (let i = 0; i < dlAllCount; i++) {
                const imageName = title + '_' + fillNumber(i+1, numLen) + '.jpg';
                const url = allImages[i].src;
                if (allImages[i].src.substr(0,5) === 'blob:') {
                    const image = new Image();
                    image.onload = function() {
                        saveBlobToFile(toImageFormatURL(image, 1), imageName);
                        dlIncrease(button);
                    }
                    image.src = url;
                } else {
                    download(url, imageName, button);
                }
            }
        }

        // File download function
        function download(url, name, displayElement) {
            // Check
            if (!url || !name) {
                return false;
            }

            // xmlHTTPRequest
            GM_xmlhttpRequest({
                method:       'GET',
                url:          url,
                responseType: 'blob',
                onload:       function(request) {
                    // DataURL
                    let objURL = URL.createObjectURL(request.response);

                    // toImageFormatURL
                    const image = new Image();
                    image.src = objURL;
                    image.onload = function() {
                        //image.style.display = 'none';
                        //document.body.appendChild(image);
                        const formatURL = toImageFormatURL(image, 1);
                        //document.body.removeChild(image);

                        saveBlobToFile(formatURL, name);
                        dlIncrease(displayElement);
                    };
                }
            })

            return true;
        }

        // Increase dlCompleted and judge dlAllRunning
        function dlIncrease(displayElement) {
            // Task count decrease
            dlCompleted++;
            if (dlCompleted === dlAllCount) {
                dlAllRunning = false;
            }

            // Display
            if (displayElement) {
                displayElement.innerText = TEXT_GUI_DOWNLOADING_ALL
                    .replace('C', String(dlCompleted)).replace('A', String(dlAllCount));
                if (!dlAllRunning) {
                    displayElement.innerText = TEXT_GUI_DOWNLOADED_ALL;
                    rightButtonDiv.style.width = '550px';
                }
            }
        }

        // Blob url file saving function
        function saveBlobToFile(blobURL, name) {
            // Create <a>
            const a = document.createElement('a');
            a.style.display = 'none';
            a.href = blobURL;
            a.download = name;
            a.click();
        }

        // Image format changing function
        function toImageFormatURL(image, format) {
            if (typeof(format) === 'number') {format = ['image/jpeg', 'image/png', 'image/webp'][format-1]}
            const cvs = document.createElement('canvas');
            cvs.width = image.width;
		    cvs.height = image.height;
            const ctx = cvs.getContext('2d');
            ctx.drawImage(image, 0, 0);
            return cvs.toDataURL(format);
        }
    }

    // Index page add-on
    function pageIndex() {
    }

    // Download page add-on
    function pageDownload() {
        let i;
        let dlCount = 0; // number of active download tasks
        let dlAllRunning = false; // whether there is downloadAll running
        /* ******************* GUI ******************* */
        // Create left operation GUI
        let downloadGUI = document.querySelectorAll('#left div.block')[1].cloneNode(true);
        // Rename title
        downloadGUI.querySelector('.blocktitle .txt').innerHTML = '下载全部章节';
        // Remove content
        downloadGUI.removeChild(downloadGUI.querySelector('.blockcontent'));
        // Create operation ul list
        let optionButtonsForm = document.querySelector('#left div.block div.blockcontent div ul[style]').cloneNode(true);
        // Reset lis
        const NAMES = ['本地简体(G)', '本地简体(U)', '本地繁体(U)', '地址二简体(G)', '地址二简体(U)', '地址二繁体(U)'];
        let lis = optionButtonsForm.querySelectorAll('li');
        let li = lis[0].cloneNode(true);
        let newli;
        li.querySelector('a').href = 'javascript:void(0);';
        li.querySelector('a').className = '';
        li.querySelector('a').classList.add(CLASSNAME_BUTTON);
        li.querySelector('a').innerHTML = '默认按钮文本';
        for (i = 0; i < 6; i++) {
            // If li exist, remove it
            if (lis[i]) {
                optionButtonsForm.removeChild(lis[i]);
            };
            // Create a new one
            newli = li.cloneNode(true);
            // Modify name
            newli.querySelector('a').innerHTML = NAMES[i];
            // Mark i
            newli.i = i;
            // Append it
            optionButtonsForm.appendChild(newli);
            // Add event listener
            newli.addEventListener('click',
            function() { // i refers to its current value in loop by marking on the li element
                downloadAll(this.i);
            })
        }
        // Create a container
        let blockcontent = document.createElement('div');
        blockcontent.classList.add('blockcontent');
        blockcontent.style.paddingLeft = '10px';
        // Append ul
        blockcontent.appendChild(optionButtonsForm);
        // Append container
        downloadGUI.appendChild(blockcontent);
        // Append GUI
        document.querySelector('#left').appendChild(downloadGUI);

        // Servers GUI
        let servers = document.querySelectorAll('#content>b');
        let serverEles = [];
        for (i = 0; i < servers.length; i++) {
            if (servers[i].innerText.includes('wenku8.com')) {
                serverEles.push(servers[i]);
            }
        }
        for (i = 0; i < serverEles.length; i++) {
            serverEles[i].classList.add(CLASSNAME_BUTTON);
            serverEles[i].addEventListener('click', function() {changeAllServers(this.innerText);});
            if (tipshow && tiphide) {
                // tipshow and tiphide is coded inside wenku8 itself, its function is to show a text tip besides the mouse
                serverEles[i].addEventListener('mouseover', function() {tipshow(TEXT_TIP_SERVERCHANGE);});
                serverEles[i].addEventListener('mouseout' , tiphide);
            } else {
                serverEles[i].title = TEXT_TIP_SERVERCHANGE;
            }
        }

        /* ******************* Code ******************* */
        // Change all server elements
        function changeAllServers(server) {
            let i;
            const allA = document.querySelectorAll('.even a');
            for (i = 0; i < allA.length; i++) {
                changeServer(server, allA[i]);
            }
        }

        // Change server for an element
        function changeServer(server, element) {
            if (!element.href) {return false;};
            element.href = element.href.replace(/\/\/dl\d?\.wenku8\.com\//g, '//' + server + '/');
        }

        // Get novel name
        const novelName = document.querySelector('html body div.main div#centerm div#content table.grid caption a').innerText;
        let downloadAll = function(type) {
            // Check: only download while no download active tasks currently
            if (dlAllRunning) {
                return false;
            }
            dlAllRunning = true;

            // GUI display
            downloadGUI.querySelector('.blocktitle .txt').innerHTML = TEXT_GUI_DOWNLOADING;

            // Name customize
            let NAME = novelName + ' {j}.';
            let allNames = getAllNames();
            if (window.location.href.indexOf('txt') != -1) {
                NAME += 'txt';
            } else {
                NAME += document.querySelector('html body div.main div#centerm div#content table.grid tbody tr td.even a').innerText.replace(/[^\w]+/, '').toLowerCase();
            }
            let i,j = 0;
            const allA = document.querySelectorAll('.even a');
            for (i = type; i < allA.length; i = i + 6) {
                /*GM_download({
                    url: allA[i].href,
                    name: NAME.replace('{j}', (window.location.href.indexOf('txtfull') === -1 ? allNames[j] : ''))
                });*/
                download(
                    allA[i].href,
                    NAME.replace('{j}', (window.location.href.indexOf('txtfull') === -1 ? allNames[j] : '')),
                    allA[i].parentElement.parentElement.querySelector('td.odd')
                )
                j += 1;
            }
            downloadGUI.querySelector('.blocktitle .txt').innerHTML = '下载全部章节';
        }

        function getAllNames() {
            let all = document.querySelectorAll('.grid tbody tr .odd');
            let names = [];
            for (let i = 0; i < all.length; i++) {
                names[i] = all[i].innerText.replace(REG_GUI_DOWNLOADED, '').replace(REG_GUI_DOWNLOADING,'');
            }
            return names;
        }

        // File download function
        function download(url, name, displayElement) {
            // Check
            if (!url || !name) {
                return false;
            }

            // dl task count increase
            dlCount++;

            // Display
            let text = '';
            if (displayElement) {
                if (displayElement.innerText) {text = displayElement.innerText.replace(REG_GUI_DOWNLOADED, '').replace(REG_GUI_DOWNLOADING,'');};
                displayElement.innerText = text + TEXT_GUI_DOWNLOADING;
            }

            // xmlHTTPRequest
            GM_xmlhttpRequest({
                method:       'GET',
                url:          url,
                responseType: 'blob',
                onload:       function(request) {
                    // DataURL
                    let objURL = URL.createObjectURL(request.response);

                    // Create <a>
                    const a = document.createElement('a');
                    a.style.display = 'none';
                    a.href = objURL;
                    a.download = name;
                    a.click();

                    // Task count decrease
                    dlCount--;
                    if (dlCount === 0) {
                        dlAllRunning = false;
                    }

                    // Display
                    if (displayElement) {
                        displayElement.innerText = TEXT_GUI_DOWNLOADED.replace(/^ /, '');
                        if (text) {displayElement.innerText = text + TEXT_GUI_DOWNLOADED;};
                    }
                }
            })

            return true;
        }
    }

    // Download and parse a url page into a html document(dom).
    // when xhr onload: callback.apply([dom, args])
    function getDocument(url, callback, args) {
        GM_xmlhttpRequest({
            method:       'GET',
            url:          url,
            responseType: 'blob',
            onload:       function(response) {
                const htmlblob = response.response;
                const reader = new FileReader();
                reader.onload = function(e) {
                    const htmlText = reader.result;
                    const dom = new DOMParser().parseFromString(htmlText, 'text/html');
                    args = [dom].concat(args);
                    callback.apply(null, args);
                    //callback(dom, htmlText);
                }
                reader.readAsText(htmlblob, 'GBK');
                /* 注意!原来这里只是使用了DOMParser,DOMParser不像iframe加载Document一样拥有完整的上下文并执行所有element的功能,
                ** 只是按照HTML格式进行解析,所以在文库页面的GBK编码下仍然会按照UTF-8编码进行解析,导致中文乱码。
                ** 所以处理dom时不要使用ASC-II字符集以外的字符!
                **
                ** 注:现在使用了FileReader来以GBK编码解析htmlText,故编码问题已经解决,可以正常使用任何字符
                */
            }
        })
    }

    // Get a time text like 1970-01-01 00:00:00
    function getTime(dateSpliter='-', timeSpliter=':') {
        const d = new Date();
        const fulltime = fillNumber(d.getFullYear(), 4) + dateSpliter + fillNumber((d.getMonth() + 1), 2) + dateSpliter + fillNumber(d.getDate(), 2)
                 + ' ' + fillNumber(d.getHours(), 2) + timeSpliter + fillNumber(d.getMinutes(), 2) + timeSpliter + fillNumber(d.getSeconds(), 2);
        return fulltime;
    }

    // Fill number text to certain length with '0'
    function fillNumber(number, length) {
        let str = String(number);
        for (let i = str.length; i < length; i++) {
            str = '0' + str;
        }
        return str;
    }

    // Judge whether the str is a number
    function isNumeric(str) {
        const result = Number(str);
        return !isNaN(result) && str !== '';
    }

    // Append a style text to document(<head>) with a <style> element
    function addStyle(css) {
        document.head.appendChild(document.createElement("style")).textContent = css;
    }

    // Copy text to clipboard (needs to be called in an user event)
    function copyText(text) {
        // Create a new textarea for copying
        const newInput = document.createElement('textarea');
        document.body.appendChild(newInput);
        newInput.value = text;
        newInput.select();
        document.execCommand('copy');
        document.body.removeChild(newInput);
    }
})();