Greasy Fork

Greasy Fork is available in English.

轻小说文库+

TXT分卷批量下载,版权限制小说TXT简繁全本下载,书名/作者名双击复制,Ctrl+Enter快捷键发表书评,单章节下载,小说JPEG插图下载,下载线路点击切换,书评帖子全贴下载保存,书评帖子回复功能增强,书架功能增强,修复文库插入链接和图片无法识别https的自身bug,轻小说标签搜索(Feature Preview),用户书评搜索,简单的阅读页和目录页美化,每日自动推书

当前为 2021-06-28 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

/* eslint-disable no-multi-spaces */

// ==UserScript==
// @name         轻小说文库+
// @namespace    Wenku8+
// @version      1.2.3
// @description  TXT分卷批量下载,版权限制小说TXT简繁全本下载,书名/作者名双击复制,Ctrl+Enter快捷键发表书评,单章节下载,小说JPEG插图下载,下载线路点击切换,书评帖子全贴下载保存,书评帖子回复功能增强,书架功能增强,修复文库插入链接和图片无法识别https的自身bug,轻小说标签搜索(Feature Preview),用户书评搜索,简单的阅读页和目录页美化,每日自动推书
// @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_openInTab
// @grant        GM_info
// @require      http://greasyfork.icu/scripts/427726-gbk-url-js/code/GBK_URLjs.js?version=939169
// @require      http://greasyfork.icu/scripts/425166-elegant-alert-%E5%BA%93/code/elegant%20alert()%E5%BA%93.js?version=922763
// @noframes
// ==/UserScript==

/* 需求记录 [容易(优先级高) ➡️ 困难(优先级低)]
** [已完成]{BK}书评页提供用户书评搜索
** {BK}图片大小(最大)限制
** {BK}回复区插入@好友
** {jack158}[部分完成]全卷/分卷下载:文件重命名为书名,而不是书号
** · [已完成分卷&Book页]添加单文件下载重命名
** {BK}回复区悬浮显示
** {热忱}[已完成]修复https引用问题
** [已完成]引用回复
** [beta已完成]支持preview版tag搜索
** [已完成]每日自动推书
** {BK}类似ehunter的阅读模式
** 改进旧代码:
** · 每个page-addon内部要按照功能分模块,执行功能靠调用模块,不能直接写功能代码
** · 共性模块要写进脚本全局作用域,可以的话写成构造函数
** {热忱}书评:@某人时通知他
** {BK}[部分完成]页面美化
** · [已完成]阅读页去除广告
** · …
** {热忱}提供带文字和插图的epub整合下载
** {BK}[待完善]书评:草稿箱功能
*/
(function() {
    'use strict';

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

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

	const KEY_BOOKCASES = 'book-cases';
	const KEY_BOOKCASE_VERSION = 'version';
	const VALUE_BOOKCASE_VERSION = '0.1';

	const KEY_ATRCMMDS = 'auto-recommends';
	const KEY_ATRCMMDS_VERSION = 'version';
	const VALUE_ATRCMMDS_VERSION = '0.1';

	const KEY_USRDETAIL = 'user-detail';
	const KEY_USRDETAIL_VERSION = 'version';
	const VALUE_USRDETAIL_VERSION = '0.1';

	const VALUE_STR_NULL = 'null';

	const URL_REVIEWSEARCH = 'https://www.wenku8.net/modules/article/reviewslist.php?keyword={K}';
	const URL_USERINFO = 'https://www.wenku8.net/userinfo.php?id={K}';
	const URL_DOWNLOAD1 = 'http://dl.wenku8.com/packtxt.php?aid={A}&vid={V}&charset={C}';
	const URL_DOWNLOAD2 = 'http://dl2.wenku8.com/packtxt.php?aid={A}&vid={V}&charset={C}';
	const URL_DOWNLOAD3 = 'http://dl3.wenku8.com/packtxt.php?aid={A}&vid={V}&charset={C}';
	const URL_RECOMMEND = 'https://www.wenku8.net/modules/article/uservote.php?id={B}';
	const URL_TAGSEARCH = 'https://www.wenku8.net/modules/article/tags.php?t={TU}';
	const URL_USRDETALE = 'https://www.wenku8.net/userdetail.php';

	const CLASSNAME_BUTTON = 'plusbtn';
	const CLASSNAME_TEXT = 'plustext';
	const CLASSNAME_BOOKCASE_FORM = 'bcform';

	const HTML_BOOK_COPY = '<span class="{C}">[复制]</span>'.replace('{C}', CLASSNAME_BUTTON);
	const HTML_BOOK_META = '{K}:{V}<span class="{C}">[复制]</span>'.replace('{C}', CLASSNAME_BUTTON);
	const HTML_BOOK_TAG = '<a class="{C}" href="{U}" target="_blank">{TN}</span>'.replace('{C}', CLASSNAME_BUTTON).replace('{U}', URL_TAGSEARCH);
    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 id="txtfull"style="margin:0px auto;overflow:hidden;"><fieldset style="width:820px;height:35px;margin:0px auto;padding:0px;"><legend><b>《{ORIBOOKNAME}》小说TXT全本下载</b></legend><div style="width:16%; float:left; text-align:center;"><a href="http://dl.wenku8.com/down.php?type=txt&amp;id={BOOKID}">G版原始下载</a></div><div style="width:16%; float:left; text-align:center;"><a href="http://dl.wenku8.com/down.php?type=txt&amp;id={BOOKID}&amp;fname={BOOKNAME}.txt">G版自动重命名</a></div><div style="width:16%; float:left; text-align:center;"><a href="http://dl.wenku8.com/down.php?type=utf8&amp;id={BOOKID}">U版原始下载</a></div><div style="width:16%; float:left; text-align:center;"><a href="http://dl.wenku8.com/down.php?type=utf8&amp;id={BOOKID}&amp;fname={BOOKNAME}">U版自动重命名</a></div><div style="width:16%; float:left; text-align:center;"><a href="http://dl.wenku8.com/down.php?type=big5&amp;id={BOOKID}">繁体原始下载</a></div><div style="width:16%; float:left; text-align:center;"><a href="http://dl.wenku8.com/down.php?type=big5&amp;id={BOOKID}&amp;fname={BOOKNAME}">繁体自动重命名</a></div></fieldset></div>'.replaceAll('{C}', CLASSNAME_BUTTON);
    const HTML_DOWNLOAD_BOARD = '<span class="{C}">[轻小说文库+] 为您提供《{ORIBOOKNAME}》的TXT简繁全本下载!</br>由此产生的一切法律及其他问题均由脚本用户承担</br>—— PY-DNG</span>'.replace('{C}', CLASSNAME_TEXT);
    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 = '.{CT} {color: rgb(30, 100, 220) !important;} .{CB} {color: rgb(0, 160, 0) !important; cursor: pointer !important;} .{CB}:hover {color: rgb(0, 100, 0) !important;} .{CB}:focus {color: rgb(0, 100, 0) !important;}'.replaceAll('{CB}', CLASSNAME_BUTTON).replaceAll('{CT}', CLASSNAME_TEXT);

	const ARR_GUI_BOOKCASE_WIDTH = ['3%', '19%', '9%', '25%', '20%', '9%', '5%', '10%'];

    const TEXT_TIP_COPY = '点击复制';
	const TEXT_TIP_COPIED = '已复制';
    const TEXT_TIP_SERVERCHANGE = '点击切换线路';
	const TEXT_TIP_SEARCH_OPTION_TAG = '有关标签搜索</br></br>未完善-开发中…</br>官方尚未正式开放此功能</br>功能预览由[轻小说文库+]提供';
	const TEXT_ALT_DOWNLOADFINISH_REVIEW = '{T}({I}) 已下载完毕</br>{N} 已保存';
	const TEXT_ALT_META_COPIED = '{M} 已复制';
	const TEXT_ALT_ATRCMMDS_SAVED = '已保存:每日自动推荐{N}次';
	const TEXT_ALT_ATRCMMDS_INVALID = '未保存:{N}不是非负整数';
	const TEXT_ALT_ATRCMMDS_OVERFLOW = '注意:推荐总数超过了{V}票';
	const TEXT_ALT_ATRCMMDS_RUNNING = '正在推荐书目:</br>{BN}({BID})';
	const TEXT_ALT_ATRCMMDS_DONE = '推荐完成:</br>{BN}({BID})';
	const TEXT_ALT_USRDTL_REFRESH = '{S}:正在更新用户信息({T})...'.replaceAll('{S}', GM_info.script.name).replaceAll('{T}', getTime());
	const TEXT_ALT_USRDTL_REFRESHED = '{S}:用户信息已更新</br>点此查看详细信息'.replaceAll('{S}', GM_info.script.name);
    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_DOWNLOADALL = '下载全部分卷,请点击右边的按钮:';
	const TEXT_GUI_WAITING = ' 等待中...';
    const TEXT_GUI_DOWNLOADING = ' 下载中...';
    const TEXT_GUI_DOWNLOADED = ' (下载完毕)';
	const TEXT_GUI_NOTHINGHERE = '<span style="color:grey">-Nothing Here-</span>';
	const TEXT_GUI_SDOWNLOAD = '地址三(程序重命名)';
    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 = '(已从书评草稿中恢复了您上次编辑的内容)';
	const TEXT_GUI_BOOKCASE_GETTING = '正在搬运书架...(C/A)';
	const TEXT_GUI_BOOKCASE_TOPTITLE = '您的书架可收藏 A 本,已收藏 B 本';
	const TEXT_GUI_BOOKCASE_MOVEBOOK = '移动到 [N]';
	const TEXT_GUI_BOOKCASE_DBLCLICK = '双击我,给我取一个好听的名字吧~';
	const TEXT_GUI_BOOKCASE_WHATNAME = '呜呜呜~会是什么名字呢?';
	const TEXT_GUI_BOOKCASE_ATRCMMD = '自动推荐';
	const TEXT_GUI_SEARCH_OPTION_TAG = '标签(preview)';
	const TEXT_GUI_BLOCK_TITLE_DEFULT = '操作区域';
	const TEXT_GUI_USER_REVIEWSEARCH = '用户书评';
	const TEXT_GUI_USER_USERINFO = '详细资料';

	// Emoji smiles (not used in the script yet)
	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: http://greasyfork.icu/zh-CN/users/63073
     *  [GreasyFork] Pixiv Previewer: http://greasyfork.icu/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.Warning;

    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
	const tipready = tipcheck();
    addStyle(CSS_COMMON);
	GMXHRHook(NUMBER_MAX_XHR);
	scheduledTasks();

	// Tags search beta
	formSearch();

    // 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')
	            .replace(/^novel[\/\d]+index\.html?$/,'novelindex');
	if (isAPIPage()) {pageAPI(API); return;};
	if (!API) {location.href = 'https://www.wenku8.net/index.php';return;};
    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;
		// Bookcase page
		case 'modules/article/bookcase.php':
			pageBookcase();
			break;
		// Tags page
		case 'modules/article/tags.php':
			pageTags();
			break;
		case 'userpage.php':
			pageUser();
			break;
        // Index page
        case 'index.php':
            pageIndex();
            break;
        // Book page
		// Also: https://www.wenku8.net/modules/article/articleinfo.php?id={ID}&charset=gbk
        case 'book':
            pageBook();
            break;
		// Novel index page
		case 'novelindex':
			pageNovelIndex();
			break;
        // Novel page
        case 'novel':
            pageNovel();
            break;
        // Other pages
        default:
            DoLog(LogLevel.Info, API);
    }

	// Autorun tasks
	function scheduledTasks() {
		refreshUserDetail();
		recommend();

		// Refresh userDetail storage everyday
		function refreshUserDetail() {
			// Time check: whether recommend has done today
			if (getMyUserDetail().lasttime === getTime('-', false)) {return false;};
			refreshMyUserDetail();
		}

		// Auto recommend books everyday
		function recommend(recommendAnyway=false) {
			let i;

			// Get config
			const arConfig = GM_getValue(KEY_ATRCMMDS, null);
			if (!arConfig) {return false;};

			// Time check: whether all recommends has done today
			if (arConfig.lasttime === getTime('-', false) && !recommendAnyway) {return false;};

			// Recommend for each
			for (const strBookID in arConfig.books) {
				// Only when inherited properties exists must we use hasOwnProperty()
				// here we know there is no inherited properties
				const book = arConfig.books[strBookID]
				const number = book.number;
				const bookID = book.id;
				const bookName = book.name;

				// Time check: whether this book's recommend has done today
				if (book.lasttime === getTime('-', false)) {continue;};

				// Soft alert
				new ElegantAlertBox(TEXT_ALT_ATRCMMDS_RUNNING.replaceAll('{BN}', bookName).replaceAll('{BID}', strBookID));

				// Go work
				for (i = 0; i < number; i++) {
					getDocument(URL_RECOMMEND.replaceAll('{B}', strBookID), function(oDoc) {
						// title: "处理成功"
						const statusText = oDoc.querySelector('.blocktitle').innerText;
						// content: "我们已经记录了本次推荐,感谢您的参与!\n\n您每天拥有 5 次推荐权利,这是您今天第 1 次推荐。"
						const returnText = oDoc.querySelector('.blockcontent').innerText.replace(/\s*\[.+\]\s*$/, '');

						// Save
						book.lasttime = getTime('-', false);
						GM_setValue(KEY_ATRCMMDS, arConfig);

						// Log
						DoLog(LogLevel.Info, statusText);
						DoLog(LogLevel.Info, returnText);
					})
				}

				// Soft alert
				new ElegantAlertBox(TEXT_ALT_ATRCMMDS_DONE.replaceAll('{BN}', bookName).replaceAll('{BID}', strBookID));
			}

			arConfig.lasttime = getTime('-', false);
			GM_setValue(KEY_ATRCMMDS, arConfig);
			return true;
		}
	}

    // Book page add-on
    function pageBook() {
		// Resource
		const pageResource = {
			elements: {},
			info: {}
		}
		collectPageResources();
		DoLog(LogLevel.Info, pageResource, true)

		// Provide meta info copy
		metaCopy();

		// Provide txtfull download for copyright book
		enableDownload();

		// Provide tag search
		tagOption();

        // Ctrl+Enter comment submit
        areaReply();

		// Get page resources
		function collectPageResources() {
			collectElements();
			collectInfos();

			function collectElements() {
				const elements = pageResource.elements;
				elements.content = document.querySelector('#content');
				elements.bookMain = elements.content.querySelector('div');
				elements.header = elements.content.querySelector('div>table');
				elements.bookName = elements.header.querySelector('b');
				elements.metaContainer = elements.header.querySelector('tr+tr');
				elements.metas = elements.metaContainer.querySelectorAll('td');
				elements.info = elements.bookMain.querySelector('div+table');
				elements.infoText = elements.info.querySelector('td+td');
				elements.notice = elements.infoText.querySelectorAll('span.hottext>b');
				elements.tags = elements.notice.length > 1 ? elements.notice[0] : null;
				elements.notice = elements.notice[elements.notice.length-1];
				elements.introduce = elements.infoText.querySelectorAll('span');
				elements.introduce = elements.introduce[elements.introduce.length-1];
			}

			function collectInfos() {
				const info = pageResource.info;
				const elements = pageResource.elements;
				info.bookName = elements.bookName.innerText;
				info.BID = Number(location.href.match(/book\/(\d+).htm/)[1]);
				info.metas = []; elements.metas.forEach(function(meta){this.push(getKeyValue(meta.innerText));}, info.metas);
				info.notice = elements.notice.innerText;
				info.tags = elements.tags ? getKeyValue(elements.tags.innerText).VALUE.split(' ') : null;
				info.introduce = elements.introduce.innerText;
				info.dlEnabled = elements.content.querySelector('legend>b');
				info.dlEnabled = info.dlEnabled ? info.dlEnabled.innerText : false;
				info.dlEnabled = info.dlEnabled ? (info.dlEnabled.indexOf('TXT') !== -1 && info.dlEnabled.indexOf('UMD') !== -1 && info.dlEnabled.indexOf('JAR') !== -1) : false;
			}
		}

		// Copy meta info
		function metaCopy() {
			let tip = TEXT_TIP_COPY;
			for (let i = -1; i < pageResource.elements.metas.length; i++) {
				const meta = i !== -1 ? pageResource.elements.metas[i] : pageResource.elements.bookName;
				const info = i !== -1 ? pageResource.info.metas[i] : pageResource.info.bookName;
				const value = i !== -1 ? info.VALUE : info;
				meta.innerHTML += HTML_BOOK_COPY;
				const copyBtn = meta.querySelector('.'+CLASSNAME_BUTTON);
				copyBtn.addEventListener('click', function() {
					copyText(value);
					showTip(TEXT_TIP_COPIED);
					const alertBox = new ElegantAlertBox(TEXT_ALT_META_COPIED.replaceAll('{M}', value));
					alertBox.elm.onclick = function() {
						// Prevent copying box content
						alertBox.close.call(alertBox);
					}
				});

				if (tipready) {
					copyBtn.addEventListener('mouseover', function() {showTip(TEXT_TIP_COPY);});
					copyBtn.addEventListener('mouseout' , tiphide);
				} else {
					copyBtn.title = TEXT_TIP_COPY;
				}
			}

			function showTip(text) {
				tip = text;
				tipshow(tip);
			}
		}

		// Download copyright book
		function enableDownload() {
			if (pageResource.info.dlEnabled) {return false;};

			let div = document.createElement('div');
			pageResource.elements.bookMain.appendChild(div);
			div.outerHTML = HTML_DOWNLOAD_LINKS
				.replaceAll('{ORIBOOKNAME}', pageResource.info.bookName)
				.replaceAll('{BOOKID}', String(pageResource.info.BID))
				.replaceAll('{BOOKNAME}', encodeURIComponent(pageResource.info.bookName));
			div = document.querySelector('#txtfull');
			pageResource.elements.txtfull = div;

			pageResource.elements.notice.innerHTML = HTML_DOWNLOAD_BOARD
				.replaceAll('{ORIBOOKNAME}', pageResource.info.bookName);
		}

		// Tag Search
		function tagOption() {
			const tagsEle = pageResource.elements.tags;
			const tags = pageResource.info.tags;
			if (!tags) {return false;}

			let html = getKeyValue(tagsEle.innerText).KEY + ':';
			for (const tag of tags) {
				html += HTML_BOOK_TAG.replace('{TU}', $URL.encode(tag)).replace('{TN}', tag) + ' ';
			}
			tagsEle.innerHTML = html;
		}
    }

	// 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*-->$/, '');
			const titleEle = document.createElement('tr');
			const caption = table.querySelector('caption');
			table.insertBefore(titleEle, caption);
			titleEle.outerHTML = titleHTML;
        }

        const commentArea = document.querySelector('#pcontent'); if (!commentArea) {return false;};
        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);
        }

		/* ## Enable https protocol for inserted url ## */
		fixHTTPS();

		/* ## 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 fixHTTPS() {
			if (typeof(UBBEditor) === 'undefined') {
				DoLog(LogLevel.Info, 'fixHTTPS: UBBEditor not loaded, waiting...');
				setTimeout(fixHTTPS, NUMBER_ELEMENT_LOADING_WAIT_INTERVAL);
				return false;
			}
			const eid = 'pcontent';

			const menuItemInsertUrl = commentForm.querySelector('#menuItemInsertUrl');
			const menuItemInsertImage = commentForm.querySelector('#menuItemInsertImage');

			// Wait until menuItemInsertUrl and menuItemInsertImage is loaded
			if (!menuItemInsertUrl || !menuItemInsertImage) {
				DoLog(LogLevel.Info, 'fixHTTPS: element not loaded, waiting...');
				setTimeout(fixHTTPS, NUMBER_ELEMENT_LOADING_WAIT_INTERVAL);
				return false;
			}

			// Wait until original onclick function is set
			if (!menuItemInsertUrl.onclick || !menuItemInsertImage.onclick) {
				DoLog(LogLevel.Info, 'fixHTTPS: defult onclick not loaded, waiting...');
				setTimeout(fixHTTPS, NUMBER_ELEMENT_LOADING_WAIT_INTERVAL);
				return false;
			}

			menuItemInsertUrl.onclick = function () {
				var url = prompt("请输入超链接地址", "http://");
				if (url != null && url.indexOf("http://") < 0 && url.indexOf("https://") < 0) {
					alert("请输入完整的超链接地址!");
					return;
				}
				if (url != null) {
					if ((document.selection && document.selection.type == "Text") ||
						(window.getSelection &&
						 document.getElementById(eid).selectionStart > -1 && document.getElementById(eid).selectionEnd >
						 document.getElementById(eid).selectionStart)) {UBBEditor.InsertTag(eid, "url", url,'');}
					else {UBBEditor.InsertTag(eid, "url", url, url);}
				}
			};

			menuItemInsertImage.onclick = function () {
				var imgurl = prompt("请输入图片路径", "http://");
				if (imgurl != null && imgurl.indexOf("http://") < 0 && imgurl.indexOf("https://") < 0) {
					alert("请输入完整的图片路径!");
					return;
				}
				if (imgurl != null) {
					UBBEditor.InsertTag(eid, "img", "", imgurl);
				}
			};

			return true;
		}

		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();
		addQueryBtns();

		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 = '';

					for (const node of subNodes) {
						const type = node.nodeName;
						switch (type) {
							case '#text':
								// Prevent 'Quote:' repeat
								content += node.data.replace(/^Quote:$/, '');
								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', getFloorContent(node));
								break;
							case 'BR':
								// no need to add \n, because \n will be preserved in #text nodes
								//content += '\n';
								break;
							case 'DIV':
								if (node.classList.contains('jieqiQuote')) {
									content += getTagedSubcontent('quote', node);
								} else if (node.classList.contains('jieqiCode')) {
									content += getTagedSubcontent('code', node);
								}
								break;
							case 'CODE': content += getFloorContent(node); break; // Just ignore
							case 'PRE':  content += getFloorContent(node); break; // Just ignore
							case 'SPAN': content += getFontedSubcontent(node); break; // Size and color
							case 'P':    content += getFloorContent(node); break; // Text Align
							case 'B':    content += getTagedSubcontent('b', node); break;
							case 'I':    content += getTagedSubcontent('i', node); break;
							case 'U':    content += getTagedSubcontent('u', node); break;
							case 'DEL':  content += getTagedSubcontent('d', node); break;
							default:     content += getFloorContent(node); 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;

					function getTagedSubcontent(tag, node) {
						const subContent = getFloorContent(node);
						return '[{T}]{S}[/{T}]'.replaceAll('{T}', tag).replaceAll('{S}', subContent);
					}

					function getFontedSubcontent(node) {
						let strSize = node.style.fontSize.match(/\d+/);
						let strColor = node.style.color;
						strSize = strSize ? strSize[0] : null;
						strColor = strColor ? rgbToHex.apply(null, strColor.match(/\d+/g)) : null;
						const tag = strSize ? 'size' : (strColor ? 'color' : null);
						const value = strSize ? strSize : (strColor ? strColor : null);
						const subContent = getFloorContent(node);
						if (tag && value) {
							return '[{T}={V}]{S}[/{T}]'.replaceAll('{T}', tag).replaceAll('{V}', value).replaceAll('{S}', subContent);
						} else {
							return subContent;
						}
					}
				}
			}
		}

		function addQueryBtns() {
			// Get floor elements
			const avatars = main.querySelectorAll('table div img.avatar');
			for (const avatar of avatars) {
				// Get container div
				const div = avatar.parentElement;

				// Create buttons
				const qBtn = document.createElement('a'); // Button for query reviews
				const iBtn = document.createElement('a'); // Button for query userinfo

				// Get UID
				const user = div.querySelector('a');
				const UID = Number(user.href.match(/uid=(\d+)/)[1]);

				// Create text spliter
				const spliter = document.createTextNode(' | ');

				// Config buttons
				qBtn.href = URL_REVIEWSEARCH.replaceAll('{K}', String(UID));
				iBtn.href = URL_USERINFO    .replaceAll('{K}', String(UID));
				qBtn.target = '_blank';
				iBtn.target = '_blank';
				qBtn.innerText = TEXT_GUI_USER_REVIEWSEARCH;
				iBtn.innerText = TEXT_GUI_USER_USERINFO;

				// Append to GUI
				div.appendChild(document.createElement('br'));
				div.appendChild(iBtn);
				div.appendChild(qBtn);
				div.insertBefore(spliter, qBtn);
			}
		}

        /*
        // 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;
				new ElegantAlertBox(TEXT_ALT_DOWNLOADFINISH_REVIEW.replaceAll('{T}', data.title).replaceAll('{I}', data.id).replaceAll('{N}', name));

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

	// Bookcase page add-on
	function pageBookcase() {
		// Get bookcase lists
		const bookCaseURL = 'https://www.wenku8.net/modules/article/bookcase.php?classid={CID}';
		const content = document.querySelector('#content');
		const selector = document.querySelector('[name="classlist"]');
		const options = selector.children;
		// Current bookcase
		const curForm = content.querySelector('#checkform');
		const curClassid = Number(document.querySelector('[name="clsssid"]').value);
		const bookcases = readPreferences();
		addTopTitle();
		decorateForm(curForm, bookcases[curClassid]);

		// gowork
		showBookcases();

		function readPreferences() {
			let bookcases = GM_getValue(KEY_BOOKCASES, null);
			if (!bookcases) {
				bookcases = initPreferences();
			}
			return bookcases;
		}

		function initPreferences() {
			const lists = [];
			for (const option of options) {
				lists.push({
					classid: Number(option.value),
					url: bookCaseURL.replace('{CID}', String(option.value)),
					name: option.innerText
				})
			}
			savePreferences(lists);
			return lists;
		}

		function savePreferences(value) {
			GM_setValue(KEY_BOOKCASES, (value ? value : bookcases));
		}

		function addTopTitle() {
			// Clone title bar
			const checkform = document.querySelector('#checkform') ? document.querySelector('#checkform') : document.querySelector('.'+CLASSNAME_BOOKCASE_FORM);
			const oriTitle = checkform.querySelector('div.gridtop');
			const topTitle = oriTitle.cloneNode(true);
			content.insertBefore(topTitle, checkform);

			// Hide bookcase selector
			const bcSelector = topTitle.querySelector('[name="classlist"]');
			bcSelector.style.display = 'none';

			// Write title text
			const textNode = topTitle.childNodes[0];
			const numMatch = textNode.nodeValue.match(/\d+/g);
			const text = TEXT_GUI_BOOKCASE_TOPTITLE.replace('A', numMatch[0]).replace('B', numMatch[1]);
			textNode.nodeValue = text;
		}

		function showBookcases() {
			// GUI
			const topTitle = content.querySelector('script+div.gridtop');
			const textNode = topTitle.childNodes[0];
			const oriTitleText = textNode.nodeValue;
			const allCount = bookcases.length;
			let finished = 1;
			textNode.nodeValue = TEXT_GUI_BOOKCASE_GETTING.replace('C', String(finished)).replace('A', String(allCount));

			// Get all bookcase pages
			for (const bookcase of bookcases) {
				if (bookcase.classid === curClassid) {continue;};
				getDocument(bookcase.url, appendBookcase, [bookcase]);
			}

			function appendBookcase(mDOM, bookcase) {
				const classid = bookcase.classid;

				// Get bookcase form and modify it
				const form = mDOM.querySelector('#checkform');
				form.parentElement.removeChild(form);

				// Find the right place to insert it in
				const forms = content.querySelectorAll('.'+CLASSNAME_BOOKCASE_FORM);
				for (let i = 0; i < forms.length; i++) {
					const thisForm = forms[i];
					const cid = thisForm.classid ? thisForm.classid : curClassid;
					if (cid > classid) {
						content.insertBefore(form, thisForm);
						break;
					}
				}
				if(!form.parentElement) {content.appendChild(form);};

				// Decorate
				decorateForm(form, bookcase);

				// finished increase
				finished++;
				textNode.nodeValue = finished < allCount ?
					TEXT_GUI_BOOKCASE_GETTING.replace('C', String(finished)).replace('A', String(allCount)) :
					oriTitleText;
			}
		}

		function decorateForm(form, bookcase) {
			const classid = bookcase.classid;
			let name = bookcase.name;

			// Provide auto-recommand button
			arBtn();

			// Modify properties
			form.classList.add(CLASSNAME_BOOKCASE_FORM);
			form.id += String(classid);
			form.classid = classid;
			form.onsubmit = my_check_confirm;

			// Hide bookcase selector
			const bcSelector = form.querySelector('[name="classlist"]');
			bcSelector.style.display = 'none';

			// Change title
			const titleBar = bcSelector.parentElement;
			titleBar.childNodes[0].nodeValue = name;
			titleBar.addEventListener('dblclick', editName);

			// Show tips
			let tip = TEXT_GUI_BOOKCASE_DBLCLICK;
			if (tipready) {
                // tipshow and tiphide is coded inside wenku8 itself, its function is to show a text tip besides the mouse
                titleBar.addEventListener('mouseover', function() {tipshow(tip);});
                titleBar.addEventListener('mouseout' , tiphide);
            } else {
                titleBar.title = tip;
            }

			// Change selector names
			renameSelectors(false);

			// Replaces the original check_confirm() function
			function my_check_confirm() {
				const checkform = this;
				let checknum = 0;
				for (let i = 0; i < checkform.elements.length; i++){
					if (checkform.elements[i].name == 'checkid[]' && checkform.elements[i].checked == true) checknum++;
				}
				if (checknum === 0){
					alert('请先选择要操作的书目!');
					return false;
				}
				const newclassid = checkform.querySelector('#newclassid');
				if(newclassid.value == -1){
					if (confirm('确实要将选中书目移出书架么?')) {return true;} else {return false;};
				} else {
					return true;
				}
			}

			// Selector name refresh
			function renameSelectors(renameAll) {
				if (renameAll) {
					const forms = content.querySelectorAll('.'+CLASSNAME_BOOKCASE_FORM);
					for (const form of forms) {
						renameFormSlctr(form);
					}
				} else {
					renameFormSlctr(form);
				}

				function renameFormSlctr(form) {
					const newclassid = form.querySelector('#newclassid');
					const options = newclassid.children;
					for (let i = 0; i < options.length; i++) {
						const option = options[i];
						const value = Number(option.value);
						const bc = bookcases[value];
						bc ? option.innerText = TEXT_GUI_BOOKCASE_MOVEBOOK.replace('N', bc.name) : function(){};
					}
				}
			}

			// Provide <input> GUI to edit bookcase name
			function editName() {
				const nameInput = document.createElement('input');
				const form = this;
				tip = TEXT_GUI_BOOKCASE_WHATNAME;
				tipready ? tipshow(tip) : function(){};

				titleBar.childNodes[0].nodeValue = '';
				titleBar.appendChild(nameInput);
				nameInput.value = name;
				nameInput.addEventListener('blur', onblur);
				nameInput.addEventListener('keydown', onkeydown)
				nameInput.focus();
				nameInput.setSelectionRange(0, name.length);

				function onblur() {
					tip = TEXT_GUI_BOOKCASE_DBLCLICK;
					tipready ? tipshow(tip) : function(){};
					const value = nameInput.value.trim();
					if (value) {
						name = value;
						bookcase.name = name;
						savePreferences();
					}
					titleBar.childNodes[0].nodeValue = name;
					titleBar.removeChild(nameInput);
					renameSelectors(true);
				}

				function onkeydown(e) {
					if (e.keyCode === 13) {
						e.preventDefault();
						onblur();
					}
				}
			}

			// Provide auto-recommend option
			function arBtn() {
				let arConfig = GM_getValue(KEY_ATRCMMDS, initConfig());
				const table = form.querySelector('table');
				for (const tr of table.querySelectorAll('tr')) {
					tr.querySelector('.odd') ? decorateRow(tr) : function() {};
					tr.querySelector('th') ? decorateHeader(tr) : function() {};
					tr.querySelector('td.foot') ? decorateFooter(tr) : function() {};
				}

				// Insert auto-recommend option for given row
				function decorateRow(tr) {
					const eleBookLink = tr.querySelector('td:nth-child(2)>a');
					const strBookID = eleBookLink.href.match(/aid=(\d+)/)[1];
					const strBookName = eleBookLink.innerText;
					const newTd = document.createElement('td');
					const input = document.createElement('input');
					newTd.classList.add('odd');
					input.type = 'number';
					input.inputmode = 'numeric';
					input.style.width = '85%';
					input.value = arConfig.books[strBookID] ? String(arConfig.books[strBookID].number) : '0';
					input.addEventListener('change', onvaluechange);
					input.strBookID = strBookID; input.strBookName = strBookName;
					newTd.appendChild(input); tr.appendChild(newTd);
				}

				// Insert a new row for auto-recommend options
				function decorateHeader(tr) {
					const allTh = tr.querySelectorAll('th');
					const width = ARR_GUI_BOOKCASE_WIDTH;
					const newTh = document.createElement('th');
					newTh.innerText = TEXT_GUI_BOOKCASE_ATRCMMD;
					newTh.classList.add(CLASSNAME_TEXT);
					tr.appendChild(newTh);
					for (let i = 0; i < allTh.length; i++) {
						const th = allTh[i];
						th.style.width = width[i];
					}
				}

				// Fit the width
				function decorateFooter(tr) {
					const td = tr.querySelector('td.foot');
					td.colSpan = ARR_GUI_BOOKCASE_WIDTH.length;
				}

				// auto-recommend onvaluechange
				function onvaluechange(e) {
					arConfig = GM_getValue(KEY_ATRCMMDS, initConfig());
					const input = e.target;
					const value = input.value;
					const strBookID = input.strBookID;
					const strBookName = input.strBookName;
					const bookID = Number(strBookID);
					const userDetail = getMyUserDetail() ? getMyUserDetail().userDetail : refreshMyUserDetail();
					if (isNumeric(value, true) && Number(value) >= 0) {
						// allCount increase
						const oriNum = arConfig.books[strBookID] ? arConfig.books[strBookID].number : 0;
						const number = Number(value);
						arConfig.allCount += number - oriNum;

						// save to config
						number > 0 ? arConfig.books[strBookID] = {number: number, name: strBookName, id: bookID} : delete arConfig.books[strBookID];
						GM_setValue(KEY_ATRCMMDS, arConfig);

						// alert
						new ElegantAlertBox(TEXT_ALT_ATRCMMDS_SAVED.replaceAll('{N}', value));
						if (userDetail && arConfig.allCount > userDetail.vote) {new ElegantAlertBox(TEXT_ALT_ATRCMMDS_OVERFLOW.replace('{V}', String(userDetail.vote)));};
					} else {
						// invalid input value, alert
						new ElegantAlertBox(TEXT_ALT_ATRCMMDS_INVALID.replaceAll('{N}', value));
					}
				}

				// Auto-recommend config init
				function initConfig() {
					const arConfig = {};
					arConfig[KEY_ATRCMMDS_VERSION] = VALUE_ATRCMMDS_VERSION;
					arConfig.allCount = 0;
					arConfig.books = {};
					return arConfig;
				}
			}
		}
	}

	// Novel ads remover
	function removeTopAds() {
		const ads = []; document.querySelectorAll('div>script+script+a').forEach(function(a) {ads.push(a.parentElement);});
		for (const ad of ads) {
			ad.parentElement.removeChild(ad);
		}
	}

	// Novel index page add-on
	function pageNovelIndex() {
		removeTopAds();
	}

    // Novel page add-on
    function pageNovel() {
		const pageResource = {elements: {}, infos: {}, download: {}};
		collectPageResources(); DoLog(LogLevel.Info, pageResource, true)

		// Remove ads
		removeTopAds();

		// Provide download GUI
		downloadGUI();

        // Prevent URL.revokeObjectURL in script 轻小说文库下载
        revokeObjectURLHOOK();

		function collectPageResources() {
			collectElements();
			collectInfos();
			initDownload();

			function collectElements() {
				const elements = pageResource.elements;
				elements.title          = document.querySelector('#title');
				elements.images         = document.querySelectorAll('.imagecontent');
				elements.rightButtonDiv = document.querySelector('#linkright');
				elements.rightNodes     = elements.rightButtonDiv.childNodes;
				elements.rightBlank     = elements.rightNodes[elements.rightNodes.length-1];
				elements.content        = document.querySelector('#content');
				elements.spliterDemo    = document.createTextNode(' | ');
			}
			function collectInfos() {
				const elements = pageResource.elements;
				const infos    = pageResource.infos;
				infos.title       = elements.title.innerText;
				infos.isImagePage = elements.images.length > 0;
				infos.content     = infos.isImagePage ? null : elements.content.innerText;
			}
			function initDownload() {
				const elements = pageResource.elements;
				const download = pageResource.download;
				download.running  = false;
				download.finished = 0;
				download.all      = elements.images.length;
				download.error    = 0;
			}
		}

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

		// Provide download GUI
		function downloadGUI() {
			const elements = pageResource.elements;
			const infos    = pageResource.infos;
			const download = pageResource.download;

			// Create donwload button
			const dlBtn = elements.downloadBtn = document.createElement('span');
			dlBtn.classList.add(CLASSNAME_BUTTON);
			dlBtn.addEventListener('click', infos.isImagePage ? dlNovelImages : dlNovelText);
			dlBtn.innerText = infos.isImagePage ? TEXT_GUI_DOWNLOAD_IMAGE : TEXT_GUI_DOWNLOAD_TEXT;

			// Create spliter
			const spliter = elements.spliterDemo.cloneNode();

			// Append to rightButtonDiv
			elements.rightButtonDiv.style.width = '550px';
			elements.rightButtonDiv.insertBefore(spliter, elements.rightBlank);
			elements.rightButtonDiv.insertBefore(dlBtn,   elements.rightBlank);

			function dlNovelImages() {
				if (download.running) {return false;};
				download.running = true; download.finished = 0; download.error = 0;
				updateDownloadStatus();

				const lenNumber = String(elements.images.length).length;
				for (let i = 0; i < elements.images.length; i++) {
					const img = elements.images[i];
					const name = infos.title + '_' + fillNumber(i+1, lenNumber) + '.jpg';
					GM_xmlhttpRequest({
						url: img.src,
						responseType: 'blob',
						onloadstart: function() {
							DoLog(LogLevel.Info, '[' + String(i) + ']downloading novel image from ' + img.src);
						},
						onload: function(e) {
							DoLog(LogLevel.Info, '[' + String(i) + ']image got: ' + img.src);

							const image = new Image();
							image.onload = function() {
								const url = toImageFormatURL(image, 1);
								DoLog(LogLevel.Info, '[' + String(i) + ']image transformed: ' + img.src);

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

								download.finished++;
								updateDownloadStatus();
								// Code below seems can work, but actually it doesn't work well and somtimes some file cannot be saved
								// The reason is still unknown, but from what I know I can tell that mistakes happend in GM_xmlhttpRequest
								// Error stack: GM_xmlhttpRequest.onload ===> image.onload ===> downloadFile ===> GM_xmlhttpRequest =X=> .onload
								// This Error will also stuck the GMXHRHook.ongoingList
								/*downloadFile({
									url: url,
									name: name,
									onload: function() {
										download.finished++;
										DoLog(LogLevel.Info, '[' + String(i) + ']file saved: ' + name);
										alert('[' + String(i) + ']file saved: ' + name);
										updateDownloadStatus();
									},
									onerror: function() {
										alert('downloadfile error! url = ' + String(url) + ', i = ' + String(i));
									}
								})*/
							}
							image.onerror = function() {
								alert('image load error! image.src = ' + String(image.src) + ', i = ' + String(i));
							}
							image.src = URL.createObjectURL(e.response);
						},
						onerror: function(e) {
							// Error dealing need...
							DoLog(LogLevel.Error, '[' + String(i) + ']image fetch error: ' + img.src);
							download.error++;
						}
					})
				}

				function updateDownloadStatus() {
					elements.downloadBtn.innerText = TEXT_GUI_DOWNLOADING_ALL.replaceAll('C', String(download.finished)).replaceAll('A', String(download.all));
					if (download.finished === download.all) {
						DoLog(LogLevel.Success, 'All images got.');
						elements.downloadBtn.innerText = TEXT_GUI_DOWNLOADED_ALL;
						download.running = false;
					}
				}
			}

			function dlNovelText() {
				const name = infos.title + '.txt';
				const text = infos.content.replaceAll(/[\r\n]+/g, '\r\n');
				downloadText(text, name);
			}
		}

        // Image format changing function
		// image: <img> or Image(); format: 1 for jpeg, 2 for png, 3 for webp
        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);
        }
    }

	// Search form add-on
	function formSearch() {
		const searchForm = document.querySelector('form[name="articlesearch"]');
		if (!searchForm) {return false;};
		const typeSelect = searchForm.querySelector('#searchtype');
		const searchText = searchForm.querySelector('#searchkey');
		const searchSbmt = searchForm.querySelector('input[class="button"][type="submit"]');

		let optionTags;
		provideTagOption();
		onsubmitHOOK();

		function provideTagOption() {
			optionTags = document.createElement('option');
			optionTags.value = VALUE_STR_NULL;
			optionTags.innerText = TEXT_GUI_SEARCH_OPTION_TAG;
			typeSelect.appendChild(optionTags);

			if (tipready) {
				// tipshow and tiphide is coded inside wenku8 itself, its function is to show a text tip besides the mouse
				typeSelect.addEventListener('mouseover', show);
				searchSbmt.addEventListener('mouseover', show);
				typeSelect.addEventListener('mouseout' , tiphide);
				searchSbmt.addEventListener('mouseout' , tiphide);
			} else {
				typeSelect.title = TEXT_TIP_SEARCH_OPTION_TAG;
				searchSbmt.title = TEXT_TIP_SEARCH_OPTION_TAG;
			}

			function show() {
				optionTags.selected ? tipshow(TEXT_TIP_SEARCH_OPTION_TAG) : function() {};
			}
		}
		function onsubmitHOOK() {
			const onsbmt = searchForm.onsubmit;
			searchForm.onsubmit = function() {
				if (optionTags.selected) {
					// DON'T USE window.open()!
					// Wenku8 has no window.open used in its own scripts, so do not use it in userscript either.
					// It might cause security problems.
					//window.open('https://www.wenku8.net/modules/article/tags.php?t=' + $URL.encode(searchText.value));
					if (typeof($URL) === 'undefined' ) {
						$URLError();
						return true;
					} else {
						GM_openInTab(URL_TAGSEARCH.replace('{TU}', $URL.encode(searchText.value)), {
							active: true, insert: true, setParent: true, incognito: false
						});
						return false;
					}
				}
			}

			function $URLError() {
				DoLog(LogLevel.Error, '$URL(from gbk.js) is not loaded.');
				DoLog(LogLevel.Warning, 'Search as plain text instead.');

				// Search as plain text instead
				for (const node of typeSelect.childNodes) {
					node.selected = (node.tagName === 'OPTION' && node.value === 'articlename') ? true : false;
				}
			}
		}
	}

	// Tags page add-on
	function pageTags() {
	}

	// User page add-on
	function pageUser() {
		const UID = Number(getUrlArgv('uid'));

		// Provide review search option
		reviewButton();

		// Review search option
		function reviewButton() {
			// clone button and container div
			const oriContainer = document.querySelectorAll('.blockcontent .userinfo')[0].parentElement;
			const container = oriContainer.cloneNode(true);
			const button = container.querySelector('a');
			button.innerText = TEXT_GUI_USER_REVIEWSEARCH;
			button.href = URL_REVIEWSEARCH.replaceAll('{K}', String(UID));
			oriContainer.parentElement.appendChild(container);
		}
	}

	// 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

		// Get novel info
		const novelInfo = {}; collectNovelInfo();
		const myDlBtns = [];

		// Donwload GUI
		downloadGUI();

        // Server GUI
        serverGUI();

        /* ******************* Code ******************* */
		function collectNovelInfo() {
			novelInfo.novelName = document.querySelector('html body div.main div#centerm div#content table.grid caption a').innerText;
			novelInfo.displays = getAllNameEles();
			novelInfo.volumeNames = getAllNames();
			novelInfo.type = getUrlArgv('type');
			novelInfo.ext = novelInfo.type !== 'txtfull' ? novelInfo.type : 'txt';
		}

		// Donwload GUI
		function downloadGUI() {
			// Only txt is really separated by volumes
			if (novelInfo.type !== 'txt') {return false;};

			// define vars
			let i;

			const tbody = document.querySelector('table>tbody');
			const header = tbody.querySelector('th').parentElement;
			const thead = header.querySelector('th');

			// Append new th
			const newHead = thead.cloneNode(true);
			newHead.innerText = TEXT_GUI_SDOWNLOAD;
			thead.width = '40%';
			header.appendChild(newHead);

			// Append new td
			const trs = tbody.querySelectorAll('tr');
			for (i = 1; i < trs.length; i++) { /* i = 1 to trs.length-1: skip header */
				const index = i-1;
				const tr = trs[i];
				const newTd = tr.querySelector('td.even').cloneNode(true);
				const links = newTd.querySelectorAll('a');
				for (const a of links) {
					a.classList.add(CLASSNAME_BUTTON);
					a.info = {
						description: 'volume download button',
						name: novelInfo.volumeNames[index],
						filename: '{NovelName} {VolumeName}.{Extension}'
							.replace('{NovelName}', novelInfo.novelName)
							.replace('{VolumeName}', novelInfo.volumeNames[index])
							.replace('{Extension}', novelInfo.ext),
						index: index,
						display: novelInfo.displays[index]
					}
					a.onclick = downloadOnclick;
					myDlBtns.push(a);
				}
				tr.appendChild(newTd);
			}

			// Append new tr, provide batch download
			const newTr = trs[trs.length-1].cloneNode(true);
			const newTds = newTr.querySelectorAll('td');
			newTds[0].innerText = TEXT_GUI_DOWNLOADALL;
			//clearChildnodes(newTds[1]); clearChildnodes(newTds[2]);
			newTds[1].innerHTML = newTds[2].innerHTML = TEXT_GUI_NOTHINGHERE;
			tbody.insertBefore(newTr, tbody.children[1]);

			const allBtns = newTds[3].querySelectorAll('a');
			for (i = 0; i < allBtns.length; i++) {
				const a = allBtns[i];
				a.href = 'javascript:void(0);';
				a.info = {
					description: 'download all button',
					index: i
				}
				a.onclick = downloadAllOnclick;
			}
		}

		// Download button onclick
		function downloadOnclick() {
			const a = this;
			a.info.display.innerText = a.info.name + TEXT_GUI_WAITING;
			downloadFile({
				url: a.href,
				name: a.info.filename,
				onloadstart: function(e) {
					a.info.display.innerText = a.info.name + TEXT_GUI_DOWNLOADING;
				},
				onload: function(e) {
					a.info.display.innerText = a.info.name + TEXT_GUI_DOWNLOADED;
				}
			});
			return false;
		}

		// DownloadAll button onclick
		function downloadAllOnclick() {
			const a = this;
			const index = (a.info.index+1)%3;
			for (let i = 0; i < myDlBtns.length; i++) {
				if ((i+1)%3 !== index) {continue;};
				const btn = myDlBtns[i];
				btn.click();
			}
			return false;
		}

		// Get all name display elements
		function getAllNameEles() {
            return document.querySelectorAll('.grid tbody tr .odd');
        }

		// Get all names
		function getAllNames() {
            const all = getAllNameEles()
            const names = [];
            for (let i = 0; i < all.length; i++) {
                names[i] = all[i].innerText;
            }
            return names;
        }

		// Server GUI
		function serverGUI() {
			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 (tipready) {
					// 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;
				}
			}
		}

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

	// isAPIPage page add-on
	function pageAPI(API) {
		DoLog(LogLevel.Info, 'This is wenku API page.');
		DoLog(LogLevel.Info, 'API is: [' + API + ']');
		DoLog(LogLevel.Info, 'There is nothing to do. Quiting...');
	}

	// Check if current page is an wenku API page ('处理成功', '出现错误!')
	function isAPIPage() {
		// API page has just one .block div and one close-page button
		const block = document.querySelectorAll('.block');
		const close = document.querySelectorAll('a[href="javascript:window.close()"]');
		return block.length === 1 && close.length === 1;
	}

	// getMyUserDetail with soft alerts
	function refreshMyUserDetail() {
		new ElegantAlertBox(TEXT_ALT_USRDTL_REFRESH);
		getMyUserDetail(function() {
			const alertBox = new ElegantAlertBox(TEXT_ALT_USRDTL_REFRESHED);

			// rewrite onclick function from copying to showing details
			alertBox.elm.onclick = function() {
				alertBox.close.call(alertBox);
				new ElegantAlertBox(JSON.stringify(getMyUserDetail()));
			}
		})
	}

	// Get my user info detail
	// if no argument provided, this function will just read userdetail from gm_storage
	// otherwise, the function will make a http request to get the latest userdetail
	// if no argument provided and no gm_storage record, then will just return false
	// if callback is not a function, then will just request&store but not callback
	function getMyUserDetail(callback, args=[]) {
		if (callback) {
			getDocument(URL_USRDETALE, function(oDoc, callback, ...args) {
				const content = oDoc.querySelector('#content');
				const userDetail = {
					userID: Number(content.querySelector('tr:nth-child(1)>.even').innerText),  // '用户ID'
					userLink: content.querySelector('tr:nth-child(2)>.even').innerText,        // '推广链接'
					userName: content.querySelector('tr:nth-child(3)>.even').innerText,        // '用户名'
					displayName: content.querySelector('tr:nth-child(4)>.even').innerText,     // '用户昵称'
					userType: content.querySelector('tr:nth-child(5)>.even').innerText,        // '等级'
					userGrade: content.querySelector('tr:nth-child(6)>.even').innerText,       // '头衔'
					gender: content.querySelector('tr:nth-child(7)>.even').innerText,          // '性别'
					email: content.querySelector('tr:nth-child(8)>.even').innerText,           // 'Email'
					qq: content.querySelector('tr:nth-child(9)>.even').innerText,              // 'QQ'
					msn: content.querySelector('tr:nth-child(10)>.even').innerText,            // 'MSN'
					site: content.querySelector('tr:nth-child(11)>.even').innerText,           // '网站'
					signupDate: content.querySelector('tr:nth-child(13)>.even').innerText,     // '注册日期'
					contibute: content.querySelector('tr:nth-child(14)>.even').innerText,      // '贡献值'
					exp: content.querySelector('tr:nth-child(15)>.even').innerText,            // '经验值'
					credit: content.querySelector('tr:nth-child(16)>.even').innerText,         // '现有积分'
					friends: content.querySelector('tr:nth-child(17)>.even').innerText,        // '最多好友数'
					mailbox: content.querySelector('tr:nth-child(18)>.even').innerText,        // '信箱最多消息数'
					bookcase: content.querySelector('tr:nth-child(19)>.even').innerText,       // '书架最大收藏量'
					vote: content.querySelector('tr:nth-child(20)>.even').innerText,           // '每天允许推荐次数'
					sign: content.querySelector('tr:nth-child(22)>.even').innerText,           // '用户签名'
					intoduction: content.querySelector('tr:nth-child(23)>.even').innerText,    // '个人简介'
					userImage: content.querySelector('tr>td>img').src                          // '头像'
				}

				// Save to gm_storage
				const storage = {userDetail: userDetail};
				storage[KEY_USRDETAIL_VERSION] = VALUE_USRDETAIL_VERSION;
				storage.lasttime = getTime('-', false);
				GM_setValue(KEY_USRDETAIL, storage);

				// Callback
				typeof(callback) === 'function' ? callback.apply(null, args) : function() {};
			}, [callback].concat(args))
			return true;
		} else {
			const storage = GM_getValue(KEY_USRDETAIL, null);
			if (!storage) {
				DoLog(LogLevel.Warning, 'Attempt to read userDetail from gm_storage but no record found');
				return false;
			};
			const userDetail = storage;
			return userDetail;
		}
	}

	// Check if tipobj is ready, if not, then make it
	function tipcheck() {
		DoLog(LogLevel.Info, 'checking tipobj...');
		if (typeof(tipobj) === 'object' && tipobj !== null) {
			DoLog(LogLevel.Info, 'tipobj ready...');
			return true;
		} else {
			DoLog(LogLevel.Warning, 'tipobj not ready');
			if (typeof(tipinit) === 'function') {
				DoLog(LogLevel.Success, 'tipinit executed');
				tipinit();
				return true;
			} else {
				DoLog(LogLevel.Error, 'tipinit not found');
				return false;
			}
		}
	}

	// Create a left .block operatingArea
	function createLeftBlock(title=TEXT_GUI_BLOCK_TITLE_DEFULT) {
		const blockEle = document.querySelector('#left>.block').cloneNode(true);
		const titleEle = blockEle.querySelector('.blocktitle>.txt');
		const cntntEle = blockEle.querySelector('.blockcontent');

		titleEle.innerText = title;
		clearChildnodes(cntntEle);
		return blockEle;
	}

	// Remove all childnodes from an element
	function clearChildnodes(element) {
		const cns = []
		for (const cn of element.childNodes) {
			cns.push(cn);
		}
		for (const cn of cns) {
			element.removeChild(cn);
		}
	}

	// 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,故编码问题已经解决,可以正常使用任何字符
                */
            }
        })
    }

	// Save dataURL to file
	function saveFile(dataURL, filename) {
		const a = document.createElement('a');
		a.href = dataURL;
		a.download = filename;
		a.click();
	}

	// File download function
	// details looks like the detail of GM_xmlhttpRequest
	// onload function will be called after file saved to disk
	function downloadFile(details) {
		if (!details.url || !details.name) {return false;};

		// Configure request object
		const requestObj = {
			url: details.url,
			responseType: 'blob',
			onload: function(e) {
				// Save file
				saveFile(URL.createObjectURL(e.response), details.name);

				// onload callback
				details.onload ? details.onload(e) : function() {};
			}
		}
		if (details.onloadstart       ) {requestObj.onloadstart        = details.onloadstart;};
		if (details.onprogress        ) {requestObj.onprogress         = details.onprogress;};
		if (details.onerror           ) {requestObj.onerror            = details.onerror;};
		if (details.onabort           ) {requestObj.onabort            = details.onabort;};
		if (details.onreadystatechange) {requestObj.onreadystatechange = details.onreadystatechange;};
		if (details.ontimeout         ) {requestObj.ontimeout          = details.ontimeout;};

		// Send request
		GM_xmlhttpRequest(requestObj);
	}

	// Save text to textfile
	function downloadText(text, name) {
		if (!text || !name) {return false;};

		// Get blob url
		const blob = new Blob([text],{type:"text/plain;charset=utf-8"});
		const url = URL.createObjectURL(blob);

		// Create <a> and download
		const a = document.createElement('a');
		a.href = url;
		a.download = name;
		a.click();
	}

	// 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
	// if dateSpliter provided false, there will be no date part. The same for timeSpliter.
    function getTime(dateSpliter='-', timeSpliter=':') {
        const d = new Date();
		let fulltime = ''
		fulltime += dateSpliter ? fillNumber(d.getFullYear(), 4) + dateSpliter + fillNumber((d.getMonth() + 1), 2) + dateSpliter + fillNumber(d.getDate(), 2) : '';
		fulltime += dateSpliter && timeSpliter ? ' ' : '';
		fulltime += timeSpliter ? fillNumber(d.getHours(), 2) + timeSpliter + fillNumber(d.getMinutes(), 2) + timeSpliter + fillNumber(d.getSeconds(), 2) : '';
        return fulltime;
    }

	// Get key-value object from text like 'key: value'/'key:value'/' key   :  value   '
	// returns: {key: value, KEY: key, VALUE: value}
	function getKeyValue(text, delimiters=[':', ':', ',']) {
		// Modify from https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Error#examples
		// Create a new object, that prototypally inherits from the Error constructor.
		function SplitError(message) {
			this.name = 'SplitError';
			this.message = message || 'SplitError Message';
			this.stack = (new Error()).stack;
		}
		SplitError.prototype = Object.create(Error.prototype);
		SplitError.prototype.constructor = SplitError;

		if (!text) {return [];};

		const result = {};
		let key, value;
		for (let i = 0; i < text.length; i++) {
			const char = text.charAt(i);
			for (const delimiter of delimiters) {
				if (delimiter === char) {
					if (!key && !value) {
						key = text.substr(0, i).trim();
						value = text.substr(i+1).trim();
						result[key] = value;
						result.KEY = key;
						result.VALUE = value;
					} else {
						throw new SplitError('Mutiple Delimiter in Text');
					}
				}
			}
		}

		return result;
	}

	// Convert rgb color(e.g. 51,51,153) to hex color(e.g. '333399')
	function rgbToHex(r, g, b) {return ((r << 16) | (g << 8) | b).toString(16);}

    // 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, disableFloat=false) {
        const result = Number(str);
        return !isNaN(result) && str !== '' && (!disableFloat || result===Math.floor(result));
    }

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

	// Clone(deep) an object variable
	// Returns the new object
	function deepclone(obj) {
		if (obj === null) return null;
		if (typeof(obj) !== 'object') return obj;
		if (obj.constructor === Date) return new Date(obj);
		if (obj.constructor === RegExp) return new RegExp(obj);
		var newObj = new obj.constructor(); //保持继承的原型
		for (let key in obj) {
			if (obj.hasOwnProperty(key)) {
				const val = obj[key];
				newObj[key] = typeof val === 'object' ? deepclone(val) : val;
			}
		}
		return newObj;
	}

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