/* eslint-disable no-multi-spaces */
// ==UserScript==
// @name 轻小说文库+
// @namespace Wenku8+
// @version 1.2.4.2
// @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 https://greasyfork.org/scripts/427726-gbk-url-js/code/GBK_URLjs.js?version=939169
// @require https://greasyfork.org/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.2';
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&id={BOOKID}">G版原始下载</a></div><div style="width:16%; float:left; text-align:center;"><a href="http://dl.wenku8.com/down.php?type=txt&id={BOOKID}&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&id={BOOKID}">U版原始下载</a></div><div style="width:16%; float:left; text-align:center;"><a href="http://dl.wenku8.com/down.php?type=utf8&id={BOOKID}&fname={BOOKNAME}">U版自动重命名</a></div><div style="width:16%; float:left; text-align:center;"><a href="http://dl.wenku8.com/down.php?type=big5&id={BOOKID}">繁体原始下载</a></div><div style="width:16%; float:left; text-align:center;"><a href="http://dl.wenku8.com/down.php?type=big5&id={BOOKID}&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 = '已保存:《{B}》</br>每日自动推荐{N}次</br>每日还可推荐{R}次';
const TEXT_ALT_ATRCMMDS_INVALID = '未保存:{N}不是非负整数';
const TEXT_ALT_ATRCMMDS_OVERFLOW = '注意:</br>您的用户信息显示您每天最多推荐{V}票</br>当前您已设置每日推荐合计{C}票</br>[单击此处以立即更新您的用户信息]';
const TEXT_ALT_ATRCMMDS_AUTO = '已开启自动推书';
const TEXT_ALT_ATRCMMDS_NOAUTO = '已关闭自动推书';
const TEXT_ALT_ATRCMMDS_ALL_START = '{S}:正在自动推书...'.replaceAll('{S}', GM_info.script.name);
const TEXT_ALT_ATRCMMDS_RUNNING = '正在推荐书目:</br>{BN}({BID})';
const TEXT_ALT_ATRCMMDS_DONE = '推荐完成:</br>{BN}({BID})';
const TEXT_ALT_ATRCMMDS_ALL_DONE = '全部书目推荐完成:</br>{R}';
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_BOOKCASE_RCMMDAT = '<span>每日自动推书:</span>';
const TEXT_GUI_BOOKCASE_RCMMDNW = '立即推书';
const TEXT_GUI_BOOKCASE_RCMMDNW_DONE = '今日推书已完成';
const TEXT_GUI_BOOKCASE_RCMMDNW_NOTYET = '今日尚未推书';
const TEXT_GUI_BOOKCASE_RCMMDNW_CONFIRM = '今天已经推过书了,是否要再推一遍?';
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: https://greasyfork.org/zh-CN/users/63073
* [GreasyFork] Pixiv Previewer: https://greasyfork.org/zh-CN/scripts/30766
* [GitHub] Pixiv Previewer: https://github.com/Ocrosoft/PixivPreviewer
**/
let LogLevel = {
None: 0,
Error: 1,
Success: 2,
Warning: 3,
Info: 4,
Elements: 5,
};
let g_logCount = 0;
let g_logLevel = LogLevel.Warning; // Info Warning Success Error
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);
const TASK = new taskManager();
// 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
// use 'new' keyword
function taskManager() {
const TM = this;
// UserDetail refresh
TM.UserDetail = {
// Refresh userDetail storage everyday
refresh: function() {
// Time check: whether recommend has done today
if (getMyUserDetail().lasttime === getTime('-', false)) {return false;};
refreshMyUserDetail();
}
}
// Auto-recommend
TM.AutoRecommend = {
// config init
initConfig: function(save) {
const arConfig = {};
arConfig[KEY_ATRCMMDS_VERSION] = VALUE_ATRCMMDS_VERSION;
arConfig.allCount = 0;
arConfig.books = {};
arConfig.auto = true;
save ? GM_getValue(KEY_ATRCMMDS, arConfig) : function() {};
return arConfig;
},
// Auto-recommend config upgrade
upgradeConfig: function() {
// Get config
const arConfig = GM_getValue(KEY_ATRCMMDS, TM.AutoRecommend.initConfig(true));
switch (arConfig[KEY_ATRCMMDS_VERSION]) {
case '0.1':
arConfig.auto = true;
DoLog(LogLevel.Success, 'Auto-recommend config successfully updated From v0.1 to {V}. '.replaceAll('{V}', VALUE_ATRCMMDS_VERSION));
break;
case VALUE_ATRCMMDS_VERSION:
DoLog(LogLevel.Info, 'Auto-recommend config is in latest version. ');
break
default:
DoLog(LogLevel.Error, 'configCheckUpgrade: Invalid config version for Auto-recommend. ');
}
// Version info rewrite
arConfig[KEY_ATRCMMDS_VERSION] = VALUE_ATRCMMDS_VERSION;
// Save to gm_storage
GM_setValue(KEY_ATRCMMDS, arConfig);
},
// Check if recommend has done
checkRcmmd: function() {
const arConfig = GM_getValue(KEY_ATRCMMDS, TM.AutoRecommend.initConfig(true));
return arConfig.lasttime === getTime('-', false);
},
// Auto recommend main function
run: function(recommendAnyway=false) {
let i;
// Get config
const arConfig = GM_getValue(KEY_ATRCMMDS, TM.AutoRecommend.initConfig(true));
// Time check: whether all recommends has done today
if (TM.AutoRecommend.checkRcmmd() && !recommendAnyway) {return false;};
// Config check: whether we need to auto-recommend
if (!arConfig.auto && !recommendAnyway) {return false;}
// Recommend for each
let recommended = '', count = 0, allCount = 0;
new ElegantAlertBox(TEXT_ALT_ATRCMMDS_ALL_START);
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) && !recommendAnyway) {continue;};
// Soft alert
//new ElegantAlertBox(TEXT_ALT_ATRCMMDS_RUNNING.replaceAll('{BN}', bookName).replaceAll('{BID}', strBookID));
// Go work
for (i = 0; i < number; i++) {
allCount++;
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
count++;
book.lasttime = getTime('-', false);
count === allCount ? arConfig.lasttime = getTime('-', false) : function() {};
GM_setValue(KEY_ATRCMMDS, arConfig);
// GUI
recommended += '</br>{BID} - {BN}'.replaceAll('{BN}', bookName).replaceAll('{BID}', strBookID);
count === allCount ? new ElegantAlertBox(TEXT_ALT_ATRCMMDS_ALL_DONE.replaceAll('{R}', recommended)) : function() {};
// Log
DoLog(LogLevel.Info, statusText);
DoLog(LogLevel.Info, returnText);
})
}
// Soft alert
//new ElegantAlertBox(TEXT_ALT_ATRCMMDS_DONE.replaceAll('{BN}', bookName).replaceAll('{BID}', strBookID));
}
return true;
}
}
TM.UserDetail.refresh();
TM.AutoRecommend.upgradeConfig();
TM.AutoRecommend.run();
}
// 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(/^\s*Quote:\s*$/, '');
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);
} else if (node.classList.contains('divimage')) {
content += getFloorContent(node);
} else {
content += getFloorContent(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 auto-recommend config
let arConfig = GM_getValue(KEY_ATRCMMDS, TASK.AutoRecommend.initConfig(true));
// 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();
recommendAllGUI();
function recommendAllGUI() {
const block = createLeftBlock(TEXT_GUI_BOOKCASE_ATRCMMD, true, {
type: 'mypage',
links: [
{innerHTML: TASK.AutoRecommend.checkRcmmd() ? TEXT_GUI_BOOKCASE_RCMMDNW_DONE : TEXT_GUI_BOOKCASE_RCMMDNW_NOTYET, id: 'arstatus'},
{innerHTML: TEXT_GUI_BOOKCASE_RCMMDAT, id: 'autorcmmd'},
{innerHTML: TEXT_GUI_BOOKCASE_RCMMDNW, id: 'rcmmdnow'}
]
})
// Configure buttons
const ulitm = block.querySelector('.ulitem');
const txtst = block.querySelector('#arstatus');
const btnAR = block.querySelector('#autorcmmd');
const btnRN = block.querySelector('#rcmmdnow');
const txtAR = btnAR.querySelector('span');
const checkbox = document.createElement('input');
txtst.classList.add(CLASSNAME_TEXT);
btnAR.classList.add(CLASSNAME_BUTTON);
btnRN.classList.add(CLASSNAME_BUTTON);
checkbox.type = 'checkbox';
checkbox.checked = arConfig.auto;
checkbox.addEventListener('click', onclick);
btnAR.addEventListener('click', onclick);
btnAR.appendChild(checkbox);
btnRN.addEventListener('click', rcmmdnow);
function onclick(e) {
e.preventDefault();
e.stopPropagation();
arConfig.auto = !arConfig.auto;
setTimeout(function() {checkbox.checked = arConfig.auto;}, 0);
GM_setValue(KEY_ATRCMMDS, arConfig);
new ElegantAlertBox(arConfig.auto ? TEXT_ALT_ATRCMMDS_AUTO : TEXT_ALT_ATRCMMDS_NOAUTO);
}
function rcmmdnow() {
if (TASK.AutoRecommend.checkRcmmd() && !confirm(TEXT_GUI_BOOKCASE_RCMMDNW_CONFIRM)) {return false;}
TASK.AutoRecommend.run(true);
}
}
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() {
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, TASK.AutoRecommend.initConfig(true));
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('{B}', strBookName)
.replaceAll('{N}', value)
.replaceAll('{R}', userDetail.vote-arConfig.allCount)
);
if (userDetail && arConfig.allCount > userDetail.vote) {
const alertBox = new ElegantAlertBox(
TEXT_ALT_ATRCMMDS_OVERFLOW
.replace('{V}', String(userDetail.vote))
.replace('{C}', String(arConfig.allCount))
);
alertBox.elm.onclick = function() {
alertBox.close.call(alertBox);
refreshMyUserDetail();
}
};
} else {
// invalid input value, alert
new ElegantAlertBox(TEXT_ALT_ATRCMMDS_INVALID.replaceAll('{N}', value));
}
}
}
}
}
// 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
// options = {type: '', ...opts}
function createLeftBlock(title=TEXT_GUI_BLOCK_TITLE_DEFULT, append=false, options) {
const leftEle = document.querySelector('#left');
const blockEle = document.querySelector('#left>.block').cloneNode(true);
const titleEle = blockEle.querySelector('.blocktitle>.txt');
const cntntEle = blockEle.querySelector('.blockcontent');
titleEle.innerText = title;
clearChildnodes(cntntEle);
const type = options ? options.type.toLowerCase() : null;
switch (type.toLowerCase()) {
case 'mypage': typeMypage(); break;
default: DoLog(LogLevel.Error, 'createLeftBlock: Invalid block type');
}
append ? leftEle.appendChild(blockEle) : function() {};
return blockEle;
// Links such as https://www.wenku8.net/userdetail.php
// options = {type: 'mypage', links = [...{href: '', innerHTML: '', id: ''}]}
function typeMypage() {
const ul = document.createElement('ul');
ul.classList.add('ulitem');
for (const link of options.links) {
const li = document.createElement('li');
const a = document.createElement('a');
a.href = link.href ? link.href : 'javascript: void(0);';
a.innerHTML = link.innerHTML;
a.id = link.id ? link.id : '';
li.appendChild(a);
ul.appendChild(li);
}
blockEle.appendChild(ul);
}
}
// 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);
}
})();