轻小说文库+

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

目前为 2021-05-15 提交的版本。查看 最新版本

// ==UserScript==
// @name         轻小说文库+
// @namespace    Wenku8+
// @version      1.0.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_getValue
// @grant        GM_setValue
// @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
// 查人API:http://www.wenku8.com/modules/article/reviewslist.php?keyword=136877
(function() {
    'use strict';

    // CONSTS
	const NUMBER_MAX_XHR = 10;
	const NUMBER_LOGSUCCESS_AFTER = NUMBER_MAX_XHR * 2;

	const KEY_COMMENT_DRAFTS = 'comment-drafts';
	const KEY_DRAFT_VERSION = 'version';
	const VALUE_DRAFT_VERSION = '0.1';

    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_COLOR_BTN_NORMAL = 'rgb(0, 160, 0)', CSS_COLOR_BTN_HOVER = 'rgb(0, 100, 0)';
    const CSS_COMMON = '.plusbtn {color: rgb(0, 160, 0);} .plusbtn:hover {color: rgb(0, 100, 0);} .plusbtn:focus {color: 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_WAITING = ' 等待中...'; const REG_GUI_WAITING = new RegExp(TEXT_GUI_WAITING + '$');
    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 = '下载图片(已完成)';
	const TEXT_GUI_AUTOSAVE = '(您输入的内容已保存到书评草稿中)';
	const TEXT_GUI_AUTOSAVE_CLEAR = '(草稿为空)';
	const TEXT_GUI_AUTOSAVE_RESTORE = '(已从书评草稿中恢复了您上次编辑的内容)';

	// Emoji smiles
	const SmList =
		  [{text:"/:O",id:"1",alt:"惊讶"}, {text:"/:~",id:"2",alt:"撇嘴"}, {text:"/:*",id:"3",alt:"色色"},
		   {text:"/:|",id:"4",alt:"发呆"}, {text:"/8-)",id:"5",alt:"得意"}, {text:"/:LL",id:"6",alt:"流泪"},
		   {text:"/:$",id:"7",alt:"害羞"}, {text:"/:X",id:"8",alt:"闭嘴"}, {text:"/:Z",id:"9",alt:"睡觉"},
		   {text:"/:`(",id:"10",alt:"大哭"}, {text:"/:-",id:"11",alt:"尴尬"}, {text:"/:@",id:"12",alt:"发怒"},
		   {text:"/:P",id:"13",alt:"调皮"}, {text:"/:D",id:"14",alt:"呲牙"}, {text:"/:)",id:"15",alt:"微笑"},
		   {text:"/:(",id:"16",alt:"难过"}, {text:"/:+",id:"17",alt:"耍酷"}, {text:"/:#",id:"18",alt:"禁言"},
		   {text:"/:Q",id:"19",alt:"抓狂"}, {text:"/:T",id:"20",alt:"呕吐"}]

    /* \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://gf.qytechs.cn/zh-CN/users/63073
     *  [GreasyFork] Pixiv Previewer: https://gf.qytechs.cn/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 = LogLevel.Info, 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);
	GMXHRHook(NUMBER_MAX_XHR);

    // 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;
		// ReviewList page
        case 'modules/article/reviews.php':
			areaReply();
			break;
		// Review page
        case 'modules/article/reviewshow.php':
			areaReply();
            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
        areaReply();

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

	// Reply area add-on
	function areaReply() {
		/* ## Release title area ## */
        if (document.querySelector('td > input[name="Submit"]') && !document.querySelector('#ptitle')) {
            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');
		const commentbttm = commentSbmt.parentElement;

        /* ## 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);
        }

		/* ## Comment auto-save ## */
		// GUI
		const asTip = document.createElement('span');
		commentbttm.appendChild(asTip);

		// Review-Page: Same rid, same savekey - 'rid123456'
		// Book-Page & Book-Review-List-Page: Same bookid, same savekey - 'bid1234'
		let commentData = {
			rid : getUrlArgv('rid', Number),
			aid : getUrlArgv('aid', Number),
			bid : location.href.match(/\/book\/(\d+).htm/) ? Number(location.href.match(/\/book\/(\d+).htm/)[1]) : 0,
			page : getUrlArgv('page', Number, 1)
		}
		commentData.key = commentData.rid ? 'rid' + String(commentData.rid) : 'bid' + String(commentData.bid);
		restoreDraft();

		const events = ['focus', 'blur', 'mousedown', 'keydown', 'keyup'];
		const eventEles = [commentArea, commenttitl];
		for (const eventEle of eventEles) {
			for (const event of events) {
				eventEle.addEventListener(event, saveDraft);
			}
		}

		function saveDraft() {
			const content = commentArea.value;
			const title = commenttitl.value;

			if (!content && !title) {
				clearDraft();
				return;
			} else if (commentData.content === content && commentData.title === title) {
				return;
			}

			commentData.content = content;
			commentData.title = title;

			const allCData = GM_getValue(KEY_COMMENT_DRAFTS, {});

			allCData[commentData.key] = commentData;
			allCData[KEY_DRAFT_VERSION] = VALUE_DRAFT_VERSION;
			GM_setValue(KEY_COMMENT_DRAFTS, allCData);
			asTip.innerHTML = TEXT_GUI_AUTOSAVE;
		}

		function restoreDraft() {
			const allCData = GM_getValue(KEY_COMMENT_DRAFTS, {});
			if (!allCData[commentData.key]) {return false;};
			commentData = allCData[commentData.key];
			commenttitl.value = commentData.title;
			commentArea.value = commentData.content;
			asTip.innerHTML = TEXT_GUI_AUTOSAVE_RESTORE;
			return true;
		}

		function clearDraft() {
			const allCData = GM_getValue(KEY_COMMENT_DRAFTS, {});
			if (!allCData[commentData.key]) {return false;};
			allCData[commentData.key] = undefined;
			GM_setValue(KEY_COMMENT_DRAFTS, allCData);
			asTip.innerHTML = TEXT_GUI_AUTOSAVE_CLEAR;
			return true;
		}

        function hotkeyReply() {
            let keycode = event.keyCode;
            if (keycode === 13 && event.ctrlKey && !event.altKey) {
				// Do not submit directly like this; we need to submit with onsubmit executed
                //commentForm.submit();
				commentSbmt.click();
            }
        }

		function submitHook() {
			const onsubmit = commentForm.onsubmit;
			commentForm.onsubmit = onsubmitForm;

			function onsubmitForm(e) {
				clearDraft();
				return onsubmit ? onsubmit() : function() {return true;};
			}
		}
	}

    // Review page add-on
    function pageReview() {
        // ## 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);

		addQuoteBtns();

		function addQuoteBtns() {
			// Get content textarea
			const pcontent = document.querySelector('#pcontent');
			const form = document.querySelector('form[action^="https://www.wenku8.net/modules/article/review"]');

			// Get floor elements
			const avatars = main.querySelectorAll('table div img.avatar');
			for (const avatar of avatars) {
				// do not insert the button as the first childnode. page saving function uses the first childnode as the time element.
				const table = avatar.parentElement.parentElement.parentElement.parentElement.parentElement;
				const numberEle = table.querySelector('td.even div a');
				const attr = numberEle.parentElement;
				const btn = createQuoteBtn(attr);
				const spliter = document.createTextNode(' | ');
				attr.insertBefore(spliter, numberEle);
				attr.insertBefore(btn, spliter);
			}

			function createQuoteBtn() {
				const btn = document.createElement('span');
				btn.classList.add(CLASSNAME_BUTTON);
				btn.addEventListener('click', quoteThisFloor);
				btn.innerHTML = '引用';
				return btn;

				function quoteThisFloor() {
					// In DOM Events, <this> keyword points to the Event Element.
					const numberEle = this.parentElement.querySelector('a[name]');
					const numberText = numberEle.innerText;
					const url = numberEle.href;
					const contentEle = this.parentElement.parentElement.querySelector('hr+div');
					const content = getFloorContent(contentEle);
					const insertPosition = pcontent.selectionEnd;
					const text = pcontent.value;
					const leftText = text.substr(0, insertPosition);
					const rightText = text.substr(insertPosition);

					/* ## Create insert value ## */
					let insertValue = '[url=U]N[/url] [quote]Q[/quote]';
					insertValue = insertValue.replace('U', url).replace('N', numberText).replace('Q', content);
					// if not at the beginning of a line then insert a whitespace before the link
					insertValue = ((leftText.length === 0 || /[\r\n]$/.test(leftText)) ? '' : ' ') + insertValue;
					// if not at the end of a line then insert a whitespace after the link
					insertValue += (rightText.length === 0 || /^[\r\n]/.test(leftText)) ? '' : ' ';

					pcontent.value = leftText + insertValue + rightText;
					const position = insertPosition + (pcontent.value.length - text.length);
					form.scrollIntoView(); pcontent.focus(); pcontent.setSelectionRange(position, position);
				}

				function getFloorContent(contentEle) {
					const subNodes = contentEle.childNodes;
					let content = '', subContent = '', size = '', color = '';

					for (const node of subNodes) {
						const type = node.nodeName;
						switch (type) {
							case '#text':
								content += node.data;
								break;
							case 'IMG':
								content += '[img]S[/img]'.replace('S', node.src);
								break;
							case 'A':
								content += '[url=U]T[/url]'.replace('U', node.href).replace('T', node.innerText);
								break;
							case 'BR':
								// no need to add \n, because \n will be preserved in #text nodes
								//content += '\n';
								break;
							case 'DIV':
								subContent = getFloorContent(node);
								if (node.classList.contains('jieqiQuote')) {
									subContent = '[quote]C[/quote]'.replace('C', subContent);
								} else if (node.classList.contains('jieqiCode')) {
									subContent = '[code]C[/code]'.replace('C', subContent);
								}
								content += subContent;
								break;
							case 'SPAN':
							case 'B':
							case 'I':
							case 'DEL':
							case 'CODE':
							case 'PRE':
								subContent = getFloorContent(node);
								content += subContent;
								break;
							/*
							case 'SPAN':
								subContent = getFloorContent(node);
								size = node.style.fontSize.match(/\d+/) ? node.style.fontSize.match(/\d+/)[0] : '';
								color = node.style.color.match(/rgb\((\d+), ?(\d+), ?(\d+)\)/);
								break;
							*/
						}
					}

					return content;
				}
			}
		}

        /*
        // 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;
			DoLog(LogLevel.Info, document, true);

            // 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 = getUrlArgv('rid', Number, 0);
                data.page = getUrlArgv('page', Number, 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.querySelector('a[name]');
                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));
                getDocument(url, joinPageData, callback);
            }

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

                // log
				const level = gotcount % NUMBER_LOGSUCCESS_AFTER ? LogLevel.Info : LogLevel.Success;
                DoLog(level, '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',
				onloadstart  : function() {
					DoLog(LogLevel.Info, 'Downloading ' + name + ' from ' + url);
				},
                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,'')
					.replace(REG_GUI_WAITING, '');
            }
            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,'')
						.replace(REG_GUI_WAITING, '');
					displayElement.innerText = text + TEXT_GUI_WAITING;
				}
			}

            // xmlHTTPRequest
            GM_xmlhttpRequest({
                method       :'GET',
                url          :url,
                responseType : 'blob',
				onloadstart  : function() {
					DoLog(LogLevel.Info, 'Downloading ' + name + ' from ' + url);

					// Display
					if (displayElement) {
						displayElement.innerText = text + TEXT_GUI_DOWNLOADING;
					}
				},
                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;
        }
    }

	// GM_XHR HOOK: The number of running GM_XHRs in a time must under maxXHR
	// Returns the abort function to stop the request anyway(no matter it's still waiting, or requesting)
	// (If the request is invalid, such as url === '', will return false and will NOT make this request)
	// If the abort function called on a request that is not running(still waiting or finished), there will be NO onabort event
	// Requires: function delItem(){...} & function uniqueIDMaker(){...}
	function GMXHRHook(maxXHR=5) {
		const GM_XHR = GM_xmlhttpRequest;
		const getID = uniqueIDMaker();
		let todoList = [], ongoingList = [];
		GM_xmlhttpRequest = safeGMxhr;

		function safeGMxhr() {
			// Get an id for this request, arrange a request object for it.
			const id = getID();
			const request = {id: id, args: arguments, aborter: null};

			// Deal onload function first
			dealEndingEvents(request);

			/* DO NOT DO THIS! KEEP ITS ORIGINAL PROPERTIES!
			// Stop invalid requests
			if (!validCheck(request)) {
				return false;
			}
			*/

			// Judge if we could start the request now or later?
			todoList.push(request);
			checkXHR();
			return makeAbortFunc(id);

			// Decrease activeXHRCount while GM_XHR onload;
			function dealEndingEvents(request) {
				const e = request.args[0];

				// onload event
				const oriOnload = e.onload;
				e.onload = function() {
					reqFinish(request.id);
					checkXHR();
					oriOnload ? oriOnload.apply(null, arguments) : function() {};
				}

				// onerror event
				const oriOnerror = e.onerror;
				e.onerror = function() {
					reqFinish(request.id);
					checkXHR();
					oriOnerror ? oriOnerror.apply(null, arguments) : function() {};
				}

				// ontimeout event
				const oriOntimeout = e.ontimeout;
				e.ontimeout = function() {
					reqFinish(request.id);
					checkXHR();
					oriOntimeout ? oriOntimeout.apply(null, arguments) : function() {};
				}

				// onabort event
				const oriOnabort = e.onabort;
				e.onabort = function() {
					reqFinish(request.id);
					checkXHR();
					oriOnabort ? oriOnabort.apply(null, arguments) : function() {};
				}
			}

			// Check if the request is invalid
			function validCheck(request) {
				const e = request.args[0];

				if (!e.url) {
					return false;
				}

				return true;
			}

			// Call a XHR from todoList and push the request object to ongoingList if called
			function checkXHR() {
				if (ongoingList.length >= maxXHR) {return false;};
				if (todoList.length === 0) {return false;};
				const req = todoList.shift();
				const reqArgs = req.args;
				const aborter = GM_XHR.apply(null, reqArgs);
				req.aborter = aborter;
				ongoingList.push(req);
				return req;
			}

			// Make a function that aborts a certain request
			function makeAbortFunc(id) {
				return function() {
					let i;

					// Check if the request haven't been called
					for (i = 0; i < todoList.length; i++) {
						const req = todoList[i];
						if (req.id === id) {
							// found this request: haven't been called
							delItem(todoList, i);
							return true;
						}
					}

					// Check if the request is running now
					for (i = 0; i < ongoingList.length; i++) {
						const req = todoList[i];
						if (req.id === id) {
							// found this request: running now
							req.aborter();
							reqFinish(id);
							checkXHR();
						}
					}

					// Oh no, this request is already finished...
					return false;
				}
			}

			// Remove a certain request from ongoingList
			function reqFinish(id) {
				let i;
				for (i = 0; i < ongoingList.length; i++) {
					const req = ongoingList[i];
					if (req.id === id) {
						ongoingList = delItem(ongoingList, i);
						return true;
					}
				}
				return false;
			}
		}
	}

    // 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',
			onloadstart  : function() {
				DoLog(LogLevel.Info, 'getting document, url=\'' + url + '\'');
			},
            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 url argument from lacation.href
	// also recieve a function to deal the matched string
	// returns defultValue if name not found
	function getUrlArgv(name, dealFunc=(function(a) {return a;}), defultValue=null) {
		const url = location.href;
		const matcher = new RegExp(name + '=([^&]+)');
		const result = url.match(matcher);
		const argv = result ? dealFunc(result[1]) : defultValue;

		return argv;
	}

    // 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 !== '';
    }

	// Del a item from an array using its index. Returns the array but can NOT modify the original array directly!!
	function delItem(arr, delIndex) {
		arr = arr.slice(0, delIndex).concat(arr.slice(delIndex+1));
		return arr;
	}

	// Makes a function that returns a unique ID number each time
	function uniqueIDMaker() {
		let id = 0;
		return makeID;
		function makeID() {
			id++;
			return id;
		}
	}

    // 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);
    }
})();

QingJ © 2025

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