'.replaceAll('{CT}', CLASSNAME_TEXT).replaceAll('{CB}', CLASSNAME_BUTTON);
const TEXT_ALT_SCRIPT_UPDATE_INFO = '更新信息';
const TEXT_ALT_SCRIPT_UPDATE_NOINFO = '没有发现更新日志。。';
const TEXT_ALT_SCRIPT_UPDATE_INSTALL = '安装';
const TEXT_ALT_SCRIPT_UPDATE_CLOSE = '朕知道了';
const TEXT_ALT_SCRIPT_UPDATE_NONE = '当前已是最新版本';
const TEXT_ALT_DETAIL_IMPORTED = '配置导入成功';
const TEXT_ALT_DETAIL_CONFIG_IMPORT_ERROR_SELECT = '您选择的文件不是配置文件,请检查后再试';
const TEXT_ALT_DETAIL_CONFIG_IMPORT_ERROR_READ = '配置文件读取出错,请检查是否粘贴了正确的配置文件,以及配置文件是否损坏';
const TEXT_ALT_DETAIL_MANAGE_NOTFOUND = '该记录已不存在,您是否已经在其他标签页删除它了呢?';
const TEXT_GUI_API_ADDBOOKCASE_TOBOOKCASE = '进入书架';
const TEXT_GUI_API_ADDBOOKCASE_REMOVE = '移出本书';
const TEXT_GUI_API_PACKSHOW_TITLE_LOADING = '初始化下载界面...';
const TEXT_GUI_API_PACKSHOW_TITLE = '{N} 轻小说TXT分卷下载 - 轻小说文库';
const TEXT_GUI_UNKNOWN = '未知';
const TEXT_GUI_DOWNLOAD_THISVOLUME = '下载本卷';
const TEXT_GUI_DOWNLOAD_THISCHAPTER = '下载本章';
const TEXT_GUI_NOVEL_FILLING = '[轻小说文库+] 正在获取章节内容...'.replaceAll('{CT}', CLASSNAME_TEXT);
const TEXT_GUI_BOOK_IMAGESDOWNLOAD = '全部插图下载';
const TEXT_GUI_BOOK_READITLATER = '稍后再读';
const TEXT_GUI_BOOK_DONTREADLATER = '移出稍后再读';
const TEXT_GUI_REVIEW_ADDFAVORITE = '收藏本帖:';
const TEXT_GUI_REVIEW_FAVORADDED = '已收藏 {N}';
const TEXT_GUI_REVIEW_FAVORDELED = '已从收藏中移除 {N}';
const TEXT_GUI_REVIEW_BEAUTIFUL = '页面美化:';
const TEXT_GUI_REVEIW_IMG_INSERTURL = '插入网图链接';
const TEXT_GUI_REVEIW_IMG_SELECTIMG = '选择本地图片';
const TEXT_GUI_REVIEW_UNLOCK_WARNING = '仅供测试使用,请勿滥用此功能!';
const TEXT_GUI_DOWNLOAD_REVIEW = '[下载本帖(共A页)]';
const TEXT_GUI_DOWNLOADING_REVIEW = '[下载中...(C/A)]';
const TEXT_GUI_DOWNLOAD_BBCODE = '保存为BBCODE格式:';
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 = '-Nothing Here-';
const TEXT_GUI_SDOWNLOAD = '地址三(程序重命名)';
const TEXT_GUI_SDOWNLOAD_FILENAME = '{NovelName} {VolumeName}.{Extension}';
const TEXT_GUI_DOWNLOADING_ALL = '下载中...(C/A)';
const TEXT_GUI_DOWNLOADED_ALL = '下载图片(已完成)';
const TEXT_GUI_AUTOREFRESH = '自动更新页面:';
const TEXT_GUI_AUTOREFRESH_PAUSED = '(回复编辑中,暂停刷新)';
const TEXT_GUI_AUTOSAVE = '(您输入的内容已保存到书评草稿中)';
const TEXT_GUI_AUTOSAVE_CLEAR = '(草稿为空)';
const TEXT_GUI_AUTOSAVE_RESTORE = '(已从书评草稿中恢复了您上次编辑的内容)';
const TEXT_GUI_AREAREPLY_AT = '想用@提到谁?';
const TEXT_GUI_INDEX_FAVORITES = '收藏的书评';
const TEXT_GUI_INDEX_STATUS = '{S} 正在运行,版本 {V}。'.replace('{S}', GM_info.script.name).replace('{V}', GM_info.script.version);
const TEXT_GUI_INDEX_LATERBOOKS = '稍后再读';
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 = '每日自动推书:';
const TEXT_GUI_BOOKCASE_RCMMDNW = '立即推书';
const TEXT_GUI_BOOKCASE_RCMMDNW_DONE = '今日推书已完成';
const TEXT_GUI_BOOKCASE_RCMMDNW_NOTYET = '今日尚未推书';
const TEXT_GUI_BOOKCASE_RCMMDNW_NOTASK = '您还没有设置自动推书';
const TEXT_GUI_BOOKCASE_RCMMDNW_CONFIRM = '今天已经推过书了,是否要再推一遍?';
const TEXT_GUI_SEARCH_OPTION_TAG = '标签(preview)';
const TEXT_GUI_DETAIL_TITLE_SETTINGS = '脚本设置';
const TEXT_GUI_DETAIL_TITLE_BGI = '页面美化背景图片';
const TEXT_GUI_DETAIL_DEFAULT_BGI = '点击选择图片 / 拖拽图片到此处 / Ctrl+V粘贴剪贴板中的图片';
const TEXT_GUI_DETAIL_BGI = '当前图片:{N}';
const TEXT_GUI_DETAIL_BGI_WORKING = '处理中...';
const TEXT_GUI_DETAIL_BGI_UPLOADING = '正在上传: {NAME}';
const TEXT_GUI_DETAIL_BGI_UPLOADFAILED = '{NAME}(上传失败,已本地保存)';
const TEXT_GUI_DETAIL_BGI_DOWNLOADING = '正在下载: {NAME}';
const TEXT_GUI_DETAIL_BGI_UPLOAD = '上传图片到图床以防止卡顿';
const TEXT_GUI_DETAIL_BGI_LEGAL = '上传图片请遵守法律以及图床使用规定请不要上传违规图片';
const TEXT_GUI_DETAIL_GUI_IMAGER = '图床选择';
const TEXT_GUI_DETAIL_GUI_SCALE = '书评字体缩放';
const TEXT_GUI_DETAIL_BTF_NOVEL = '阅读页面美化';
const TEXT_GUI_DETAIL_BTF_REVIEW = '书评页面美化';
const TEXT_GUI_DETAIL_BTF_COMMON = '其他页面美化';
const TEXT_GUI_DETAIL_FVR_LASTPAGE = '点击收藏的帖子时打开最后一页';
const TEXT_GUI_DETAIL_VERSION_CURVER = '当前版本';
const TEXT_GUI_DETAIL_VERSION_CHECKUPDATE = '检查更新';
const TEXT_GUI_DETAIL_VERSION_CHECK = '点击此处检查更新';
const TEXT_GUI_DETAIL_CONFIG_EXPORT = '导出所有脚本配置到文件(包含账号密码)';
const TEXT_GUI_DETAIL_CONFIG_EXPORT_NOPASS = '导出所有脚本配置到文件(不包含账号密码)';
const TEXT_GUI_DETAIL_EXPORT_CLICK = '点击导出';
const TEXT_GUI_DETAIL_CONFIG_IMPORT = '从文件导入脚本配置';
const TEXT_GUI_DETAIL_IMPORT_CLICK = '点击导入 / 拖拽配置文件到此处 / Ctrl+V粘贴剪贴板中的配置文件,并刷新页面';
const TEXT_GUI_DETAIL_FEEDBACK_TITLE = '提出反馈';
const TEXT_GUI_DETAIL_FEEDBACK = '点击打开反馈页面';
const TEXT_GUI_DETAIL_UPDATEINFO_TITLE = '更新日志';
const TEXT_GUI_DETAIL_UPDATEINFO = '点击去主页查看';
const TEXT_GUI_DETAIL_CONFIG_MANAGE = '管理存储的信息';
const TEXT_GUI_DETAIL_CONFIG_MANAGE_EMPTY = '没有内容';
const TEXT_GUI_DETAIL_CONFIG_MANAGE_MORE = '…';
const TEXT_GUI_DETAIL_MANAGE_CLICK = '点击打开管理页面';
const TEXT_GUI_DETAIL_MANAGE_HEADER = '脚本储存管理';
const TEXT_GUI_DETAIL_MANAGE_FAV_NOTE_BTN_OPEN = '打开';
const TEXT_GUI_DETAIL_MANAGE_FAV_NOTE_BTN_NOTE = '备注';
const TEXT_GUI_DETAIL_MANAGE_FAV_NOTE_BTN_DELETE = '删除';
const TEXT_GUI_DETAIL_MANAGE_FAV_NOTE_TIP = '为{TITLE}设置备注: 备注将在主页鼠标经过此帖子收藏的链接时悬浮显示';
const TEXT_GUI_DETAIL_MANAGE_FAV_NOTE_TITLE = '编辑备注';
const TEXT_GUI_DETAIL_MANAGE_FAV_DELETE_TIP = '确认将{TITLE}移除收藏?';
const TEXT_GUI_DETAIL_MANAGE_FAV_DELETE_TITLE = '移除收藏';
const TEXT_GUI_DETAIL_MANAGE_FAV_SAVED = '已保存';
const TEXT_GUI_DETAIL_MANAGE_FAV_DELETED = '已删除';
const TEXT_GUI_DETAIL_CONFIG_IMPORT_CONFIRM_SELECT = '是否要将您粘贴的图片({N})中设置为页面美化背景图片?';
const TEXT_GUI_DETAIL_CONFIG_IMPORT_CONFIRM_PASTE = '是否要从您粘贴的配置文件({N})中导入配置?\n建议先备份您当前的配置,再导入新配置';
const TEXT_GUI_BLOCK_TITLE_DEFULT = '操作区域';
const TEXT_GUI_USER_REVIEWSEARCH = '用户书评';
const TEXT_GUI_USER_USERINFO = '详细资料';
const TEXT_GUI_USER_USERREMARKEDIT = '编辑备注';
const TEXT_GUI_USER_USERREMARKSHOW = '用户备注:';
const TEXT_GUI_USER_USERREMARKEMPTY = '假装这里有个备注';
const TEXT_GUI_USER_USERREMARKEDIT_TITLE = '编辑备注';
const TEXT_GUI_USER_USERREMARKEDIT_MSG = '设置 [{N}] 的备注为:';
const TEXT_GUI_LINK_TOLASTPAGE = '[打开尾页]';
const TEXT_GUI_ACCOUNT_SWITCH = '切换账号:';
const TEXT_GUI_ACCOUNT_CONFIRM = '是否要切换到帐号 "{N}"?';
const TEXT_GUI_ACCOUNT_NOACCOUNT = '(帐号列表为空)';
const TEXT_GUI_ACCOUNT_NOTLOGGEDIN = '(没有登录信息)';
// 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]';
// Arguments: level=LogLevel.Info, logContent, asObject=false
// Needs one call "DoLog();" to get it initialized before using it!
function DoLog() {
// Global log levels set
unsafeWindow.LogLevel = {
None: 0,
Error: 1,
Success: 2,
Warning: 3,
Info: 4,
}
unsafeWindow.LogLevelMap = {};
unsafeWindow.LogLevelMap[LogLevel.None] = {prefix: '' , color: 'color:#ffffff'}
unsafeWindow.LogLevelMap[LogLevel.Error] = {prefix: '[Error]' , color: 'color:#ff0000'}
unsafeWindow.LogLevelMap[LogLevel.Success] = {prefix: '[Success]' , color: 'color:#00aa00'}
unsafeWindow.LogLevelMap[LogLevel.Warning] = {prefix: '[Warning]' , color: 'color:#ffa500'}
unsafeWindow.LogLevelMap[LogLevel.Info] = {prefix: '[Info]' , color: 'color:#888888'}
unsafeWindow.LogLevelMap[LogLevel.Elements] = {prefix: '[Elements]', color: 'color:#000000'}
// Current log level
DoLog.logLevel = (unsafeWindow ? unsafeWindow.isPY_DNG : window.isPY_DNG) ? LogLevel.Info : LogLevel.Warning; // Info Warning Success Error
// Log counter
DoLog.logCount === undefined && (DoLog.logCount = 0);
if (++DoLog.logCount > 512) {
console.clear();
DoLog.logCount = 0;
}
// Get args
let level, logContent, asObject;
switch (arguments.length) {
case 1:
level = LogLevel.Info;
logContent = arguments[0];
asObject = false;
break;
case 2:
level = arguments[0];
logContent = arguments[1];
asObject = false;
break;
case 3:
level = arguments[0];
logContent = arguments[1];
asObject = arguments[2];
break;
default:
level = LogLevel.Info;
logContent = 'DoLog initialized.';
asObject = false;
break;
}
// Log when log level permits
if (level <= DoLog.logLevel) {
let msg = '%c' + LogLevelMap[level].prefix;
let subst = LogLevelMap[level].color;
if (asObject) {
msg += ' %o';
} else {
switch(typeof(logContent)) {
case 'string': msg += ' %s'; break;
case 'number': msg += ' %d'; break;
case 'object': msg += ' %o'; break;
}
}
console.log(msg, subst, logContent);
}
}
DoLog();
let tipready, CONFIG, TASK, DMode, SPanel, AndAPI
let API
main();
// Main
function main() {
// Get tab url api part
API = window.location.href.replace(/https?:\/\/www\.wenku8\.(net|cc)\//, '').replace(/\?.*/, '').replace(/#.*/, '')
.replace(/^book\/\d+\.html?/, 'book').replace(/novel\/(\d+\/?)+\.html?$/, 'novel')
.replace(/^novel[\/\d]+index\.html?$/, 'novelindex');
// Common actions
loadinResourceCSS();
loadinFontAwesome();
polyfillAlert();
tipready = tipcheck();
tipscroll();
addStyle(CSS_COMMON);
GMXHRHook(NUMBER_MAX_XHR);
CONFIG = new configManager();
TASK = new taskManager();
AndAPI = new AndroidAPI();
//DMode = new Darkmode({autoMatchOsTheme: false});
formSearch();
linkReview();
multiAccount();
commonBeautify(API);
SPanel = sideFunctions();
unsafeWindow.alertify = alertify;
alertify.set('notifier','position', 'top-right');
if (isAPIPage()) {
if (!pageAPI(API)) {
return;
}
}
if (!API) {
location.href = `https://${location.host}/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;
// ReviewEdit page
case 'modules/article/reviewedit.php':
areaReply();
pageReviewedit();
break;
// Bookcase page
case 'modules/article/bookcase.php':
pageBookcase();
break;
// Tags page
case 'modules/article/tags.php':
pageTags();
break;
// Mylink page
case 'mylink.php':
pageMylink();
break;
case 'userpage.php':
pageUser();
break;
// Detail page
case 'userdetail.php':
pageDetail();
break;
// Index page
case 'index.php':
pageIndex();
break;
// Book page
// Also: https://www.wenku8.net/modules/article/articleinfo.php?id={ID}&charset=gbk
case 'modules/article/articleinfo.php':
case 'book':
pageBook();
break;
// Novel index page
case 'novelindex':
pageNovelIndex();
break;
// Novel page
case 'novel':
pageNovel();
break;
// Novel index page & novel page
case 'modules/article/reader.php':
chapter_id === '0' ? pageNovelIndex() : pageNovel();
break;
// Login page
case 'login.php':
pageLogin();
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 = {
// Check if recommend has done
checkRcmmd: function() {
const arConfig = CONFIG.AutoRecommend.getConfig();
return arConfig.lasttime === getTime('-', false);
},
// Auto recommend main function
run: function(recommendAnyway=false) {
let i;
// Get config
const arConfig = CONFIG.AutoRecommend.getConfig();
// 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;}
// Config check: whether the recommend list is empty
if (arConfig.allCount === 0) {
const altBox = alertify.notify(
/modules\/article\/bookcase\.php$/.test(location.href) ?
TEXT_ALT_ATRCMMDS_NOTASK_PLSSET + (getMyUserDetail().userDetail ? ''+TEXT_ALT_ATRCMMDS_MAXRCMMD.replace('{V}', String(getMyUserDetail().userDetail.vote)) : '') :
TEXT_ALT_ATRCMMDS_NOTASK_OPENBC
);
altBox.callback = (isClicked) => {
isClicked && window.open(URL_BOOKCASE);
}
return false;
};
// Recommend for each
let recommended = {}, AM = new AsyncManager();
AM.onfinish = allFinish;
alertify.notify(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
//alertify.notify(TEXT_ALT_ATRCMMDS_RUNNING.replaceAll('{BN}', bookName).replaceAll('{BID}', strBookID));
// Go work
for (i = 0; i < number; i++) {
AM.add();
getDocument(URL_RECOMMEND.replaceAll('{B}', strBookID), bookFinish,[book, strBookID, bookName]);
}
// Soft alert
//alertify.notify(TEXT_ALT_ATRCMMDS_DONE.replaceAll('{BN}', bookName).replaceAll('{BID}', strBookID));
}
AM.finishEvent = true;
return true;
function bookFinish(oDoc, book, strBookID, bookName) {
// title: "处理成功"
const statusText = $(oDoc, '.blocktitle').innerText;
// success: "我们已经记录了本次推荐,感谢您的参与!\n\n您每天拥有 5 次推荐权利,这是您今天第 1 次推荐。"
// overflow: "\n错误原因:对不起,您今天已经用完了推荐的权利!\n\n您每天可以推荐 20 次。\n\n请 返 回 并修正"
const returnText = $(oDoc, '.blockcontent').innerText.replace(/\s*\[.+\]\s*$/, '');
// Save book
book.lasttime = getTime('-', false);
CONFIG.AutoRecommend.saveConfig(arConfig);
// Log
DoLog(statusText + '\n' + returnText);
/*
// Check status
const success = /我们已经记录了本次推荐,感谢您的参与!\s*您每天拥有\s*(\d+)\s*次推荐权利,这是您今天第\s*(\d+)\s*次推荐。/;
const overflow = /\s*错误原因:对不起,您今天已经用完了推荐的权利!\s*您每天可以推荐\s*(\d+)\s*次。\s*请\s*返\s*回\s*并修正/;
*/
const b = recommended[strBookID] = recommended[strBookID] || {name: bookName, strID: strBookID, count: 0};
b.count++;
AM.finish();
}
function allFinish() {
// Save config
arConfig.lasttime = getTime('-', false);
CONFIG.AutoRecommend.saveConfig(arConfig);
// Soft alert
let text = [];
for (const strBookID of Object.keys(recommended)) {
const book = recommended[strBookID];
text.push('[{BID}]{BN} 推荐了{C}次'.replaceAll('{C}', book.count).replaceAll('{BID}', book.strID).replaceAll('{BN}', book.name));
}
alertify.success(TEXT_ALT_ATRCMMDS_ALL_DONE.replaceAll('{R}', text.join('')));
}
}
}
// Config Maintainer
TM.Cleaner = {
cleanPageStatus: function() {
const config = CONFIG.BkReviewPrefs.getConfig();
const history = config.history;
let count = 0;
for (const [rid, his] of Object.entries(history)) {
if (!his.time || (new Date()).getTime() - his.time > 30*1000) {
delete history[rid];
count++;
}
}
CONFIG.BkReviewPrefs.saveConfig(config);
DoLog(count > 0 ? LogLevel.Success : LogLevel.Info, 'Review page status cleaned ({C})'.replace('{C}', count.toString()));
},
imagerFix: function() {
const config = CONFIG.UserGlobalCfg.getConfig();
const curimager = config.imager;
// If imager does not exist or imager disabled, change it to default
if (!DATA_IMAGERS[curimager] || !DATA_IMAGERS[curimager].available) {
DoLog(LogLevel.Warning, 'Current imager unavailable, changing to default.');
if (curimager !== DATA_IMAGERS.default && DATA_IMAGERS[DATA_IMAGERS.default].available) {
// Default available
config.imager = DATA_IMAGERS.default;
DoLog(LogLevel.Success, 'Changed to default.');
} else {
// Default not available
DoLog(LogLevel.Warning, 'Default imager unavailable, trying to find another imager for use. ')
for (const [key, imager] of Object.entries(DATA_IMAGERS)) {
if (imager.available) {
config.imager = key;
DoLog(LogLevel.Success, 'Changed to {K}.'.replace('{K}', key));
break;
}
}
if (config.imager === curimager) {
// OMG, There's NO IMAGER AVAILABLE!!
DoLog(LogLevel.Error, 'OMG, There\'s NO IMAGER AVAILABLE!!');
}
}
CONFIG.UserGlobalCfg.saveConfig(config);
alertify.warning((config.imager !== curimager ? TEXT_ALT_IMAGER_RESET : TEXT_ALT_IMAGER_NOAVAILBLE).replace('{O}', DATA_IMAGERS[curimager].name).replace('{N}', DATA_IMAGERS[config.imager].name));
}
},
}
// Script
TM.Script = {
// Check & Update to latest version of script
update: function(force=false) {
// Check for update once a day
const scriptID = 416310;
const config = CONFIG.GlobalConfig.getConfig();
if (!force && config.scriptUpdate.lasttime === getTime('-', false)) {return false;}
const GFU = new GreasyForkUpdater();
alertify.notify(TEXT_ALT_SCRIPT_UPDATE_CHECKING);
GFU.checkUpdate(scriptID, GM_info.script.version, function(update, updateurl, metaData) {
if (update) {
const box = alertify.notify(TEXT_ALT_SCRIPT_UPDATE_GOT.replaceAll('{SN}', metaData.name).replaceAll('{NV}', metaData.version).replaceAll('{CV}', GM_info.script.version));
const btnInfo = $(box.element, '#script_update_info');
const btnInstall = $(box.element, '#script_update_install');
btnInfo.addEventListener('click', show);
btnInstall.addEventListener('click', install);
} else {
alertify.message(TEXT_ALT_SCRIPT_UPDATE_NONE);
}
config.scriptUpdate.lasttime = getTime('-', false);
CONFIG.GlobalConfig.saveConfig(config);
function install(e) {
location.href = updateurl;
}
function show(e) {
const info = metaData.updateinfo;
const box = alertify.confirm(info ? info : TEXT_ALT_SCRIPT_UPDATE_NOINFO, install);
box.setHeader(TEXT_ALT_SCRIPT_UPDATE_INFO);
box.set('labels', {ok: TEXT_ALT_SCRIPT_UPDATE_INSTALL, cancel: TEXT_ALT_SCRIPT_UPDATE_CLOSE});
box.set('overflow', true);
}
});
return true;
}
}
TM.Script.update();
TM.Cleaner.cleanPageStatus();
TM.Cleaner.imagerFix();
TM.UserDetail.refresh();
TM.AutoRecommend.run();
}
// Config Manager
// use 'new' keyword
function configManager() {
const CM = this;
const [getValue, setValue, deleteValue, listValues] = [
window.getValue ? window.getValue : GM_getValue,
window.setValue ? window.setValue : GM_setValue,
window.deleteValue ? window.deleteValue : GM_deleteValue,
window.listValues ? window.listValues : GM_listValues,
]
CM.GlobalConfig = {
saveConfig: function(config) {
config ? config[KEY_CM_VERSION] = VALUE_CM_VERSION : function() {};
setValue(KEY_CM, config);
},
initConfig: function(save=true, func) {
let config = {
users: {},
scriptUpdate: {
lasttime: ''
}
};
config = func ? func(config) : config;
save ? CM.GlobalConfig.saveConfig(config) : function() {};
return config;
},
getConfig: function(init) {
let config = getValue(KEY_CM, null);
config = config ? config : (init ? CM.GlobalConfig.initConfig(true, init) : CM.GlobalConfig.initConfig());
return config;
},
// Review config upgrade (Uses GM_functions)
upgradeConfig: function() {
// Get version
const default_self = {}; default_self[KEY_CM_VERSION] = '0.1'; // v0.1 has no self object
const self = GM_getValue(KEY_CM, default_self);
const version = self[KEY_CM_VERSION];
// Upgrade by version
if (self[KEY_CM_VERSION] === VALUE_CM_VERSION) {DoLog(LogLevel.Info, 'Config Manager self config is in latest version. ');};
switch(version) {
case '0.1':
v01_To_v02();
v02_To_v03();
logUpgrade();
break;
case '0.2':
v02_To_v03();
logUpgrade();
break;
}
// Save to global gm_storage
self[KEY_CM_VERSION] = VALUE_CM_VERSION;
setValue(KEY_CM, self);
function logUpgrade() {
DoLog(LogLevel.Success, 'Config Manager self config successfully upgraded From v{V1} to v{V2}. '.replaceAll('{V1}', version).replaceAll('{V2}', VALUE_CM_VERSION));
}
function v01_To_v02() {
const props = GM_listValues();
const userStorage = {};
for (const prop of props) {
userStorage[prop] = GM_getValue(prop);
}
const userID = getUserID();
userID ? GM_setValue(userID, userStorage) : GM_setValue('temp', userStorage);
for (const prop of props) {
GM_deleteValue(prop);
}
}
function v02_To_v03() {
self.scriptUpdate = self.scriptUpdate ? self.scriptUpdate : {lasttime: ''};
}
},
// Redirect global gm_storage to user's storage area (Uses GM_functions)
// callback(key)
redirectToUser: function (callback) {
// Get userID from cookies
const userID = getUserID();
if (userID) {
// delete temp data if exist
GM_deleteValue('temp');
// Save lastUserID
const config = CM.GlobalConfig.getConfig();
config.lastUserID = userID;
CM.GlobalConfig.saveConfig(config);
// Redirect to user storage area
redirectGMStorage(userID);
DoLog(LogLevel.Info, 'GM_storage redirected to ' + String(userID));
} else {
// Redirect to temp storage area before request finish
const lastUserID = CM.GlobalConfig.getConfig().lastUserID;
redirectTemp(lastUserID);
// Request userID
getMyUserDetail((userDetail)=>{
const key = userDetail.userDetail.userID;
// Move temp data to user storage area
redirectGMStorage();
const tempStorage = GM_getValue('temp');
GM_setValue(lastUserID ? lastUserID : key, tempStorage);
GM_deleteValue('temp');
// Save lastUserID
const config = CM.GlobalConfig.getConfig();
config.lastUserID = key;
CM.GlobalConfig.saveConfig(config);
// Redirect to user storage area
redirectGMStorage(key);
DoLog(LogLevel.Info, 'GM_storage redirected to ' + String(key));
// callback
callback ? callback(key) : function() {};
})
}
// When userID request not finished, use 'temp' as gm_storage key
function redirectTemp(lastUserID) {
if (lastUserID) {
// Copy config of the user we use last time to 'temp' storage area
const lastUser = GM_getValue(lastUserID, {});
GM_setValue('temp', lastUser);
}
redirectGMStorage('temp');
DoLog(LogLevel.Info, 'GM_storage redirected to temp');
}
}
}
CM.GlobalConfig.upgradeConfig();
CM.GlobalConfig.redirectToUser();
CM.AutoRecommend = {
saveConfig: function(config) {
config ? config[KEY_ATRCMMDS_VERSION] = VALUE_ATRCMMDS_VERSION : function() {};
GM_setValue(KEY_ATRCMMDS, config);
},
initConfig: function(save=true, func) {
let config = {};
config[KEY_ATRCMMDS_VERSION] = VALUE_ATRCMMDS_VERSION;
config.allCount = 0;
config.books = {};
config.auto = true;
config = func ? func(config) : config;
save ? CM.AutoRecommend.saveConfig(config) : function() {};
return config;
},
getConfig: function(init) {
let config = GM_getValue(KEY_ATRCMMDS, null);
config = config ? config : (init ? CM.AutoRecommend.initConfig(true, init) : CM.AutoRecommend.initConfig());
return config;
},
// Auto-recommend config upgrade
upgradeConfig: function() {
// Get config
const config = CM.AutoRecommend.getConfig();
// if not inited
if (!config) {return;};
switch (config[KEY_ATRCMMDS_VERSION]) {
case '0.1':
config.auto = true;
logUpgrade();
break;
case VALUE_ATRCMMDS_VERSION:
DoLog(LogLevel.Info, 'Auto-recommend config is in latest version. ');
break;
default:
DoLog(LogLevel.Error, 'configCheckUpgrade: Invalid config version({V}) for Auto-recommend. '.replace('{V}', config[KEY_ATRCMMDS_VERSION]));
}
// Save to gm_storage
CM.AutoRecommend.saveConfig(config);
function logUpgrade() {
DoLog(LogLevel.Success, 'Auto-recommend config successfully upgraded From v{V1} to {V2}. '.replaceAll('{V1}', config[KEY_ATRCMMDS_VERSION]).replaceAll('{V2}', VALUE_ATRCMMDS_VERSION));
}
}
}
CM.commentDrafts = {
saveConfig: function(config) {
config ? config[KEY_DRAFT_VERSION] = VALUE_DRAFT_VERSION : function() {};
GM_setValue(KEY_DRAFT_DRAFTS, config);
},
initConfig: function(save=true, func) {
let config = {};
config = func ? func(config) : config;
save ? CM.commentDrafts.saveConfig(config) : function() {};
return config;
},
getConfig: function(init) {
let config = GM_getValue(KEY_DRAFT_DRAFTS, null);
config = config ? config : (init ? CM.commentDrafts.initConfig(true, init) : CM.commentDrafts.initConfig());
return config;
},
// Comment-drafts config upgrade
upgradeConfig: function() {
// Get config
let config = CM.commentDrafts.getConfig();
// if not inited
if (!config) {return;};
switch (config[KEY_DRAFT_VERSION]) {
case '0.1':
case undefined:
v01_To_v02();
logUpgrade();
break;
case VALUE_DRAFT_VERSION:
DoLog(LogLevel.Info, 'comment-drafts config is in latest version. ');
break;
default:
DoLog(LogLevel.Error, 'configCheckUpgrade: Invalid config version({V}) for comment-drafts. '.replace('{V}', config[KEY_DRAFT_VERSION]));
}
// Save to gm_storage
CM.commentDrafts.saveConfig(config);
function logUpgrade() {
DoLog(LogLevel.Success, 'comment-drafts config successfully upgraded From v{V1} to {V2}. '.replaceAll('{V1}', config[KEY_DRAFT_VERSION]).replaceAll('{V2}', VALUE_DRAFT_VERSION));
}
function v01_To_v02() {
// Fix bug caused bookcase's config overwriting comment-drafts' config
if (config instanceof Array) {
config = {};
}
}
}
}
CM.bookcasePrefs = {
saveConfig: function(config) {
config ? config[KEY_BOOKCASE_VERSION] = VALUE_BOOKCASE_VERSION : function() {};
GM_setValue(KEY_BOOKCASES, config);
},
initConfig: function(save=true, func) {
let config = {
bookcases: [],
laterbooks: {
sortby: 'addTime_old2new',
books: {}
}
};
config = func ? func(config) : config;
save ? CM.bookcasePrefs.saveConfig(config) : function() {};
return config;
},
getConfig: function(init) {
let config = GM_getValue(KEY_BOOKCASES, null);
config = config ? config : (init ? CM.bookcasePrefs.initConfig(true, init) : CM.bookcasePrefs.initConfig());
return config;
},
// Bookcase config upgrade
upgradeConfig: function() {
// Get config
let config = CM.bookcasePrefs.getConfig();
// if not inited
if (!config) {return;};
// Original version
let V = config && config[KEY_BOOKCASE_VERSION] ? config[KEY_BOOKCASE_VERSION] : '0';
switch (V) {
case '0.1':
case undefined:
case '0':
v01_To_v02();
v02_To_v03();
v03_To_v04();
v04_To_v05();
logUpgrade();
break;
case '0.2':
v02_To_v03();
v03_To_v04();
v04_To_v05();
logUpgrade();
break;
case '0.3':
v03_To_v04();
v04_To_v05();
logUpgrade();
break;
case '0.4':
v04_To_v05();
logUpgrade();
break;
case VALUE_BOOKCASE_VERSION:
DoLog(LogLevel.Info, 'Bookcase config is in latest version. ');
break;
default:
DoLog(LogLevel.Error, 'configCheckUpgrade: Invalid config version({V}) for Bookcase. '.replace('{V}', config[KEY_BOOKCASE_VERSION]));
}
// Save to gm_storage
CM.bookcasePrefs.saveConfig(config);
function logUpgrade() {
DoLog(LogLevel.Success, 'Bookcase config successfully upgraded From v{V1} to v{V2}. '.replaceAll('{V1}', V).replaceAll('{V2}', VALUE_BOOKCASE_VERSION));
}
function v01_To_v02() {
// Clear useless key added falsely
delete config.bbcode;
// Convert array to an object
if (Array.isArray(config)) {
const newConfig = {bookcases: []};
for (let i = 0; i < config.length; i++) {
newConfig.bookcases[i] = config[i];
}
config = newConfig;
}
}
function v02_To_v03() {
// Fix bug caused config.bookcases equals to []
if (config && config.bookcases && config.bookcases.length === 0) {
config = CM.bookcasePrefs.initConfig();
}
}
function v03_To_v04() {
if (config.laterbooks) {return false;}
config.laterbooks = {
sortby: 'addTime_old2new',
books: {}
};
}
function v04_To_v05() {
const books = config.laterbooks.books;
const sorts = [];
let err = false;
for (const book of Object.values(books)) {
if (sorts.includes(book.sort)) {
err = true;
break;
}
sorts.push(book.sort);
}
Math.max.apply(null, sorts) > books.length && (err = true);
if (err) {
let i = 0;
for (const book of Object.values(books)) {
book.sort = ++i;
}
alertify.notify(TEXT_ALT_BOOKCASE_AFTERBOOKS_V4BUG, '', 0);
}
}
}
}
CM.userDtlePrefs = {
saveConfig: function(config) {
config ? config[KEY_USRDETAIL_VERSION] = VALUE_USRDETAIL_VERSION : function() {};
GM_setValue(KEY_USRDETAIL, config);
},
initConfig: function(save=true, func) {
let config = {userDetail: null};
config = func ? func(config) : config;
save ? CM.userDtlePrefs.saveConfig(config) : function() {};
return config;
},
getConfig: function(init) {
let config = GM_getValue(KEY_USRDETAIL, null);
config = config ? config : (init ? CM.userDtlePrefs.initConfig(true, init) : CM.userDtlePrefs.initConfig());
return config;
},
// userDetail config upgrade
upgradeConfig: function() {
// Get config
const config = CM.userDtlePrefs.getConfig();
// if not inited
if (!config) {return;};
// Original version
let V = config && config[KEY_BOOKCASE_VERSION] ? config[KEY_BOOKCASE_VERSION] : '0';
switch (V) {
case '0.1':
refreshMyUserDetail(logUpgrade);
break;
case VALUE_USRDETAIL_VERSION:
DoLog(LogLevel.Info, 'User-detail config is in latest version. ');
break;
default:
DoLog(LogLevel.Error, 'configCheckUpgrade: Invalid config version({V}) for User-detail. '.replace('{V}', V));
}
// Save to gm_storage
CM.userDtlePrefs.saveConfig(config);
function logUpgrade() {
DoLog(LogLevel.Success, 'User-detail config successfully upgraded From v{V1} to v{V2}. '.replaceAll('{V1}', V).replaceAll('{V2}', VALUE_USRDETAIL_VERSION));
}
}
}
CM.BkReviewPrefs = {
saveConfig: function(config) {
config ? config[KEY_REVIEW_VERSION] = VALUE_REVIEW_VERSION : function() {};
GM_setValue(KEY_REVIEW_PREFS, config);
},
initConfig: function(save=true, func) {
let config = {
bbcode: false,
autoRefresh: false,
beautiful: true,
backgroundImage: 'https://img12.360buyimg.com/ddimg/jfs/t1/197476/22/6462/3478996/613227a8E03e8ffc3/99970183ddb9f896.jpg',
favorites: {
228884: {
name: '文库导航姬',
href: `https://${location.host}/modules/article/reviewshow.php?rid=228884`,
tiptitle: '梦想成为书评区大水怪的可以来康康'
}
},
favorlast: false,
history: {}
};
config = func ? func(config) : config;
save ? CM.BkReviewPrefs.saveConfig(config) : function() {};
return config;
},
getConfig: function(init) {
let config = GM_getValue(KEY_REVIEW_PREFS, null);
config = config ? config : (init ? CM.BkReviewPrefs.initConfig(true, init) : CM.BkReviewPrefs.initConfig());
return config;
},
// Review config upgrade
upgradeConfig: function() {
// Get config
const config = CM.BkReviewPrefs.getConfig();
// if not inited
if (!config) {return;};
switch (config[KEY_REVIEW_VERSION]) {
case '0.1':
v01_To_v02();
v02_To_v03();
v03_To_v04();
v04_To_v05();
v05_To_v06();
v06_To_v07();
v07_To_v08();
v08_To_v09();
logUpgrade();
break;
case '0.2':
v02_To_v03();
v03_To_v04();
v04_To_v05();
v05_To_v06();
v06_To_v07();
v07_To_v08();
v08_To_v09();
logUpgrade();
break;
case '0.3':
v03_To_v04();
v04_To_v05();
v05_To_v06();
v06_To_v07();
v07_To_v08();
v08_To_v09();
logUpgrade();
break;
case '0.4':
v04_To_v05();
v05_To_v06();
v06_To_v07();
v07_To_v08();
v08_To_v09();
logUpgrade();
break;
case '0.5':
v05_To_v06();
v06_To_v07();
v07_To_v08();
v08_To_v09();
logUpgrade();
break;
case '0.6':
v06_To_v07();
v07_To_v08();
v08_To_v09();
logUpgrade();
break;
case '0.7':
v07_To_v08();
v08_To_v09();
logUpgrade();
break;
case '0.8':
v08_To_v09();
logUpgrade();
break;
case VALUE_REVIEW_VERSION:
DoLog(LogLevel.Info, 'Review config is in latest version. ');
break;
default:
DoLog(LogLevel.Error, 'configCheckUpgrade: Invalid config version({V}) for Review. '.replace('{V}', config[KEY_REVIEW_VERSION]));
}
// Save to gm_storage
CM.BkReviewPrefs.saveConfig(config);
function logUpgrade() {
DoLog(LogLevel.Success, 'Review config successfully upgraded From v{V1} to v{V2}. '.replaceAll('{V1}', config[KEY_REVIEW_VERSION]).replaceAll('{V2}', VALUE_REVIEW_VERSION));
}
function v01_To_v02() {
config.autoRefresh = false;
delete config.downloading;
}
function v02_To_v03() {
config.favorites = {
228884: {
name: '文库导航姬',
href: `https://${location.host}/modules/article/reviewshow.php?rid=228884`,
tiptitle: '梦想成为书评区大水怪的可以来康康'
}
}
}
function v03_To_v04() {
if (config.favorites) {return;};
config.favorites = {
228884: {
name: '文库导航姬',
href: `https://${location.host}/modules/article/reviewshow.php?rid=228884`,
tiptitle: '梦想成为书评区大水怪的可以来康康'
}
};
}
function v04_To_v05() {
if (config.history) {return;};
config.history = {};
}
function v05_To_v06() {
if (config.beautiful !== undefined) {return;};
config.beautiful = true;
config.backgroundImage = 'https://img12.360buyimg.com/ddimg/jfs/t1/197476/22/6462/3478996/613227a8E03e8ffc3/99970183ddb9f896.jpg';
}
function v06_To_v07() {
// Move CM.BkReviewPrefs.upgradeConfig.beautiful to CM.BeautifierCfg
if (config.beautiful === undefined) {return;};
const beautifierConfig = {
reviewshow: {
beautiful: config.beautiful,
backgroundImage: config.backgroundImage
}
}
CM.BeautifierCfg.saveConfig(beautifierConfig);
delete config.beautiful;
delete config.backgroundImage;
}
function v07_To_v08() {
// Move CM.BkReviewPrefs.upgradeConfig.beautiful to CM.BeautifierCfg
if (config.favorlast !== undefined) {return;};
config.favorlast = false;
for (const [rid, favorite] of Object.entries(config.favorites)) {
config.favorites[rid] = {
name: favorite.name,
href: favorite.href.replace(/&page=1$/, ''),
tiptitle: favorite.tiptitle
};
}
}
function v08_To_v09() {
// Fill all favorite bookreviews' tiptitle using null for those don't have
config.favorlast = false;
for (const [rid, favorite] of Object.entries(config.favorites)) {
!favorite.tiptitle && (favorite.tiptitle = null);
}
}
}
}
CM.BeautifierCfg = {
saveConfig: function(config) {
config ? config[KEY_BEAUTIFIER_VERSION] = VALUE_BEAUTIFIER_VERSION : function() {};
GM_setValue(KEY_BEAUTIFIER, config);
},
initConfig: function(save=true, func) {
let config = {
upload: false,
reviewshow: {
beautiful: true,
},
novel: {
beautiful: true,
},
common: {
beautiful: false,
},
backgroundImage: 'https://img12.360buyimg.com/ddimg/jfs/t1/197476/22/6462/3478996/613227a8E03e8ffc3/99970183ddb9f896.jpg',
bgiName: '默认背景图片 - Pixiv ID: 88913164',
textScale: 100
};
config = func ? func(config) : config;
save ? CM.BeautifierCfg.saveConfig(config) : function() {};
return config;
},
getConfig: function(init) {
let config = GM_getValue(KEY_BEAUTIFIER, null);
config = config ? config : (init ? CM.BeautifierCfg.initConfig(true, init) : CM.BeautifierCfg.initConfig());
return config;
},
// Beautifier config upgrade
upgradeConfig: function() {
// Get config
const config = CM.BeautifierCfg.getConfig();
// if not inited
if (!config) {return;};
switch (config[KEY_BEAUTIFIER_VERSION]) {
/*case '0.1':
v01_To_v02();
break;*/
case VALUE_BEAUTIFIER_VERSION:
DoLog(LogLevel.Info, 'Beautifier config is in latest version. ');
break;
case '0.1':
v01_To_v02();
v02_To_v03();
v03_To_v04();
v04_To_v05();
v05_To_v06();
v06_To_v07();
v07_To_v08();
v08_To_v09();
logUpgrade();
break;
case '0.2':
v02_To_v03();
v03_To_v04();
v04_To_v05();
v05_To_v06();
v06_To_v07();
v07_To_v08();
v08_To_v09();
logUpgrade();
break;
case '0.3':
v03_To_v04();
v04_To_v05();
v05_To_v06();
v06_To_v07();
v07_To_v08();
v08_To_v09();
logUpgrade();
break;
case '0.4':
v04_To_v05();
v05_To_v06();
v06_To_v07();
v07_To_v08();
v08_To_v09();
logUpgrade();
break;
case '0.5':
v05_To_v06();
v06_To_v07();
v07_To_v08();
v08_To_v09();
logUpgrade();
break;
case '0.6':
v06_To_v07();
v07_To_v08();
v08_To_v09();
logUpgrade();
break;
case '0.7':
v07_To_v08();
v08_To_v09();
logUpgrade();
break;
case '0.8':
v08_To_v09();
logUpgrade();
break;
default:
DoLog(LogLevel.Error, 'configCheckUpgrade: Invalid config version({V}) for Beautifier. '.replace('{V}', config[KEY_BEAUTIFIER_VERSION]));
}
// Save to gm_storage
CM.BeautifierCfg.saveConfig(config);
function logUpgrade() {
DoLog(LogLevel.Success, 'Beautifier config successfully upgraded From v{V1} to v{V2}. '.replaceAll('{V1}', config[KEY_BEAUTIFIER_VERSION]).replaceAll('{V2}', VALUE_BEAUTIFIER_VERSION));
}
function v01_To_v02() {
if (config.upload !== undefined) {return false;};
config.upload = false;
}
function v02_To_v03() {
if (config.reviewshow.bgiName !== undefined) {return false;};
config.reviewshow.bgiName = 'image.jpeg';
}
function v03_To_v04() {
if (config.textScale !== undefined) {return false;};
config.textScale = 100;
}
function v04_To_v05() {
if (config.novel !== undefined) {return false;};
config.novel = {
beautiful: true
};
}
function v05_To_v06() {
if (!config.textScale) {config.textScale = 100;};
if (!config.novel) {config.novel = {beautiful: true};};
}
function v06_To_v07() {
config.backgroundImage = config.reviewshow.backgroundImage;
config.bgiName = config.reviewshow.bgiName;
delete config.reviewshow.backgroundImage;
delete config.reviewshow.bgiName;
}
function v07_To_v08() {
if (config.common) {return false;}
config.common = {
beautiful: false
};
}
function v08_To_v09() {
if (config.common) {return false;}
config.common = {
beautiful: false
};
}
}
}
CM.RemarksConfig = {
saveConfig: function(config) {
config ? config[KEY_REMARKS_VERSION] = VALUE_REMARKS_VERSION : function() {};
GM_setValue(KEY_REMARKS, config);
},
initConfig: function(save=true, func) {
let config = {
user: {}
};
config = func ? func(config) : config;
save ? CM.RemarksConfig.saveConfig(config) : function() {};
return config;
},
getConfig: function(init) {
let config = GM_getValue(KEY_REMARKS, null);
config = config ? config : (init ? CM.RemarksConfig.initConfig(true, init) : CM.RemarksConfig.initConfig());
return config;
},
// Beautifier config upgrade
upgradeConfig: function() {
// Get config
const config = CM.RemarksConfig.getConfig();
// if not inited
if (!config) {return;};
switch (config[KEY_REMARKS_VERSION]) {
//case '0.1':
// v01_To_v02();
// logUpgrade();
// break;
case VALUE_REMARKS_VERSION:
DoLog(LogLevel.Info, 'RemarksConfig config is in latest version. ');
break;
default:
DoLog(LogLevel.Error, 'configCheckUpgrade: Invalid config version({V}) for RemarksConfig. '.replace('{V}', config[KEY_REMARKS_VERSION]));
}
// Save to gm_storage
CM.RemarksConfig.saveConfig(config);
function logUpgrade() {
DoLog(LogLevel.Success, 'RemarksConfig config successfully upgraded From v{V1} to v{V2}. '.replaceAll('{V1}', config[KEY_REMARKS_VERSION]).replaceAll('{V2}', VALUE_REMARKS_VERSION));
}
//function v#BEFORE_To_v#AFTER() {
// if (config.#NEWPROP !== undefined) {return false;};
// config.#NEWPROP = #DEFAULTVALUE;
//}
}
}
CM.UserGlobalCfg = {
saveConfig: function(config) {
config ? config[KEY_USERGLOBAL_VERSION] = VALUE_USERGLOBAL_VERSION : function() {};
GM_setValue(KEY_USERGLOBAL, config);
},
initConfig: function(save=true, func) {
let config = {
imager: DATA_IMAGERS.default
};
config = func ? func(config) : config;
save ? CM.UserGlobalCfg.saveConfig(config) : function() {};
return config;
},
getConfig: function(init) {
let config = GM_getValue(KEY_USERGLOBAL, null);
config = config ? config : (init ? CM.UserGlobalCfg.initConfig(true, init) : CM.UserGlobalCfg.initConfig());
return config;
},
// Beautifier config upgrade
upgradeConfig: function() {
// Get config
const config = CM.UserGlobalCfg.getConfig();
// if not inited
if (!config) {return;};
switch (config[KEY_USERGLOBAL_VERSION]) {
//case '0.1':
// v01_To_v02();
// logUpgrade();
// break;
case VALUE_USERGLOBAL_VERSION:
DoLog(LogLevel.Info, 'UserGlobal config is in latest version. ');
break;
default:
DoLog(LogLevel.Error, 'configCheckUpgrade: Invalid config version({V}) for UserGlobalCfg. '.replace('{V}', config[KEY_USERGLOBAL_VERSION]));
}
// Save to gm_storage
CM.UserGlobalCfg.saveConfig(config);
function logUpgrade() {
DoLog(LogLevel.Success, 'UserGlobal config successfully upgraded From v{V1} to v{V2}. '.replaceAll('{V1}', config[KEY_USERGLOBAL_VERSION]).replaceAll('{V2}', VALUE_USERGLOBAL_VERSION));
}
//function v#BEFORE_To_v#AFTER() {
// if (config.#NEWPROP !== undefined) {return false;};
// config.#NEWPROP = #DEFAULTVALUE;
//}
}
}
// New Config Item Template
/*CM.#NEWCONFIGNAME = {
saveConfig: function(config) {
config ? config[#KEY_NEWCONFIG_VERSION] = #VALUE_NEWCONFIG_VERSION : function() {};
GM_setValue(#KEY_NEWCONFIG, config);
},
initConfig: function(save=true, func) {
let config = {
#key: #value,
#key: #value
};
config = func ? func(config) : config;
save ? CM.#NEWCONFIGNAME.saveConfig(config) : function() {};
return config;
},
getConfig: function(init) {
let config = GM_getValue(#KEY_NEWCONFIG, null);
config = config ? config : (init ? CM.#NEWCONFIGNAME.initConfig(true, init) : CM.#NEWCONFIGNAME.initConfig());
return config;
},
// Beautifier config upgrade
upgradeConfig: function() {
// Get config
const config = CM.#NEWCONFIGNAME.getConfig();
// if not inited
if (!config) {return;};
switch (config[#KEY_NEWCONFIG_VERSION]) {
//case '0.1':
// v01_To_v02();
// logUpgrade();
// break;
case #VALUE_NEWCONFIG_VERSION:
DoLog(LogLevel.Info, '#NEWCONFIGNAME config is in latest version. ');
break;
default:
DoLog(LogLevel.Error, 'configCheckUpgrade: Invalid config version({V}) for #NEWCONFIGNAME. '.replace('{V}', config[#KEY_NEWCONFIG_VERSION]));
}
// Save to gm_storage
CM.#NEWCONFIGNAME.saveConfig(config);
function logUpgrade() {
DoLog(LogLevel.Success, '#NEWCONFIGNAME config successfully upgraded From v{V1} to v{V2}. '.replaceAll('{V1}', config[#KEY_NEWCONFIG_VERSION]).replaceAll('{V2}', #VALUE_NEWCONFIG_VERSION));
}
//function v#BEFORE_To_v#AFTER() {
// if (config.#NEWPROP !== undefined) {return false;};
// config.#NEWPROP = #DEFAULTVALUE;
//}
}
}*/
CM.AutoRecommend.upgradeConfig();
CM.commentDrafts.upgradeConfig();
CM.bookcasePrefs.upgradeConfig();
CM.userDtlePrefs.upgradeConfig();
CM.BkReviewPrefs.upgradeConfig();
CM.BeautifierCfg.upgradeConfig();
CM.RemarksConfig.upgradeConfig();
CM.UserGlobalCfg.upgradeConfig();
//CM.#NEWCONFIGNAME.upgradeConfig();
}
// Beautifier for all wenku pages
function commonBeautify(API) {
// No beautifier on exluded pages
const excludes = ['novel']
if (excludes.includes(API)) {return false;}
// No beatifier if user does not want
if (!CONFIG.BeautifierCfg.getConfig().common.beautiful) {return false;}
const img = $CrE('img');
img.src = CONFIG.BeautifierCfg.getConfig().backgroundImage;
img.classList.add('plus_cbty_image');
document.body.appendChild(img);
const cover = $CrE('div');
cover.classList.add('plus_cbty_cover');
document.body.appendChild(cover);
document.body.classList.add('plus_cbty');
addStyle(CSS_COMMONBEAUTIFIER, 'plus_commonbeautifier')
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 read-later button
laterReads();
// Provide txtfull download for copyright book
enableDownload();
// Provide images download
imagesDownload();
// Provide tag search
tagOption();
// Ctrl+Enter comment submit
areaReply();
// Get page resources
function collectPageResources() {
collectElements();
collectInfos();
function collectElements() {
const elements = pageResource.elements;
elements.content = $('#content');
elements.bookMain = $(elements.content, 'div');
elements.header = $(elements.content, 'div>table');
elements.titleContainer = $(elements.header, 'table td>span');
elements.bookName = $(elements.header, 'b');
elements.recommend = $(elements.content, `a[href^="https://${location.host}/modules/article/uservote.php"]`);
elements.metaContainer = $(elements.header, 'tr+tr');
elements.metas = $All(elements.metaContainer, 'td');
elements.info = $(elements.bookMain, 'div+table');
elements.cover = $(elements.info, 'img');
elements.infoText = $(elements.info, 'td+td');
elements.notice = $All(elements.infoText, 'span.hottext>b');
elements.tags = elements.notice.length > 1 ? elements.notice[0] : null;
elements.notice = elements.notice[elements.notice.length-1];
elements.introduce = $All(elements.infoText, 'span');
elements.introduce = elements.introduce[elements.introduce.length-1];
elements.downloadContainer = $(pageResource.elements.bookMain, 'div>fieldset');
elements.downloadPanel = elements.downloadContainer ? elements.downloadContainer.parentElement : null;
}
function collectInfos() {
const info = pageResource.info;
const elements = pageResource.elements;
info.bookName = elements.bookName.innerText;
info.BID = Number(getUrlArgv('id') || 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.cover = elements.cover.src;
info.dlEnabled = $(elements.content, '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, '.'+CLASSNAME_BUTTON);
copyBtn.addEventListener('click', function() {
copyText(value);
showtip(TEXT_TIP_COPIED);
alertify.message(TEXT_ALT_META_COPIED.replaceAll('{M}', value));
});
settip(copyBtn, TEXT_TIP_COPY);
}
}
// Add to later-reads
function laterReads() {
// Make button
let btn = installBtn(makeBtn(inAfterbooks() ? 'remove' : 'add'));
// Update book info if in list
inAfterbooks() && add(false);
function add(alt=true) {
// Add to config
const config = CONFIG.bookcasePrefs.getConfig();
config.laterbooks.books[pageResource.info.BID] = {
sort: Object.keys(config.laterbooks.books).length + 1,
addTime: new Date().getTime(),
name: pageResource.info.bookName,
aid: pageResource.info.BID,
metas: pageResource.info.metas,
tags: pageResource.info.tags,
introduce: pageResource.info.introduce,
cover: pageResource.info.cover
};
CONFIG.bookcasePrefs.saveConfig(config);
// New button
removeBtn(btn);
btn = installBtn(makeBtn('remove'));
// Soft alert
alt && alertify.success(TEXT_ALT_BOOK_AFTERBOOKS_ADDED);
}
function remove() {
// Remove from config
const config = CONFIG.bookcasePrefs.getConfig();
const books = config.laterbooks.books;
const book = books[pageResource.info.BID];
if (!book) {return false;}
delete books[pageResource.info.BID];
Array.prototype.forEach.call(Object.values(books), (b) => (b.sort > book.sort && b.sort--));
CONFIG.bookcasePrefs.saveConfig(config);
// New button
removeBtn(btn);
btn = installBtn(makeBtn('add'));
// Soft alert
alertify.success(TEXT_ALT_BOOK_AFTERBOOKS_REMOVED);
}
function makeBtn(type='add') {
const btn = $CrE('span');
btn.classList.add(CLASSNAME_BUTTON);
switch (type) {
case 'add':
btn.innerHTML = TEXT_GUI_BOOK_READITLATER;
btn.addEventListener('click', add);
break;
case 'remove':
btn.innerHTML = TEXT_GUI_BOOK_DONTREADLATER;
btn.addEventListener('click', remove);
break;
}
return btn;
}
function installBtn(btn) {
pageResource.elements.recommend.previousElementSibling.insertAdjacentElement('afterend', btn);
btn.insertAdjacentText('beforebegin', '[');
btn.insertAdjacentText('afterend', ']');
return btn;
}
function removeBtn(btn) {
const parent = btn.parentElement;
for (const node of [btn.previousSibling, btn, btn.nextSibling]) {
parent.removeChild(node);
}
return btn;
}
function inAfterbooks() {
return CONFIG.bookcasePrefs.getConfig().laterbooks.books[pageResource.info.BID] ? true : false;
}
}
// Download copyright book
function enableDownload() {
if (pageResource.info.dlEnabled) {return false;};
// Download panel
// Create panel
let div = $CrE('div');
pageResource.elements.bookMain.appendChild(div);
div.outerHTML = HTML_DOWNLOAD_LINKS
.replaceAll('{ORIBOOKNAME}', pageResource.info.bookName)
.replaceAll('{BOOKID}', String(pageResource.info.BID))
.replaceAll('{CHARSET}', getUrlArgv('charset') ? '&charset=' + getUrlArgv('charset') : '')
// Use about:blank instead of direct url; aims to aviod unnecessary web requests
const container = pageResource.elements.downloadContainer = $(pageResource.elements.bookMain, 'div>fieldset');
div = pageResource.elements.downloadPanel = container.parentElement;
for (const a of $All(container, 'div>a')) {
//a.addEventListener('click', openDlPage);
}
// Notice board
pageResource.elements.notice.innerHTML = HTML_DOWNLOAD_BOARD
.replaceAll('{ORIBOOKNAME}', pageResource.info.bookName);
function openDlPage(e) {
e.preventDefault();
const url = e.target.href;
const win = window.open(`https://${location.host}/`);
win.history.replaceState({...win.history.state}, '', url);
}
}
// All images downloader
function imagesDownload() {
const container = pageResource.elements.downloadContainer;
const divImage = $CrE('div'), a = $CrE('a');
divImage.setAttribute('style', 'width:164px; float:left; text-align:center');
a.href = 'javascript:void(0);';
a.innerHTML = TEXT_GUI_BOOK_IMAGESDOWNLOAD;
a.addEventListener('click', confirm);
divImage.appendChild(a);
container.appendChild(divImage);
for (const div of $All(container, 'div')) {
div.style.width = '164px';
}
function confirm() {
const title = TEXT_ALT_DOWNLOADIMG_CONFIRM_TITLE;
const message = TEXT_ALT_DOWNLOADIMG_CONFIRM_MESSAGE.replace('{N}', pageResource.info.bookName);
const ok = TEXT_ALT_DOWNLOADIMG_CONFIRM_OK;
const cancel = TEXT_ALT_DOWNLOADIMG_CONFIRM_CANCEL;
alertify.confirm(title, message, download, function() {/* oncancel */}).set('labels', {ok: ok, cancel: cancel});
}
function download() {
// GUI
const delay = alertify.get('notifier','delay');
alertify.set('notifier','delay', 0);
let finished = false, CAll, CCur = 0;
const AM = new AsyncManager();
AM.onfinish = downloadFinish;
const box = alertify.message(TEXT_ALT_DOWNLOADIMG_STATUS_INDEX);
box.ondismiss = function() {return finished;}
// Start download
AM.add()
AndAPI.getNovelIndex({
aid: pageResource.info.BID,
lang: 0,
callback: function(xml) {
const allChapters = $All(xml, 'chapter');
const chapters = Array.prototype.filter.call(allChapters, (c) => (c.firstChild.nodeValue.includes('插图')));
CAll = chapters.length;
box.setContent(TEXT_ALT_DOWNLOADIMG_STATUS_LOADING.replace('{CCUR}', CCur).replace('{CALL}', CAll));
for (const chapter of chapters) {
AM.add();
getChapter(chapter.getAttribute('cid'), chapter.parentNode);
}
AM.finish();
}
});
AM.finishEvent = true;
function getChapter(cid, volume) {
AndAPI.getNovelContent({
aid: pageResource.info.BID,
cid: cid,
lang: 0,
callback: getImgs,
args: [volume]
});
function getImgs(str, volume) {
const imgs = str.match(/https?:[^<>]+/g);
const len = imgs.length.toString().length;
const CAM = new AsyncManager();
CAM.onfinish = chapterFinish;
for (let i = 0; i < imgs.length; i++) {
const img = imgs[i];
const src = img.match(/(https?:[^<>]+)/)[1];
const ext = src.match(/\.(\w+)$/) ? src.match(/\.(\w+)$/)[1] : 'jpg';
const filename = pageResource.info.bookName + '_' + volume.firstChild.nodeValue + ' ' + ['插图', '插圖'][getLang()] + '_' + fillNumber(i+1, len) + '.' + ext;
CAM.add();
downloadFile({
url: src,
name: filename,
onload: function() {
CAM.finish();
}
});
}
CAM.finishEvent = true;
function chapterFinish() {
AM.finish();
box.setContent(TEXT_ALT_DOWNLOADIMG_STATUS_LOADING.replace('{CCUR}', ++CCur).replace('{CALL}', CAll));
}
}
}
function downloadFinish() {
finished = true;
alertify.set('notifier','delay', delay);
box.dismiss();
alertify.success(TEXT_ALT_DOWNLOADIMG_STATUS_FINISH);
}
}
}
// Download copyright book full txt
function enableDownload_old() {
if (pageResource.info.dlEnabled) {return false;};
let div = $CrE('div');
pageResource.elements.bookMain.appendChild(div);
div.outerHTML = HTML_DOWNLOAD_LINKS_OLD
.replaceAll('{ORIBOOKNAME}', pageResource.info.bookName)
.replaceAll('{BOOKID}', String(pageResource.info.BID))
.replaceAll('{BOOKNAME}', encodeURIComponent(pageResource.info.bookName));
div = $('#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 ($('td > input[name="Submit"]') && !$('#ptitle')) {
const table = $('form>table');
const titleText = table.innerHTML.match(//)[0];
const titleHTML = titleText.replace(/^$/, '');
const titleEle = $CrE('tr');
const caption = $(table, 'caption');
table.insertBefore(titleEle, caption);
titleEle.outerHTML = titleHTML;
}
const commentArea = $('#pcontent'); if (!commentArea) {return false;};
const commentForm = $(`form[action^="https://${location.host}/modules/article/review"]`);
const commentSbmt = $('td > input[name="Submit"]');
const commenttitl = $('#ptitle');
const commentbttm = commentSbmt.parentElement;
/* ## Ctrl+Enter comment submit ## */
let btnSbmtValue = commentSbmt.value;
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();
// Provide image upload & insert
imageplus();
// At user
atUser();
// Comment auto-save
// GUI
const asTip = $CrE('span');
commentbttm.appendChild(asTip);
// Review-Page: Same rid, same savekey - 'rid123456'
// Book-Page & Book-Review-List-Page: Same bookid, same savekey - 'bid1234'
const rid = getUrlArgv({url: commentForm.action, name: 'rid', dealFunc: Number});
const aid = getUrlArgv({url: commentForm.action, name: 'aid', dealFunc: Number});
const bid = location.href.match(/\/book\/(\d+).htm/) ? Number(location.href.match(/\/book\/(\d+).htm/)[1]) : 0;
const key = rid ? 'rid' + String(rid) : 'bid' + String(bid);
let commentData = CONFIG.commentDrafts.getConfig()[key] || {
key : key,
rid : rid,
aid : aid,
bid : bid,
page : getUrlArgv({name: 'rid', dealFunc: Number, defaultValue: 1}),
time : (new Date()).getTime()
};
restoreDraft();
submitHook();
const events = ['focus', 'blur', 'mousedown', 'keydown', 'keyup', 'change'];
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 = CONFIG.commentDrafts.getConfig();
allCData[commentData.key] = commentData;
CONFIG.commentDrafts.saveConfig(allCData);
asTip.innerHTML = TEXT_GUI_AUTOSAVE;
}
function restoreDraft() {
const allCData = CONFIG.commentDrafts.getConfig();
if (!allCData[commentData.key]) {return false;};
if (!commenttitl.value && !commentArea.value) {
commentData = allCData[commentData.key];
commenttitl.value = commentData.title;
commentArea.value = commentData.content;
asTip.innerHTML = TEXT_GUI_AUTOSAVE_RESTORE;
}
return true;
}
function clearDraft() {
const allCData = CONFIG.commentDrafts.getConfig();
if (!allCData[commentData.key]) {return false;};
delete allCData[commentData.key];
CONFIG.commentDrafts.saveConfig(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') {
fixHTTPS.wait = fixHTTPS.wait ? fixHTTPS.wait : 0;
if (++fixHTTPS.wait > 50) {return false;}
DoLog('fixHTTPS: UBBEditor not loaded, waiting...');
setTimeout(fixHTTPS, NUMBER_ELEMENT_LOADING_WAIT_INTERVAL);
return false;
}
const eid = 'pcontent';
const menuItemInsertUrl = $(commentForm, '#menuItemInsertUrl');
const menuItemInsertImage = $(commentForm, '#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 imageplus() {
if (typeof(UBBEditor) === 'undefined') {
DoLog('imageplus: UBBEditor not loaded, waiting...');
setTimeout(imageplus, NUMBER_ELEMENT_LOADING_WAIT_INTERVAL);
return false;
}
// Imager menu
const menu = $('#UBB_Menu');
const elmImage = $(commentForm, '#menuItemInsertImage');
const onclick = elmImage.onclick;
const imagers = new PlusList({
id: 'plus_imager',
list: [
{value: TEXT_GUI_REVEIW_IMG_INSERTURL, tip: TEXT_TIP_REVIEW_IMG_INSERTURL, onclick: onclick},
{value: TEXT_GUI_REVEIW_IMG_SELECTIMG, tip: TEXT_TIP_REVIEW_IMG_SELECTIMG, onclick: pickfile}
],
parentElement: menu.parentElement,
insertBefore: $('#SmileListTable'),
visible: false,
onshow: onshow
});
elmImage.onclick = (e) => {
e.stopPropagation();
imagers.show();
};
document.addEventListener('click', imagers.hide);
// drag-drop & copy-paste
commentArea.addEventListener('paste', pictureGot);
commentArea.addEventListener('dragenter', destroyEvent);
commentArea.addEventListener('dragover', destroyEvent);
commentArea.addEventListener('drop', pictureGot);
function onshow() {
imagers.div.style.left = String(UBBEditor.GetPosition(elmImage).x) + 'px';
imagers.div.style.top = String(UBBEditor.GetPosition(elmImage).y + 20) + 'px';
}
function pickfile() {
const fileinput = $CrE('input');
fileinput.type = 'file';
fileinput.addEventListener('change', pictureGot);
fileinput.click();
}
function pictureGot(e) {
// Get picture file
const input = e.dataTransfer || e.clipboardData || window.clipboardData || e.target;
if (!input.files || input.files.length === 0) {return false;};
const file = input.files[0];
const mimetype = file.type;
const name = file.name;
// Pasting an unrecognizable file is not a mistake
// Maybe the user just wants to paste the filename here
// Otherwise getting an unrecognizable file is a mistake
if (!mimetype || mimetype.split('/')[0] !== 'image') {
if (!e.clipboardData && !window.clipboardData) {
destroyEvent(e);
alertify.error(TEXT_ALT_IMAGE_FORMATERROR);
}
return false;
} else {
destroyEvent(e);
}
// Insert picture marker
const marker = '[image_uploading={ID} name={NAME}]'.replace('{ID}', randstr(16, true, commentArea.value)).replace('{NAME}', name);
insertText(marker);
// Upload
alertify.notify(TEXT_ALT_IMAGE_UPLOAD_WORKING);
uploadImage({
file: file,
onerror: (e) => {
alertify.error(TEXT_ALT_IMAGE_UPLOAD_ERROR);
DoLog(LogLevel.Error, ['Upload error at imageplus>upload:', e]);
},
onload: (json) => {
const name = json.name;
const url = json.url;
commentArea.value = commentArea.value.replace(marker, url);
alertify.success(TEXT_ALT_IMAGE_UPLOAD_SUCCESS.replaceAll('{NAME}', name).replaceAll('{URL}', url));
}
});
}
}
function submitHook() {
const onsubmit = commentForm.onsubmit;
commentForm.onsubmit = onsubmitForm;
function onsubmitForm(e) {
// Cancel submit while content empty
if (commentArea.value === '' && commenttitl.value === '') {return false;};
// Clear Draft
clearDraft();
// Restore original submit button value
if (commentSbmt.value !== btnSbmtValue) {
commentSbmt.value = btnSbmtValue;
setTimeout(()=>{commentSbmt.click.call(commentSbmt);}, 0);
return false;
}
// Continue submit
return onsubmit ? onsubmit() : function() {return true;};
}
}
function atUser() {
if (typeof(UBBEditor) === 'undefined') {
DoLog(LogLevel.Info, 'atUser: UBBEditor not loaded, waiting...');
setTimeout(atUser, NUMBER_ELEMENT_LOADING_WAIT_INTERVAL);
return false;
}
const menu = $('#UBB_Menu');
const list = new PlusList({
id: 'plus_AtTable',
list: [],
parentElement: menu.parentElement,
insertBefore: $('#FontSizeTable'),
visible: false,
onshow: showlist
});
list.onhide = list.clear;
document.addEventListener('click', list.hide);
const firstBtn = menu.children[0];
const atBtn = $CrE('input');
atBtn.type = 'button';
atBtn.style.backgroundImage = 'none';
atBtn.value = '@';
atBtn.title = TEXT_GUI_AREAREPLY_AT;
atBtn.id = 'plus_At';
atBtn.classList.add(CLASSNAME_BUTTON);
atBtn.classList.add('UBB_MenuItem');
atBtn.addEventListener('click', (e) => {
e.stopPropagation();
list.show();
});
menu.insertBefore(atBtn, firstBtn);
function showlist(shown) {
if (shown) {return false;};
if (typeof(ubb_subdiv) === 'string' && typeof(hideeve) === 'function') {
hideeve(ubb_subdiv);
ubb_subdiv = 'plus_AtTable';
}
makelist();
list.ul.focus();
return true;
}
function makelist() {
// Get users
const allUsers = getAllUsers();
// Make list
for (const user of allUsers) {
const item = list.append({
value: user.userName,
tip: ()=>{return 'uid: ' + String(user.userID);},
onclick: btnClick
});
item.li.user = user;
item.button.user = user;
}
// Style
list.div.style.left = String(UBBEditor.GetPosition(atBtn).x) + 'px';
list.div.style.top = String(UBBEditor.GetPosition(atBtn).y + 20) + 'px';
return true;
function getAllUsers() {
const pageUsers = $All(`#content table strong>a[href^="https://${location.host}/userpage.php"]`);
const friends = getMyUserDetail().userFriends;
if (!friends) {
refreshMyUserDetail(refreshList);
return false;
}
// concat to one array
const allUsers = [];
for (const pageUser of pageUsers) {
// Valid check
if (isNaN(Number(pageUser.href.match(/\?uid=(\d+)/)[1]))) {continue;};
const user = {
userName: pageUser.innerText,
userID: Number(pageUser.href.match(/\?uid=(\d+)/)[1]),
referred: 0
}
if (!userExist(allUsers, user)) {
const userAsFriend = userExist(friends, user);
allUsers.push(userAsFriend ? userAsFriend : user);
}
}
for (const friend of friends) {
if (!userExist(allUsers, friend)) {
allUsers.push(friend);
}
}
// Sort by referred
allUsers.sort((a,b)=>{return (b.referred?b.referred:0) - (a.referred?a.referred:0);});
return allUsers;
// returns the exist user object found in users, or false if not found
function userExist(users, user) {
for (const u of users) {
if (u.userID === user.userID) {return u;};
}
return false;
}
}
function btnClick() {
const btn = this;
const user = btn.user;
const name = btn.user.userName;
const insertValue = '@' + name;
insertText(insertValue);
// referred increase
const userDetail = getMyUserDetail();
const friends = userDetail.userFriends;
user.referred = user.referred ? user.referred+1 : 1;
for (let i = 0; i < friends.length; i++) {
if (friends[i].userID === user.userID) {
friends[i] = user;
break;
}
}
CONFIG.userDtlePrefs.saveConfig(userDetail);
}
}
}
function insertText(insertValue) {
const insertPosition = commentArea.selectionEnd;
const text = commentArea.value;
const leftText = text.substr(0, insertPosition);
const rightText = text.substr(insertPosition);
// 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(rightText)) ? '' : ' ';
commentArea.value = leftText + insertValue + rightText;
const position = insertPosition + insertValue.length;
commentForm.scrollIntoView(); commentArea.focus(); commentArea.setSelectionRange(position, position);
}
}
// Review link add-on
function linkReview() {
// Get all review links and apply add-on functions
const allRLinks = $All(`td>a[href^="https://${location.host}/modules/article/reviewshow.php?"]`);
for (const RLink of allRLinks) {
lastPage(RLink);
}
// Provide button direct to review last page
// New version. Uses '&page=last' keyword.
function lastPage(a) {
const p = a.parentElement;
const lastpg = $CrE('a');
const strrid = getUrlArgv({url: a.href, name: 'rid'});
lastpg.href = URL_REVIEWSHOW_2.replace('{R}', strrid).replace('{P}', 'last');
lastpg.classList.add(CLASSNAME_BUTTON);
lastpg.target = '_blank';
lastpg.innerText = TEXT_GUI_LINK_TOLASTPAGE;
p.insertBefore(lastpg, a);
}
}
// Side functions area
function sideFunctions() {
const SPanel = new SidePanel();
SPanel.usercss = CSS_SIDEPANEL;
SPanel.create();
SPanel.setPosition('bottom-right');
commonButtons();
return SPanel;
function commonButtons() {
// Button show/hide-all-buttons
const btnShowHide = SPanel.add({
faicon: 'fa-solid fa-down-left-and-up-right-to-center',
className: 'accept-pointer',
tip: '隐藏面板',
onclick: (function() {
let hidden = false;
return (e) => {
hidden = !hidden;
btnShowHide.faicon.className = 'fa-solid ' + (hidden ? 'fa-up-right-and-down-left-from-center' : 'fa-down-left-and-up-right-to-center');
btnShowHide.classList[hidden ? 'add' : 'remove']('low-opacity');
btnShowHide.setAttribute('aria-label', (hidden ? '显示面板' : '隐藏面板'));
SPanel.elements.panel.style.pointerEvents = hidden ? 'none' : 'auto';
for (const button of SPanel.elements.buttons) {
if (button === btnShowHide) {continue;}
//button.style.display = hidden ? 'none' : 'block';
button.style.pointerEvents = hidden ? 'none' : 'auto';
button.style.opacity = hidden ? '0' : '1';
}
};
}) ()
});
// Button scroll-to-bottom
const btnDown = SPanel.add({
faicon: 'fa-solid fa-angle-down',
tip: '转到底部',
onclick: (e) => {
const elms = [document.body.parentElement, $('#content'), $('#contentmain')];
for (const elm of elms) {
elm && elm.scrollTo && elm.scrollTo(elm.scrollLeft, elm.scrollHeight);
}
}
});
// Button scroll-to-top
const btnUp = SPanel.add({
faicon: 'fa-solid fa-angle-up',
tip: '转到顶部',
onclick: (e) => {
const elms = [document.body.parentElement, $('#content'), $('#contentmain')];
for (const elm of elms) {
elm && elm.scrollTo && elm.scrollTo(elm.scrollLeft, 0);
}
}
});
// Darkmode
/*
const btnDarkmode = SPanel.add({
faicon: 'fa-solid ' + (DMode.isActivated() ? 'fa-sun' : 'fa-moon'),
tip: '明暗切换',
onclick: (e) => {
DMode.toggle();
btnDarkmode.faicon.className = 'fa-solid ' + (DMode.isActivated() ? 'fa-sun' : 'fa-moon');
}
});
*/
// Refresh page
const btnRefresh = SPanel.add({
faicon: 'fa-solid fa-rotate-right',
tip: '刷新页面',
onclick: (e) => {
reloadPage();
}
});
}
}
// Reviewedit page add-on
function pageReviewedit() {
redirectToCorrectPage();
function redirectToCorrectPage() {
// Get redirect target rid
const refreshMeta = $('meta[http-equiv="refresh"]');
const metaurl = refreshMeta.content.match(/url=(.+)/)[1];
if (!refreshMeta) {return false;};
if (getUrlArgv({url: metaurl, name: 'page'})) {return false;};
// Read correct redirect location
const rid = Number(getUrlArgv({url: metaurl, name: 'rid'}));
const config = CONFIG.BkReviewPrefs.getConfig();
const history = config.history;
const pageHist = history[rid];
if (!pageHist) {return false;}
const url = pageHist.href;
// Check if time expired (Expire time: 30 seconds)
if ((new Date()).getTime() - pageHist.time > 30*1000) {
// Delete expired record
delete history[rid];
CONFIG.BkReviewPrefs.saveConfig(config);
}
// Redirect link
$('a').href = url;
// Redirect
setTimeout(() => {location.href = url;}, 1500);
}
}
// Review page add-on
function pageReview() {
// Elements
const main = $('#content');
const headBars = $All(main, 'tr>td[align]');
// Page Info
const rid = Number(getUrlArgv('rid'));
const aid = getUrlArgv('aid') ? Number(getUrlArgv('aid')) : Number($(main, 'td[width]>a').href.match(/(\d+)\.html?$/)[1]);
const page = Number($('#pagelink strong').innerText);
const title = $(main, 'th>strong').textContent;
// URL correction
correctURL();
// Enhancements
pageStatus();
downloader();
sideButtons();
beautifier();
floorEnhance();
autoRefresh();
addFavorite();
addUnlock();
function correctURL() {
(getUrlArgv('page') === 'last' || !getUrlArgv('page')) && setPageUrl(URL_REVIEWSHOW.replace('{A}', aid).replace('{R}', rid).replace('{P}', page));
}
function sideButtons() {
// Last page
SPanel.add({
faicon: 'fa-solid fa-angles-right',
tip: '最后一页',
onclick: (e) => {findclick('#pagelink>.last');}
});
// Next page
SPanel.add({
faicon: 'fa-solid fa-angle-right',
tip: '下一页',
onclick: (e) => {findclick('#pagelink>.next');}
});
// Previous page
SPanel.add({
faicon: 'fa-solid fa-angle-left',
tip: '上一页',
onclick: (e) => {findclick('#pagelink>.prev');}
});
// First page
SPanel.add({
faicon: 'fa-solid fa-angles-left',
tip: '第一页',
onclick: (e) => {findclick('#pagelink>.first');}
});
function findclick(selector) {return $(selector) && $(selector).click();}
}
function beautifier() {
// GUI
const span = $CrE('span');
const check = $CrE('input');
check.type = 'checkbox';
check.checked = CONFIG.BeautifierCfg.getConfig().reviewshow.beautiful;
span.innerHTML = TEXT_GUI_REVIEW_BEAUTIFUL;
span.classList.add(CLASSNAME_BUTTON);
span.style.marginLeft = '0.5em';
span.addEventListener('click', toggleBeautiful);
check.addEventListener('click', toggleBeautiful);
settip(span, TEXT_TIP_REVIEW_BEAUTIFUL);
settip(check, TEXT_TIP_REVIEW_BEAUTIFUL);
headBars[0].appendChild(span);
headBars[0].appendChild(check);
CONFIG.BeautifierCfg.getConfig().reviewshow.beautiful && beautiful();
function toggleBeautiful(e) {
// stop event
destroyEvent(e);
// Togle & save to config
const config = CONFIG.BeautifierCfg.getConfig();
config.reviewshow.beautiful = !config.reviewshow.beautiful;
CONFIG.BeautifierCfg.saveConfig(config);
setTimeout(() => {check.checked = config.reviewshow.beautiful;}, 0);
alertify.notify(config.reviewshow.beautiful ? TEXT_ALT_BEAUTIFUL_ON : TEXT_ALT_BEAUTIFUL_OFF);
// beautifier
config.reviewshow.beautiful ? beautiful() : recover();
}
function beautiful() {
const config = CONFIG.BeautifierCfg.getConfig();
addStyle(CSS_REVIEWSHOW
.replaceAll('{BGI}', config.backgroundImage)
.replaceAll('{S}', config.textScale)
, 'beautifier');
scaleimgs();
hookPosition();
function scaleimgs() {
const imgs = $All('.divimage>img');
const w = main.clientWidth * 0.8 - 3; // td.width = "80%", .even {padding: 3px;}
for (const img of imgs) {
img.width = w;
}
}
}
function recover() {
addStyle('', 'beautifier');
restorePosition();
}
function hookPosition() {
if (!CONFIG.BeautifierCfg.getConfig().reviewshow.beautiful) {return false;};
if (typeof(UBBEditor) !== 'object') {
hookPosition.wait = hookPosition.wait ? hookPosition.wait : 0;
if (++hookPosition.wait > 50) {return false;}
DoLog('beautiful/hookPosition: UBBEditor not loaded, waiting...');
setTimeout(hookPosition, NUMBER_ELEMENT_LOADING_WAIT_INTERVAL);
return false;
}
UBBEditor.GetPosition_BK = UBBEditor.GetPosition;
UBBEditor.GetPosition = function (obj) {
var r = new Array();
r.x = obj.offsetLeft;
r.y = obj.offsetTop;
while (obj = obj.offsetParent) {
if (unsafeWindow.$(obj).getStyle('position') == 'absolute' || unsafeWindow.$(obj).getStyle('position') == 'relative') break;
r.x += obj.offsetLeft;
r.y += obj.offsetTop;
}
r.x -= main.scrollLeft;
r.y -= main.scrollTop;
return r;
}
}
function restorePosition() {
if (typeof(UBBEditor) !== 'object') {return false;};
if (!UBBEditor.GetPosition_BK) {return false;};
UBBEditor.GetPosition = UBBEditor.GetPosition_BK;
}
}
function pageStatus() {
window.addEventListener('load', () => {
// Recover page status
applyPageStatus();
// Record the current page status of current review
setInterval(recordPage, 1000);
});
}
// Apply page status sored in history record
function applyPageStatus() {
const config = CONFIG.BkReviewPrefs.getConfig();
const history = config.history;
const pageHist = history[rid];
// Scroll to the last position
if (pageHist && pageHist.page === page) {
// Check if time expired
if (pageHist.time && (new Date()).getTime() - pageHist.time < 30*1000) {
// Do not scroll when opening a positioned link(http[s]://.../...#yidxxxxxx)
if (/#yid\d+$/.test(location.href)) {return;}
// Scroll
pageHist.scrollX !== undefined && window.scrollTo(pageHist.scrollX, pageHist.scrollY);
pageHist.contentsclX !== undefined && main.scrollTo(pageHist.contentsclX, pageHist.contentsclY);
} else {
// Delete expired record
delete history[rid];
CONFIG.BkReviewPrefs.saveConfig(config);
}
}
}
function recordPage() {
const config = CONFIG.BkReviewPrefs.getConfig();
const history = config.history;
// Save page history
config.history[rid] = {
rid: rid,
aid: aid,
page: page,
href: URL_REVIEWSHOW.replace('{R}', String(rid)).replace('{A}', String(aid)).replace('{P}', String(page)),
scrollX: window.pageXOffset,
scrollY: window.pageYOffset,
contentsclX: main.scrollLeft,
contentsclY: main.scrollTop,
time: (new Date()).getTime()
}
CONFIG.BkReviewPrefs.saveConfig(config);
}
function floorEnhance() {
const floors = getAllFloors();
floors.forEach((f)=>(correctFloorLink(f)));
for (const floor of floors) {
alinkEdit(floor);
addQuoteBtn(floor);
addQueryBtn(floor);
addRemark(floor);
alinktofloor(floor.table);
}
}
function alinktofloor(parent=main) {
const floorLinks = $All(main, `a[name][href^="https://${location.host}/modules/article/reviewshow.php"][href*="#yid"]`);
for (const a of $All(parent, 'a')) {
if (!a.href.match(/^https?:\/\/www\.wenku8\.(net|cc)\/modules\/article\/reviewshow\.php\?(&?rid=\d+|&?aid=\d+|&?page=\d+){1,4}#yid\d+$/)) {continue;};
for (const flink of floorLinks) {
if (isSameReply(a, flink)) {
// Set scroll target
a.targetNode = flink;
while (a.targetNode.nodeName !== 'TABLE') {
a.targetNode = a.targetNode.parentElement;
}
// Scroll when clicked
a.addEventListener('click', (e) => {
destroyEvent(e);
e.currentTarget.targetNode.scrollIntoView();
})
};
}
}
function isSameReply(link1, link2) {
const url1 = link1.href.toLowerCase().replace('http://', 'https://');
const url2 = link2.href.toLowerCase().replace('http://', 'https://');
const rid1 = getUrlArgv({url: url1, name: 'rid', defaultValue: null});
const yid1 = url1.match(/#yid(\d+)/) ? url1.match(/#yid(\d+)/)[1] : null;
const rid2 = getUrlArgv({url: url2, name: 'rid', defaultValue: null});
const yid2 = url2.match(/#yid(\d+)/) ? url2.match(/#yid(\d+)/)[1] : null;
return rid1 === rid2 && yid1 === yid2;
}
}
function alinkEdit(parent=document) {
const eLinks = $All(`a[href^="https://${location.host}/modules/article/reviewedit.php?yid="]`);
for (const eLink of eLinks) {
eLink.addEventListener('click', (e) => {
// NO e.stopPropagation() here. Just hooks the open action.
e.preventDefault();
// Open editor dialog
openDialog(e.target.href + '&ajax_gets=jieqi_contents');
// Show mask if mask not shown
!document.getElementById("mask") && showMask();
})
}
}
function autoRefresh() {
let working=false, interval=0;
const pagelink = $('#pagelink');
const tdLink = pagelink.parentElement;
const trContainer = tdLink.parentElement;
const tdAutoRefresh = $CrE('td');
const chkAutoRefresh = $CrE('input');
const txtAutoRefresh = $CrE('span');
const txtPaused = $CrE('span');
const ptitle = $('#ptitle');
const pcontent = $('#pcontent');
txtAutoRefresh.innerText = TEXT_GUI_AUTOREFRESH;
txtAutoRefresh.classList.add(CLASSNAME_BUTTON);
txtAutoRefresh.addEventListener('click', toggleRefresh);
chkAutoRefresh.addEventListener('click', toggleRefresh);
chkAutoRefresh.type = 'checkbox';
chkAutoRefresh.checked = false;
txtPaused.innerText = '';
txtPaused.classList.add(CLASSNAME_TEXT);
txtPaused.style.marginLeft = '0.5em';
tdAutoRefresh.style.align = 'left';
tdAutoRefresh.appendChild(txtAutoRefresh);
tdAutoRefresh.appendChild(chkAutoRefresh);
tdAutoRefresh.appendChild(txtPaused);
trContainer.insertBefore(tdAutoRefresh, tdLink);
// Apply config
CONFIG.BkReviewPrefs.getConfig().autoRefresh ? toggleRefresh() : function() {};
/* No pauses after v1.5.7
// Show pause
// Note: Blur event triggers after Focus event was triggered
for (const editElm of [ptitle, pcontent]) {
if (!editElm) {continue;};
editElm.addEventListener('blur', (e) => {
txtPaused.innerText = '';
});
editElm.addEventListener('focus', (e) => {
txtPaused.innerText = TEXT_GUI_AUTOREFRESH_PAUSED;
});
}
*/
function toggleRefresh(e) {
// stop event
destroyEvent(e);
// Not in last Page, no auto refresh
if (!isCurLastPage() && !working) {
const box = alertify.notify(TEXT_ALT_AUTOREFRESH_NOTLAST);
box.callback = (isClicked) => {isClicked && (location.href = $('#pagelink>a.last').href);};
return false;
}
// toggle
working = !working;
working ? interval = setInterval(refresh, 20*1000) : clearInterval(interval);
working && refresh();
// Save to config
const review = CONFIG.BkReviewPrefs.getConfig();
review.autoRefresh = working;
CONFIG.BkReviewPrefs.saveConfig(review);
setTimeout(() => {chkAutoRefresh.checked = working;}, 0);
alertify.notify(working ? TEXT_ALT_AUTOREFRESH_ON : TEXT_ALT_AUTOREFRESH_OFF);
}
function refresh() {
const box = alertify.notify(TEXT_ALT_AUTOREFRESH_WORKING);
const url = URL_REVIEWSHOW.replace('{R}', String(rid)).replace('{A}', String(aid)).replace('{P}', 'last');
getDocument(url, refreshLoaded, url);
function refreshLoaded(oDoc, pageurl) {
// Clost alert box
box.exist ? box.close.apply(box) : function() {};
// Update all existing floor content (and title)
const nowfloors = $All('#content>table[class="grid"]');
const newfloors = $All(oDoc, '#content>table[class="grid"]');
let i, modified = false;
for (i = 1; i < Math.min(nowfloors.length, newfloors.length); i++) {
isFloorTable(nowfloors[i]) && isFloorTable(newfloors[i]) && getFloorNumber(nowfloors[i]) === getFloorNumber(newfloors[i]) && updatefloor(i);
}
modified && alertify.notify(TEXT_ALT_AUTOREFRESH_MODIFIED);
const newtop = getTopFloorNumber(oDoc);
const nowtop = getTopFloorNumber(document);
if (unsafeWindow.isPY_DNG && newtop === 9899) {
sendReviewReply({rid: rid, title: '测试标题', content: '测试内容'});
}
if (newtop > nowtop) {
const newmain = $(oDoc, '#content');
const eleLastPage = $(oDoc, '#pagelink a.last');
const urlLastPage = newmain.url = eleLastPage.href;
const newpage = Number(getUrlArgv({url: urlLastPage, name: 'page'}));
const newfloors = getAllFloors(newmain);
const nowfloors = getAllFloors();
if (newpage === page) {
// In same page, append floors
for (let i = nowfloors.length; i < newfloors.length; i++) {
const floor = newfloors[i];
appendfloor(floor);
}
} else {
// In New page, remake floors
let box = alertify.notify(TEXT_ALT_AUTOREFRESH_APPLIED);
// Remove old floors
for (const oldfloor of nowfloors) {
oldfloor.table.parentElement.removeChild(oldfloor.table);
}
// Append new floors
for (const newfloor of newfloors) {
appendfloor(newfloor);
}
// Remake #pagelink
$(main, '#pagelink').innerHTML = $(newmain, '#pagelink').innerHTML;
// Reset location.href
page !== 'last' && setPageUrl(urlLastPage);
return true;
}
} else {
alertify.message(TEXT_ALT_AUTOREFRESH_NOMORE);
return false;
}
function updatefloor(i) {
const nowfloor = nowfloors[i];
const newfloor = newfloors[i];
const nowTitle = getEleFloorTitle(nowfloor);
const newTitle = getEleFloorTitle(newfloor);
const nowContent = getEleFloorContent(nowfloor);
const newContent = getEleFloorContent(newfloor);
if (nowTitle.innerHTML !== newTitle.innerHTML) {
nowTitle.innerHTML = newTitle.innerHTML;
nowTitle.classList.add(CLASSNAME_MODIFIED);
nowTitle.addEventListener('click', (e) => {e.currentTarget.classList.remove(CLASSNAME_MODIFIED);});
modified = true;
}
if (getFloorContent(nowContent) !== getFloorContent(newContent)) {
nowContent.innerHTML = newContent.innerHTML;
nowContent.classList.add(CLASSNAME_MODIFIED);
nowContent.addEventListener('click', (e) => {e.currentTarget.classList.remove(CLASSNAME_MODIFIED);});
modified = true;
}
if (modified) {
alinktofloor(nowfloor);
}
}
}
}
function isCurLastPage() {
return $('#pagelink>strong').innerText === $('#pagelink>a.last').innerText;
}
function getTopFloorNumber(oDoc) {
const tblfloors = $All(oDoc, '#content>table[class="grid"]');
for (let i = tblfloors.length-1; i >= 0; i--) {
const tbllast = tblfloors[i];
if (isFloorTable(tbllast)) {return getFloorNumber(tbllast);}
}
return null;
}
}
function correctFloorLink(floor) {
floor.hrefa.href = floor.href;
}
function addFavorite() {
// Create GUI
const spliter = $CrE('span');
const favorBtn = $CrE('span');
const favorChk = $CrE('input');
spliter.style.marginLeft = '1em';
favorBtn.innerText = TEXT_GUI_REVIEW_ADDFAVORITE;
favorBtn.classList.add(CLASSNAME_BUTTON);
favorChk.type = 'checkbox';
favorChk.checked = CONFIG.BkReviewPrefs.getConfig().favorites.hasOwnProperty(rid);
favorBtn.addEventListener('click', checkChange);
favorChk.addEventListener('change', checkChange);
headBars[0].appendChild(spliter);
headBars[0].appendChild(favorBtn);
headBars[0].appendChild(favorChk);
function checkChange(e) {
if (e && e.target === favorChk) {
destroyEvent(e);
}
let inFavorites;
const config = CONFIG.BkReviewPrefs.getConfig();
if (config.favorites.hasOwnProperty(rid)) {
delete config.favorites[rid];
inFavorites = false;
} else {
config.favorites[rid] = {
rid: rid,
name: title,
href: URL_REVIEWSHOW_3.replace('{R}', rid).replace('{A}', aid),
time: (new Date()).getTime(), // time added in version 1.6.7
tiptitle: null
};
inFavorites = true;
}
CONFIG.BkReviewPrefs.saveConfig(config);
setTimeout(() => {favorChk.checked = inFavorites;}, 0);
alertify.notify((inFavorites ? TEXT_GUI_REVIEW_FAVORADDED : TEXT_GUI_REVIEW_FAVORDELED).replace('{N}', title));
}
function updateFavorite() {
const config = CONFIG.BkReviewPrefs.getConfig();
if (config.favorites.hasOwnProperty(rid)) {
config.favorites[rid] = {
rid: rid,
name: title,
href: URL_REVIEWSHOW_3.replace('{R}', rid).replace('{A}', aid)
};
}
}
}
function addQuoteBtn(floor) {
const table = floor.table;
const numberEle = $(table, '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() {
// Get content textarea
const pcontent = $('#pcontent');
const form = $(`form[action^="https://${location.host}/modules/article/review"]`);
// Create button
const btn = $CrE('span');
btn.classList.add(CLASSNAME_BUTTON);
btn.addEventListener('click', quoteThisFloor);
btn.innerHTML = '引用';
const tip_panel = $CrE('div');
tip_panel.insertAdjacentText('afterbegin', '或者,');
const btn_qtnum = $CrE('span');
btn_qtnum.classList.add(CLASSNAME_BUTTON);
btn_qtnum.addEventListener('click', quoteFloorNum);
btn_qtnum.innerHTML = '仅引用序号';
tip_panel.appendChild(btn_qtnum);
const panel = tippy(btn, {
content: tip_panel,
theme: 'wenku_tip',
placement: 'top',
interactive: true,
});
return btn;
function quoteThisFloor() {
// In DOM Events, keyword points to the Event Element.
const numberEle = $(this.parentElement, 'a[name]');
const numberText = numberEle.innerText;
const url = URL_REVIEWSHOW_4.replace('{R}', rid).replace('{P}', page).replace('{Y}', numberEle.name);
const contentEle = $(this.parentElement.parentElement, '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(rightText)) ? '' : ' ';
pcontent.value = leftText + insertValue + rightText;
const position = insertPosition + (pcontent.value.length - text.length);
form.scrollIntoView(); pcontent.focus(); pcontent.setSelectionRange(position, position);
}
function quoteFloorNum() {
// In DOM Events, keyword points to the Event Element.
const numberEle = $(this.parentElement.parentElement.parentElement.parentElement.parentElement, 'a[name]');
const numberText = numberEle.innerText;
const url = URL_REVIEWSHOW_4.replace('{R}', rid).replace('{P}', page).replace('{Y}', numberEle.name);
const contentEle = $(this.parentElement.parentElement.parentElement.parentElement.parentElement.parentElement, 'hr+div');
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]';
insertValue = insertValue.replace('U', url).replace('N', numberText);
// 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(rightText)) ? '' : ' ';
pcontent.value = leftText + insertValue + rightText;
const position = insertPosition + (pcontent.value.length - text.length);
form.scrollIntoView(); pcontent.focus(); pcontent.setSelectionRange(position, position);
}
}
}
function addQueryBtn(floor) {
// Get container div
const div = floor.leftdiv;
// Create buttons
const qBtn = $CrE('a'); // Button for query reviews
const iBtn = $CrE('a'); // Button for query userinfo
const mBtn = $CrE('a'); // Button for edit user remark
// Get UID
const user = $(div, 'a');
const name = user.innerText;
const UID = Math.floor(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));
mBtn.href = 'javascript: void(0);'
qBtn.target = '_blank';
iBtn.target = '_blank';
mBtn.addEventListener('click', editUserRemark.bind(null, UID, name, reloadRemarks));
qBtn.innerText = TEXT_GUI_USER_REVIEWSEARCH;
iBtn.innerText = TEXT_GUI_USER_USERINFO;
mBtn.innerText = TEXT_GUI_USER_USERREMARKEDIT;
// Append to GUI
div.appendChild($CrE('br'));
div.appendChild(iBtn);
div.appendChild(qBtn);
div.insertBefore(spliter, qBtn);
div.appendChild($CrE('br'));
div.appendChild(mBtn);
function reloadRemarks() {
const floors = getAllFloors();
floors.forEach((f) => (addRemark(f)));
}
}
function addRemark(floor) {
// Get container div
const div = floor.leftdiv;
const strong = $(div, 'strong');
// Get config
const config = CONFIG.RemarksConfig.getConfig();
const uid = Math.floor($(div, 'strong>a').href.match(/\?uid=(\d+)/)[1]);
const user = (config.user[uid] || {});
if ($(div, '.user-remark')) {
// Edit remark displayer
const name = $(div, '.user-remark-remark');
name.innerText = user.remark || TEXT_GUI_USER_USERREMARKEMPTY;
name.style.color = user.remark ? 'black' : 'grey';
} else {
// Add remark displayer
const container = $CrE('span');
const br = $CrE('br');
const name = $CrE('span');
container.classList.add('user-remark');
container.classList.add(CLASSNAME_TEXT);
container.innerText = TEXT_GUI_USER_USERREMARKSHOW;
name.innerText = user.remark || TEXT_GUI_USER_USERREMARKEMPTY;
name.style.color = user.remark ? 'black' : 'grey';
name.classList.add('user-remark-remark');
container.appendChild(name);
strong.insertAdjacentElement('afterend', br);
br.insertAdjacentElement('afterend', container);
}
}
// Provide a hidden function to reply overtime book-reviews
function addUnlock() {
listen();
function listen() {
if ($('#pcontent')) {return;}
const target = $('#content>table>caption+tbody>tr>td:nth-child(2)');
let count = 0;
document.addEventListener('click', function hidden_unlocker(e) {
e.target === target ? count++ : (count = 0);
count >= 10 && add();
count >= 10 && document.removeEventListener('click', hidden_unlocker);
count >= 10 && (target.innerHTML = TEXT_GUI_REVIEW_UNLOCK_WARNING);
});
}
function add() {
const container = $CrE('div');
$('#content').appendChild(container);
makeEditor(container, rid, aid);
}
}
// Reply without refreshing the document
function hookReply() {
const form = $('form[name="frmreview"]');
const onsubmit = form.onsubmit;
form.onsubmit = function() {
const title = $(form, '#ptitle').value;
const content = $(form, '#pcontent').value;
(onsubmit ? onsubmit() : true) && sendReviewReply({
rid: rid, title: title, content: content,
onload: function(oDoc) {
// Make floor(s)
},
onerror: function(e) {
DoLog(LogLevel.Error, 'pageReview/hookReply: submit onerror.');
}
});
};
}
function downloader() {
// GUI
const pageCountText = $('#pagelink>.last').href.match(/page=(\d+)/)[1];
const lefta = $(headBars[0], 'a');
const lefttext = document.createTextNode('书评回复');
clearChildnodes(headBars[0]);
headBars[0].appendChild(lefta);
headBars[0].appendChild(lefttext);
headBars[0].width = '45%';
headBars[1].width = '55%';
const saveBtn = $CrE('span');
saveBtn.innerText = TEXT_GUI_DOWNLOAD_REVIEW.replaceAll('A', pageCountText);
saveBtn.classList.add(CLASSNAME_BUTTON);
saveBtn.addEventListener('click', downloadWholePost);
headBars[1].appendChild(saveBtn);
const spliter = $CrE('span');
const bbcdTxt = $CrE('span');
const bbcdChk = $CrE('input');
spliter.style.marginLeft = '1em';
bbcdTxt.innerText = TEXT_GUI_DOWNLOAD_BBCODE;
bbcdChk.type = 'checkbox';
bbcdChk.checked = CONFIG.BkReviewPrefs.getConfig().bbcode;
bbcdTxt.addEventListener('click', bbcodeOnclick);
bbcdChk.addEventListener('click', bbcodeOnclick);
settip(bbcdTxt, TEXT_TIP_DOWNLOAD_BBCODE);
settip(bbcdChk, TEXT_TIP_DOWNLOAD_BBCODE);
bbcdTxt.classList.add(CLASSNAME_BUTTON);
headBars[1].appendChild(spliter);
headBars[1].appendChild(bbcdTxt);
headBars[1].appendChild(bbcdChk);
function bbcodeOnclick(e) {
destroyEvent(e);
if (downloadWholePost.working) {
alertify.warning(TEXT_ALT_DOWNLOAD_BBCODE_NOCHANGE);
return false;
}
const cmConfig = CONFIG.BkReviewPrefs.getConfig();
cmConfig.bbcode = !cmConfig.bbcode;
setTimeout(() => {bbcdChk.checked = cmConfig.bbcode;}, 0);
CONFIG.BkReviewPrefs.saveConfig(cmConfig);
}
// ## 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, '#content');
const avatars = $All(main, '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({name: 'rid', dealFunc: Number, defaultValue: 0});
data.page = getUrlArgv({name: 'page', dealFunc: Number, defaultValue: 1});
data.title = $(main, '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, 'tr');
elements.tdUser = $(elements.table, 'td.odd');
elements.tdReply = $(elements.table, 'td.even');
elements.divUser = $(elements.tdUser, 'div');
elements.aUser = $(elements.divUser, 'a');
elements.attr = $(elements.tdReply, 'div a').parentElement;
elements.time = elements.attr.childNodes[0];
elements.number = $(elements.attr, 'a[name]');
elements.title = $(elements.tdReply, 'div>strong');
elements.content = $(elements.tdReply, '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 = CONFIG.BkReviewPrefs.getConfig().bbcode ? getFloorContent(elements.content, true) : 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 page 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 = $('#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;
bbcdTxt.classList.add(CLASSNAME_DISABLED);
// 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 = '文库贴 - ' + data.title + ' - ' + data.id.toString() + '.txt';
const a = $CrE('a');
a.href = url;
a.download = name;
a.click();
// GUI
saveBtn.innerText = TEXT_GUI_DOWNLOADFINISH_REVIEW;
alertify.success(TEXT_ALT_DOWNLOADFINISH_REVIEW.replaceAll('{T}', data.title).replaceAll('{I}', data.id).replaceAll('{N}', name));
// Work finish
downloadWholePost.working = false;
bbcdTxt.classList.remove(CLASSNAME_DISABLED);
})
}
}
// Get all floor object
/* Contains:
** floor.table
** floor.tbody
** floor.tr
** floor.lefttd
** floor.righttd
** floor.leftdiv
** floor.titlediv
** floor.titlestrong
** floor.metadiv
** floor.replydiv
*/
function getAllFloors(parent=main) {
const avatars = $All(parent, 'table div img.avatar');
const floors = [];
for (const avt of avatars) {
const floor = {};
floor.leftdiv = avt.parentElement;
floor.lefttd = floor.leftdiv.parentElement;
floor.tr = floor.lefttd.parentElement
floor.righttd = floor.tr.children[1];
floor.titlediv = floor.righttd.children[0];
floor.titlestrong = floor.titlediv.children[0];
floor.metadiv = floor.righttd.children[1];
floor.replydiv = floor.righttd.children[3];
floor.hrefa = $(floor.metadiv, 'a[name]');
floor.tbody = floor.tr.parentElement;
floor.table = floor.tbody.parentElement;
floor.rid = Number(getUrlArgv({url: parent.url || location.href, name: 'rid'}));
floor.aid = Number($(parent, 'td[width]>a').href.match(/(\d+)\.html?$/)[1]);
floor.page = Number($(avt.ownerDocument, '#pagelink strong').innerText);
floor.pagehref = URL_REVIEWSHOW.replace('{R}', floor.rid.toString()).replace('{A}', floor.aid.toString()).replace('{P}', floor.page.toString());
floor.href = URL_REVIEWSHOW_5.replace('{R}', floor.rid.toString()).replace('{A}', floor.aid.toString()).replace('{P}', floor.page.toString()).replace('{Y}', floor.hrefa.name);
floors.push(floor);
}
return floors;
}
// Validate a
element whether is a floor
function isFloorTable(tbl) {
return $(tbl, 'a[href*="#yid"][name^="yid"]') ? true : false;
}
// Get floor title element ()
// Argv:
element of the floor
function getEleFloorTitle(tblfloor) {
return $(tblfloor, 'td.even>div:first-child>strong'); // or :nth-child(1)
}
// Get floor content element (
)
// Argv:
element of the floor
function getEleFloorContent(tblfloor) {
return $(tblfloor, 'td.even>hr+div');
}
// Get the floor number
// Argv:
element of the floor
function getFloorNumber(tblfloor) {
const eleNumber = $(tblfloor, 'a[name^="yid"]');
return eleNumber ? Number(eleNumber.innerText.match(/\d+/)[0]) : false;
}
// Get floor content by BBCode format (content only, no title)
// Argv:
content Element
function getFloorContent(contentEle, original=false) {
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':
// wenku8 has forbidden [img] tag for secure reason (preventing CSRF)
//content += '[img]S[/img]'.replace('S', node.src);
content += original ? '[img]S[/img]'.replace('S', node.src) : ' S '.replace('S', node.src);
break;
case 'A':
content += '[url=U]T[/url]'.replace('U', node.getAttribute('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, original);
} else {
content += getFloorContent(node, original);
}
break;
case 'CODE': content += getFloorContent(node, original); break; // Just ignore
case 'PRE': content += getFloorContent(node, original); break; // Just ignore
case 'SPAN': content += getFontedSubcontent(node); break; // Size and color
case 'P': content += getFontedSubcontent(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, original); 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, original);
return '[{T}]{S}[/{T}]'.replaceAll('{T}', tag).replaceAll('{S}', subContent);
}
function getFontedSubcontent(node) {
let tag, value;
let strSize = node.style.fontSize.match(/\d+/);
let strColor = node.style.color;
let strAlign = node.align;
strSize = strSize ? strSize[0] : null;
strColor = strColor ? rgbToHex.apply(null, strColor.match(/\d+/g)) : null;
tag = tag || (strSize ? 'size' : null);
tag = tag || (strColor ? 'color' : null);
tag = tag || (strAlign ? 'align' : null);
value = value || strSize || null;
value = value || strColor || null;
value = value || strAlign || null;
const subContent = getFloorContent(node, original);
if (tag && value) {
return '[{T}={V}]{S}[/{T}]'.replaceAll('{T}', tag).replaceAll('{V}', value).replaceAll('{S}', subContent);
} else {
return subContent;
}
}
}
// Append floor to #content
function appendfloor(floor) {
// Append
const table = floor.table;
const elmafter = $(main, 'table.grid+table[border]');
main.insertBefore(table, elmafter);
// Enhances
correctFloorLink(floor);
alinkEdit(floor);
addQuoteBtn(floor);
addQueryBtn(floor);
addRemark(floor);
alinktofloor(floor.table);
}
}
// Bookcase page add-on
function pageBookcase() {
// Get auto-recommend config
let arConfig = CONFIG.AutoRecommend.getConfig();
// Get bookcase lists
const bookCaseURL = `https://${location.host}/modules/article/bookcase.php?classid={CID}`;
const content = $('#content');
const selector = $('[name="classlist"]');
const options = selector.children;
// Current bookcase
const curForm = $(content, '#checkform');
const curClassid = Number($('[name="clsssid"]').value);
// Init bookcase config if need
initPreferences();
const bookcases = CONFIG.bookcasePrefs.getConfig().bookcases;
addTopTitle();
decorateForm(curForm, bookcases[curClassid]);
// gowork
laterReads();
showBookcases();
recommendAllGUI();
function recommendAllGUI() {
const block = createWenkuBlock({
type: 'mypage',
parent: '#left',
title: TEXT_GUI_BOOKCASE_ATRCMMD,
items: [
{innerHTML: arConfig.allCount === 0 ? TEXT_GUI_BOOKCASE_RCMMDNW_NOTASK : (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, '.ulitem');
const txtst = $(block, '#arstatus');
const btnAR = $(block, '#autorcmmd');
const btnRN = $(block, '#rcmmdnow');
const txtAR = $(block, 'span');
const checkbox = $CrE('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) {
destroyEvent(e);
arConfig.auto = !arConfig.auto;
setTimeout(function() {checkbox.checked = arConfig.auto;}, 0);
CONFIG.AutoRecommend.saveConfig(arConfig);
alertify.notify(arConfig.auto ? TEXT_ALT_ATRCMMDS_AUTO : TEXT_ALT_ATRCMMDS_NOAUTO);
}
function rcmmdnow() {
if (TASK.AutoRecommend.checkRcmmd() && !confirm(TEXT_GUI_BOOKCASE_RCMMDNW_CONFIRM)) {return false;}
if (arConfig.allCount === 0) {alertify.warning(TEXT_ALT_ATRCMMDS_NOTASK); return false;};
TASK.AutoRecommend.run(true);
}
}
function initPreferences() {
const config = CONFIG.bookcasePrefs.getConfig();
if (config.bookcases.length === 0) {
for (const option of options) {
config.bookcases.push({
classid: Number(option.value),
url: bookCaseURL.replace('{CID}', String(option.value)),
name: option.innerText
});
}
CONFIG.bookcasePrefs.saveConfig(config);
}
}
function addTopTitle() {
// Clone title bar
const checkform = $('#checkform') ? $('#checkform') : $('.'+CLASSNAME_BOOKCASE_FORM);
const oriTitle = $(checkform, 'div.gridtop');
const topTitle = oriTitle.cloneNode(true);
content.insertBefore(topTitle, checkform);
// Hide bookcase selector
const bcSelector = $(topTitle, '[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, '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, '#checkform');
form.parentElement.removeChild(form);
// Find the right place to insert it in
const forms = $All(content, '.'+CLASSNAME_BOOKCASE_FORM);
for (let i = 0; i < forms.length; i++) {
const thisForm = forms[i];
const cid = typeof thisForm.classid === 'number' ? thisForm.classid : curClassid;
if (cid > classid) {
content.insertBefore(form, thisForm);
break;
}
}
if(!form.parentElement) {$('#laterbooks').insertAdjacentElement('beforebegin', 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, '[name="classlist"]');
bcSelector.style.display = 'none';
// Dblclick Change title
const titleBar = bcSelector.parentElement;
titleBar.childNodes[0].nodeValue = name;
titleBar.addEventListener('dblclick', editName);
// Longpress Change title for mobile
let touchTimer;
titleBar.addEventListener('touchstart', () => {touchTimer = setTimeout(editName, 500);});
titleBar.addEventListener('touchmove', () => {clearTimeout(touchTimer);});
titleBar.addEventListener('touchend', () => {clearTimeout(touchTimer);});
titleBar.addEventListener('mousedown', () => {touchTimer = setTimeout(editName, 500);});
titleBar.addEventListener('mouseup', () => {clearTimeout(touchTimer);});
// 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, '#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 = $All(content, '.'+CLASSNAME_BOOKCASE_FORM);
for (const form of forms) {
renameFormSlctr(form);
}
} else {
renameFormSlctr(form);
}
function renameFormSlctr(form) {
const newclassid = $(form, '#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 GUI to edit bookcase name
function editName() {
const nameInput = $CrE('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 ? tipobj.innerHTML = tip : function(){};
const value = nameInput.value.trim();
if (value) {
name = value;
bookcase.name = name;
CONFIG.bookcasePrefs.saveConfig(bookcases);
}
titleBar.childNodes[0].nodeValue = name;
try {titleBar.removeChild(nameInput)} catch (DOMException) {};
renameSelectors(true);
}
function onkeydown(e) {
if (e.keyCode === 13) {
e.preventDefault();
onblur();
}
}
}
// Provide auto-recommend option
function arBtn() {
const table = $(form, 'table');
for (const tr of $All(table, 'tr')) {
$(tr, '.odd') ? decorateRow(tr) : function() {};
$(tr, 'th') ? decorateHeader(tr) : function() {};
$(tr, 'td.foot') ? decorateFooter(tr) : function() {};
}
// Insert auto-recommend option for given row
function decorateRow(tr) {
const eleBookLink = $(tr, 'td:nth-child(2)>a');
const strBookID = eleBookLink.href.match(/aid=(\d+)/)[1];
const strBookName = eleBookLink.innerText;
const newTd = $CrE('td');
const input = $CrE('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 = $All(tr, 'th');
const width = ARR_GUI_BOOKCASE_WIDTH;
const newTh = $CrE('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, 'td.foot');
td.colSpan = ARR_GUI_BOOKCASE_WIDTH.length;
}
// auto-recommend onvaluechange
function onvaluechange(e) {
arConfig = CONFIG.AutoRecommend.getConfig();
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];
CONFIG.AutoRecommend.saveConfig(arConfig);
// alert
alertify.notify(
TEXT_ALT_ATRCMMDS_SAVED
.replaceAll('{B}', strBookName)
.replaceAll('{N}', value)
.replaceAll('{R}', userDetail.vote-arConfig.allCount)
);
if (userDetail && arConfig.allCount > userDetail.vote) {
const alertBox = alertify.warning(
TEXT_ALT_ATRCMMDS_OVERFLOW
.replace('{V}', String(userDetail.vote))
.replace('{C}', String(arConfig.allCount))
);
alertBox.callback = function(isClicked) {
isClicked && refreshMyUserDetail();
}
};
} else {
// invalid input value, alert
alertify.error(TEXT_ALT_ATRCMMDS_INVALID.replaceAll('{N}', value));
}
}
}
}
function laterReads() {
// Container
const container = $CrE('div');
container.id = 'laterbooks';
content.appendChild(container);
// Title div
const titlebar = $CrE('div');
titlebar.classList.add('gridtop');
titlebar.style.display = 'grid';
titlebar.style['grid-template-columns'] = '1fr 1fr 1fr';
container.appendChild(titlebar);
const title = $CrE('span');
title.innerHTML = '稍后再读';
title.style['grid-column'] = '2/3';
titlebar.appendChild(title);
// Sorter select container
const sortContainer = $CrE('span');
sortContainer.style['grid-column'] = '3/4';
sortContainer.style.textAlign = 'right';
titlebar.appendChild(sortContainer);
// Sorter select
const sltsort = $CrE('select');
sltsort.style.width = 'max-content';
sltsort.addEventListener('change', function() {
const config = CONFIG.bookcasePrefs.getConfig();
config.laterbooks.sortby = sltsort.value;
CONFIG.bookcasePrefs.saveConfig(config);
showBooks();
});
sortContainer.appendChild(sltsort);
// Sorter select options
const sorttypes = Object.keys(FUNC_LATERBOOK_SORTERS);
for (const type of sorttypes) {
const sort = FUNC_LATERBOOK_SORTERS[type];
const option = $CrE('option');
option.innerHTML = sort.name;
option.value = type;
sltsort.appendChild(option);
}
sltsort.selectedIndex = sorttypes.indexOf(CONFIG.bookcasePrefs.getConfig().laterbooks.sortby);
// Body table
const body = $CrE('table');
setAttributes(body, {
'class': 'grid',
'width': '100%',
'align': 'center'
});
const tbody = $CrE('tbody');
body.appendChild(tbody);
container.appendChild(body);
// Header & Rows
showBooks();
function showBooks() {
const config = CONFIG.bookcasePrefs.getConfig().laterbooks;
clearChildnodes(body);
// headers
const headtr = $CrE('tr');
headtr.setAttribute('align', 'center');
const headers = [{
name: '名称',
width: '22%'
},{
name: '简介',
width: '60%'
},{
name: '操作',
width: '18%'
}];
for (const head of headers) {
const th = $CrE('th');
th.innerHTML = head.name;
th.style.width = head.width;
headtr.appendChild(th);
}
body.appendChild(headtr);
// Book rows
const books = sortLaterReads(config.books, config.sortby);
for (const book of books) {
makeRow(book);
}
function makeRow(book) {
const config = CONFIG.bookcasePrefs.getConfig().laterbooks;
// row
const row = $CrE('tr');
// cover & name
const tdName = $CrE('td');
tdName.classList.add('odd');
tdName.style.textAlign = 'center';
const clink = $CrE('a');
clink.href = URL_NOVELINDEX.replace('{I}', book.aid);
clink.target = '_blank';
tdName.appendChild(clink);
const cover = $CrE('img');
cover.src = book.cover;
cover.style.width = '100px';
clink.appendChild(cover);
clink.insertAdjacentHTML('beforeend', '');
clink.insertAdjacentText('beforeend', book.name);
row.appendChild(tdName);
// info
const tdInfo = $CrE('td');
tdInfo.classList.add('even');
tdInfo.insertAdjacentHTML('afterbegin', '作品Tags:');
for (const tag of book.tags) {
const a = $CrE('a');
a.target = '_blank';
a.href = URL_TAGSEARCH.replace('{TU}', $URL.encode(tag));
a.classList.add(CLASSNAME_BUTTON);
a.innerText = tag + ' ';
tdInfo.appendChild(a);
}
tdInfo.insertAdjacentHTML('beforeend', '内容简介:');
tdInfo.insertAdjacentText('beforeend', book.introduce);
row.appendChild(tdInfo);
// operator
const tdOprt = $CrE('td');
tdOprt.classList.add('odd');
tdOprt.style.textAlign = 'center';
const btnDel = makeBtn();
btnDel.innerHTML = '删除';
btnDel.addEventListener('click', del);
tdOprt.appendChild(btnDel);
const btnAbc = makeBtn('a'); // Abc ==> AddBookCase
btnAbc.innerHTML = '加入书架';
btnAbc.href = URL_ADDBOOKCASE.replace('{A}', book.aid);
btnAbc.target = '_blank';
tdOprt.appendChild(btnAbc);
if (config.sortby === 'sort') {
tdOprt.appendChild($CrE('br'));
const btnUp = makeBtn();
btnUp.innerHTML = '上移';
btnUp.addEventListener('click', function () {
const config = CONFIG.bookcasePrefs.getConfig();
const books = Object.values(config.laterbooks.books);
const cur = books.filter((b) => (b.sort === book.sort));
const previous = books.filter((b) => (b.sort === book.sort-1));
if (cur) {
if (previous.length > 0) {
previous[0].sort++;
cur[0].sort--;
CONFIG.bookcasePrefs.saveConfig(config);
showBooks();
}
} else {
alertify.warning(TEXT_ALT_BOOKCASE_AFTERBOOKS_MISSING);
}
});
tdOprt.appendChild(btnUp);
const btnDown = makeBtn();
btnDown.innerHTML = '下移';
btnDown.addEventListener('click', function () {
const config = CONFIG.bookcasePrefs.getConfig();
const books = Object.values(config.laterbooks.books);
const cur = books.filter((b) => (b.sort === book.sort));
const after = books.filter((b) => (b.sort === book.sort+1));
if (cur) {
if (after.length > 0) {
after[0].sort--;
cur[0].sort++;
CONFIG.bookcasePrefs.saveConfig(config);
showBooks();
}
} else {
alertify.warning(TEXT_ALT_BOOKCASE_AFTERBOOKS_MISSING);
}
});
tdOprt.appendChild(btnDown);
}
row.appendChild(tdOprt);
body.appendChild(row);
function del() {
const config = CONFIG.bookcasePrefs.getConfig();
const books = config.laterbooks.books;
const bk = books[book.aid];
if (!bk) {
body.removeChild(row);
return false;
}
delete config.laterbooks.books[book.aid];
Array.prototype.forEach.call(Object.values(books), (b) => (b.sort > bk.sort && b.sort--));
CONFIG.bookcasePrefs.saveConfig(config);
body.removeChild(row);
}
function makeBtn(tagName='span') {
const btn = $CrE(tagName);
btn.classList.add(CLASSNAME_BUTTON);
btn.style.margin = '0 1em';
return btn;
}
}
}
}
// Set attributes to an element
function setAttributes(elm, attributes) {
for (const [name, attr] of Object.entries(attributes)) {
elm.setAttribute(name, attr);
}
}
}
// Novel ads remover
function removeTopAds() {
const ads = []; $All('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();
//downloader();
function downloader() {
AndAPI.getNovelIndex({
aid: unsafeWindow.article_id,
lang: 0,
callback: indexGot
});
function indexGot(xml) {
const volumes = $All(xml, 'volume');
const vtitles = $All('.vcss');
if (volumes.length !== vtitles.length) {return false;}
for (let i = 0; i < volumes.length; i++) {
const volume = volumes[i];
const vtitle = vtitles[i];
const vname = volume.childNodes[0].nodeValue;
// Title element
const elmTitle = $CrE('span');
elmTitle.innerText = vname;
// Spliter element
const elmSpliter = $CrE('span');
elmSpliter.style.margin = '0 0.5em';
// Download button
const elmDlBtn = $CrE('span');
elmDlBtn.classList.add(CLASSNAME_BUTTON);
elmDlBtn.innerHTML = TEXT_GUI_DOWNLOAD_THISVOLUME;
elmDlBtn.addEventListener('click', function() {
// getAttribute returns string rather than number,
// but downloadVolume accepts both string and number as vid
downloadVolume(volume.getAttribute('vid'), vname, ['utf-8', 'big5'][getLang()]);
});
clearChildnodes(vtitle);
vtitle.appendChild(elmTitle);
vtitle.appendChild(elmSpliter);
vtitle.appendChild(elmDlBtn);
}
}
function downloadVolume(vid, vname, charset='utf-8') {
const url = URL_DOWNLOAD1.replace('{A}', unsafeWindow.article_id).replace('{V}', vid).replace('{C}', charset);
downloadFile({
url: url,
name: TEXT_GUI_SDOWNLOAD_FILENAME
.replace('{NovelName}', $('#title').innerText)
.replace('{VolumeName}', vname)
.replace('{Extension}', 'txt')
});
}
}
}
// Novel page add-on
function pageNovel() {
const CSM = new ConfigSetManager();
CSM.install();
const pageResource = {elements: {}, infos: {}, download: {}};
collectPageResources();
// Remove ads
removeTopAds();
// Side-Panel buttons
sideButtons();
// Provide download GUI
downloadGUI();
// Prevent URL.revokeObjectURL in script 轻小说文库下载
revokeObjectURLHOOK();
// Font changer
fontChanger();
// More font-sizes
moreFontSizes();
// Fill content if need
fillContent();
// Beautifier page
beautifier();
function collectPageResources() {
collectElements();
collectInfos();
initDownload();
function collectElements() {
const elements = pageResource.elements;
elements.title = $('#title');
elements.images = $All('.imagecontent');
elements.rightButtonDiv = $('#linkright');
elements.rightNodes = elements.rightButtonDiv.childNodes;
elements.rightBlank = elements.rightNodes[elements.rightNodes.length-1];
elements.content = $('#content');
elements.contentmain = $('#contentmain');
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);
}
}
// Side-Panel buttons
function sideButtons() {
// Download
SPanel.add({
faicon: 'fa-solid fa-download',
tip: TEXT_GUI_DOWNLOAD_THISCHAPTER,
onclick: dlNovel
});
// Next page
SPanel.add({
faicon: 'fa-solid fa-angle-right',
tip: '下一页',
onclick: (e) => {$('#foottext>a:nth-child(4)').click();}
});
// Previous page
SPanel.add({
faicon: 'fa-solid fa-angle-left',
tip: '上一页',
onclick: (e) => {$('#foottext>a:nth-child(3)').click();}
});
}
// Provide download GUI
function downloadGUI() {
const elements = pageResource.elements;
const infos = pageResource.infos;
// Create donwload button
const dlBtn = elements.downloadBtn = $CrE('span');
dlBtn.classList.add(CLASSNAME_BUTTON);
dlBtn.addEventListener('click', dlNovel);
dlBtn.innerText = TEXT_GUI_DOWNLOAD_THISCHAPTER;
// 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);
}
// Page beautifier
function beautifier() {
CONFIG.BeautifierCfg.getConfig().novel.beautiful && beautiful();
function beautiful() {
const config = CONFIG.BeautifierCfg.getConfig();
const usedHeight = getRestHeight();
addStyle(CSS_NOVEL
.replaceAll('{BGI}', config.backgroundImage)
.replaceAll('{S}', config.textScale)
.replaceAll('{H}', usedHeight), 'beautifier'
);
unsafeWindow.stopScroll = beautiful_stopScroll;
document.onmousedown = beautiful_stopScroll;
unsafeWindow.scrolling = beautiful_scrolling;
// Get rest height without #contentmain
function getRestHeight() {
let usedHeight = 0;
['adv1', 'adtop', 'headlink', 'footlink', 'adbottom'].forEach((id) => {
const node = $('#'+id);
if (node instanceof Element && node.id !== 'contentmain') {
const cs = getComputedStyle(node);
['height', 'marginTop', 'marginBottom', 'paddingTop', 'paddingBottom', 'borderTop', 'borderBottom'].forEach((style) => {
const reg = cs[style].match(/([\.\d]+)px/);
reg && (usedHeight += Number(reg[1]));
});
};
});
usedHeight = usedHeight.toString() + 'px';
return usedHeight;
}
// Mouse dblclick scroll with beautifier applied
function beautiful_scrolling() {
let contentmain = pageResource.elements.contentmain;
let currentpos = contentmain.scrollTop || 0;
contentmain.scrollTo(0, ++currentpos);
let nowpos = contentmain.scrollTop || 0;
pageResource.elements.content.style.userSelect = 'none';
currentpos != nowpos && beautiful_stopScroll();
}
function beautiful_stopScroll() {
pageResource.elements.content.style.userSelect = '';
unsafeWindow.clearInterval(timer);
}
}
}
// Provide font changer
function fontChanger() {
// Button
const bcolor = $('#bcolor');
const txtfont = $CrE('select');
txtfont.id = 'txtfont';
txtfont.addEventListener('change', applyFont);
bcolor.insertAdjacentElement('afterend', txtfont);
bcolor.insertAdjacentText('afterend', '\t\t\t 字体选择');
// Provided fonts
const FONTS = [{"name":"默认","value":"unset"}, {"name":"微软雅黑","value":"Microsoft YaHei"},{"name":"黑体","value":"SimHei"},{"name":"微软正黑体","value":"Microsoft JhengHei"},{"name":"宋体","value":"SimSun"},{"name":"仿宋","value":"FangSong"},{"name":"新宋体","value":"NSimSun"},{"name":"细明体","value":"MingLiU"},{"name":"新细明体","value":"PMingLiU"},{"name":"楷体","value":"KaiTi"},{"name":"标楷体","value":"DFKai-SB"}]
for (const font of FONTS) {
const option = $CrE('option');
option.innerText = font.name;
option.value = font.value;
txtfont.appendChild(option);
}
// Function
CSM.ConfigSets.txtfont = {
save: () => (setCookies('txtfont', txtfont[txtfont.selectedIndex].value)),
load: () => {
const tmpstr = ReadCookies("txtfont");
if (tmpstr != "") {
for (let i = 0; i < txtfont.length; i++) {
if (txtfont.options[i].value == tmpstr) {
txtfont.selectedIndex = i;
break;
}
}
}
applyFont();
}
};
// Load saved font
CSM.ConfigSets.txtfont.load();
function applyFont() {
$('#content').style['font-family'] = txtfont[txtfont.selectedIndex].value;
}
}
// Provide more font-sizes
function moreFontSizes() {
const select = $('#fonttype');
const savebtn = $('#saveset');
const sizes = [
{
name: '更小',
size: '10px'
},
{
name: '更大',
size: '28px'
},
{
name: '很大',
size: '32px'
},
{
name: '超大',
size: '36px'
},
{
name: '极大',
size: '40px'
},
{
name: '过大',
size: '44px'
},
];
for (const size of sizes) {
const option = $CrE('option');
option.innerHTML = size.name;
option.value = size.size;
// Insert with sorting
for (const opt of select.children) {
const sizeNum1 = getSizeNum(opt.value);
const sizeNum2 = getSizeNum(option.value);
if (isNaN(sizeNum1) || isNaN(sizeNum2)) {continue;} // Code shouldn't be here in normal cases
if (sizeNum1 > sizeNum2) {
select.insertBefore(option, opt);
break;
}
}
option.parentElement !== select && select.appendChild(option);
}
// Load saved fonttype
CSM.ConfigSets.fonttype.load();
function getSizeNum(size) {
return Number(size.match(/(\d+)px/)[1]);
}
}
// Provide content using AndroidAPI
function fillContent() {
// Check whether needs filling
if ($('#contentmain>span')) {
if ($('#contentmain>span').innerText.trim() !== 'null') {
return false;
}
} else {return false;}
// prepare
const content = pageResource.elements.content;
content.innerHTML = TEXT_GUI_NOVEL_FILLING;
const charset = (function() {
const match = document.cookie.match(/(; *)?jieqiUserCharset=(.+?)( *;|$)/);
return match && match[2] && match[2].toLowerCase() === 'big5' ? 1 : 0;
}) ();
// Get content xml
AndAPI.getNovelContent({
aid: unsafeWindow.article_id,
cid: unsafeWindow.chapter_id,
lang: charset,
callback: function(text) {
const imgModel = '
';
// Trim whitespaces
text = text.trim();
// Get images like http://pic.wenku8.com/pictures/0/716/24406/11588.jpg
const imgUrls = text.match(/[^<>]+?/g) || [];
// Parse for every image url
let html = '';
for (const url of imgUrls) {
const index = text.indexOf(url);
const src = htmlEncode(url.match(/([^<>]+?)/)[1]);
html += htmlEncode(text.substring(0, index)).replaceAll('\r\n', '\n').replaceAll('\r', '\n').replaceAll('\n', '');
html += imgModel.replaceAll('{U}', src);
text = text.substring(index + url.length);
}
html += htmlEncode(text);
// Set content
pageResource.elements.content.innerHTML = html;
// Reset pageResource-image if need
pageResource.infos.isImagePage = imgUrls.length > 0;
pageResource.elements.images = $All('.imagecontent');
pageResource.download.all = pageResource.elements.images.length;
}
})
return true;
}
// Download button onclick
function dlNovel() {
pageResource.infos.isImagePage ? dlNovelImages() : dlNovelText();
}
// Download Images
function dlNovelImages() {
const elements = pageResource.elements;
const infos = pageResource.infos;
const download = pageResource.download;
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 = $CrE('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() {
throw new Error('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;
}
}
}
// Download Text
function dlNovelText() {
const infos = pageResource.infos;
const name = infos.title + '.txt';
const text = infos.content.replaceAll(/[\r\n]+/g, '\r\n');
downloadText(text, name);
}
// Image format changing function
// image: 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 = $CrE('canvas');
cvs.width = image.width;
cvs.height = image.height;
const ctx = cvs.getContext('2d');
ctx.drawImage(image, 0, 0);
return cvs.toDataURL(format);
}
function ConfigSetManager() {
const CSM = this;
/*const setCookies = unsafeWindow.setCookies,
ReadCookies = unsafeWindow.ReadCookies,
bcolor = unsafeWindow.bcolor,
txtcolor = unsafeWindow.txtcolor,
fonttype = unsafeWindow.fonttype,
scrollspeed = unsafeWindow.scrollspeed,
setSpeed = unsafeWindow.setSpeed,
contentobj = unsafeWindow.contentobj;*/
CSM.ConfigSets = {
'bcolor': {
save: () => (setCookies("bcolor", bcolor.options[bcolor.selectedIndex].value)),
load: () => {
const tmpstr = ReadCookies("bcolor");
bcolor.selectedIndex = 0;
if (tmpstr != "") {
for (let i = 0; i < bcolor.length; i++) {
if (bcolor.options[i].value == tmpstr) {
bcolor.selectedIndex = i;
break;
}
}
}
document.bgColor = bcolor.options[bcolor.selectedIndex].value;
}
},
'txtcolor': {
save: () => (setCookies("txtcolor", txtcolor.options[txtcolor.selectedIndex].value)),
load: () => {
const tmpstr = ReadCookies("txtcolor");
txtcolor.selectedIndex = 0;
if (tmpstr != "") {
for (let i = 0; i < txtcolor.length; i++) {
if (txtcolor.options[i].value == tmpstr) {
txtcolor.selectedIndex = i;
break;
}
}
}
$('#content').style.color = txtcolor.options[txtcolor.selectedIndex].value;
}
},
'fonttype': {
save: () => (setCookies("fonttype", fonttype.options[fonttype.selectedIndex].value)),
load: () => {
const tmpstr = ReadCookies("fonttype");
fonttype.selectedIndex = 2;
if (tmpstr != "") {
for (let i = 0; i < fonttype.length; i++) {
if (fonttype.options[i].value == tmpstr) {
fonttype.selectedIndex = i;
break;
}
}
}
$('#content').style.fontSize = fonttype.options[fonttype.selectedIndex].value;
}
},
'scrollspeed': {
save: () => (setCookies("scrollspeed", scrollspeed.value)),
load: () => {
const tmpstr = ReadCookies("scrollspeed");
if (tmpstr == '') {tmpstr = 5;}
scrollspeed.value = tmpstr;
setSpeed();
}
}
};
CSM.saveSet = function() {
for (const [name, set] of Object.entries(CSM.ConfigSets)) {
set.save();
}
};
CSM.loadSet = function() {
for (const [name, set] of Object.entries(CSM.ConfigSets)) {
set.load();
}
};
CSM.install = function() {
Object.defineProperty(unsafeWindow, 'saveSet', {
configurable: false,
enumerable: true,
value: CSM.saveSet,
writable: false
});
Object.defineProperty(unsafeWindow, 'loadSet', {
configurable: false,
enumerable: true,
value: CSM.loadSet,
writable: false
});
};
}
}
// Search form add-on
function formSearch() {
const searchForm = $('form[name="articlesearch"]');
if (!searchForm) {return false;};
const typeSelect = $(searchForm, '#searchtype');
const searchText = $(searchForm, '#searchkey');
const searchSbmt = $(searchForm, 'input[class="button"][type="submit"]');
let optionTags;
provideTagOption();
onsubmitHOOK();
function provideTagOption() {
optionTags = $CrE('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() {
}
// Mylink page add-on
function pageMylink() {
// Get elements
const main = $('#content');
const tbllink = $('#content>table');
linkEnhance();
function fixEdit(link) {
const aedit = link.aedit;
aedit.setAttribute('onclick', "editlink({ULID},'{NAME}','{HREF}','{INFO}')".replace('{ULID}', deal(link.ulid)).replace('{NAME}', deal(link.name)).replace('{HREF}', deal(link.href)).replace('{INFO}', deal(link.info)));
function deal(str) {
return str.replaceAll("'", "\\'");
}
}
function linkEnhance() {
const links = getAllLinks();
for (const link of links) {
fixEdit(link);
}
}
function getAllLinks() {
const links = [];
const trs = $All(tbllink, 'tbody>tr+tr');
for (const tr of trs) {
const link = {};
// All