/* eslint-disable no-multi-spaces */ // ==UserScript== // @name 轻小说文库+ // @namespace Wenku8+ // @version 1.1.9 // @description TXT分卷批量下载,版权限制小说TXT简繁全本下载,书名/作者名双击复制,Ctrl+Enter快捷键发表书评,单章节下载,小说JPEG插图下载,下载线路点击切换,书评帖子全贴下载保存,书评帖子回复功能增强,书架功能增强,修复文库插入链接和图片无法识别https的自身bug,轻小说标签搜索(Feature Preview),用户书评搜索 // @author PY-DNG // @match http*://www.wenku8.net/* // @connect wenku8.com // @connect wenku8.net // @grant GM_download // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_openInTab // @grant GM_info // @require https://greasyfork.org/scripts/427726-gbk-url-js/code/GBK_URLjs.js?version=939169 // @noframes // @downloadURL none // ==/UserScript== /* 需求记录 [容易(优先级高) ➡️ 困难(优先级低)] ** [已完成]{BK}书评页提供用户书评搜索 ** {BK}图片大小(最大)限制 ** {jack158}[部分完成]全卷/分卷下载:文件重命名为书名,而不是书号 ** · [已完成分卷&Book页]添加单文件下载重命名 ** {热忱}[已完成]修复https引用问题 ** [beta已完成]支持preview版tag搜索 ** 改进旧代码: ** · 每个page-addon内部要按照功能分模块,执行功能靠调用模块,不能直接写功能代码 ** · 共性模块要写进脚本全局作用域,可以的话写成构造函数 ** {热忱}书评:@某人时通知他 ** {热忱}提供带文字和插图的epub整合下载 ** {BK}[待完善]书评:草稿箱功能 */ (function() { 'use strict'; // CONSTS const NUMBER_MAX_XHR = 10; const NUMBER_LOGSUCCESS_AFTER = NUMBER_MAX_XHR * 2; const NUMBER_ELEMENT_LOADING_WAIT_INTERVAL = 500; const KEY_COMMENT_DRAFTS = 'comment-drafts'; const KEY_DRAFT_VERSION = 'version'; const VALUE_DRAFT_VERSION = '0.1'; const KEY_BOOKCASES = 'book-cases'; const KEY_BOOKCASE_VERSION = 'version'; const VALUE_BOOKCASE_VERSION = '0.1'; const VALUE_STR_NULL = 'null'; const URL_REVIEWSEARCH = 'https://www.wenku8.net/modules/article/reviewslist.php?keyword={K}'; const URL_USERINFO = 'https://www.wenku8.net/userinfo.php?id={K}'; const URL_DOWNLOAD1 = 'http://dl.wenku8.com/packtxt.php?aid={A}&vid={V}&charset={C}'; const URL_DOWNLOAD2 = 'http://dl2.wenku8.com/packtxt.php?aid={A}&vid={V}&charset={C}'; const URL_DOWNLOAD3 = 'http://dl3.wenku8.com/packtxt.php?aid={A}&vid={V}&charset={C}'; const URL_TAGSEARCH = 'https://www.wenku8.net/modules/article/tags.php?t={TU}'; const CLASSNAME_BUTTON = 'plusbtn'; const CLASSNAME_TEXT = 'plustext'; const CLASSNAME_BOOKCASE_FORM = 'bcform'; const HTML_BOOK_COPY = '[复制]'.replace('{C}', CLASSNAME_BUTTON); const HTML_BOOK_META = '{K}:{V}[复制]'.replace('{C}', CLASSNAME_BUTTON); const HTML_BOOK_TAG = '{TN}'.replace('{C}', CLASSNAME_BUTTON).replace('{U}', URL_TAGSEARCH); const HTML_DOWNLOAD_CONTENER = '
\n
\n《{BOOKNAME}》小说TXT简繁全本下载\n
\n
'; const HTML_DOWNLOAD_LINKS = '
《{ORIBOOKNAME}》小说TXT全本下载
G版原始下载
G版自动重命名
U版原始下载
U版自动重命名
繁体原始下载
繁体自动重命名
'.replaceAll('{C}', CLASSNAME_BUTTON); const HTML_DOWNLOAD_BOARD = '[轻小说文库+] 为您提供《{ORIBOOKNAME}》的TXT简繁全本下载!
由此产生的一切法律及其他问题均由脚本用户承担
—— PY-DNG
'.replace('{C}', CLASSNAME_TEXT); const CSS_DOWNLOAD = '.even {display: grid; grid-template-columns: repeat(3, 1fr); text-align: center;} .dlink {text-align: center;}'; const CSS_COLOR_BTN_NORMAL = 'rgb(0, 160, 0)', CSS_COLOR_BTN_HOVER = 'rgb(0, 100, 0)'; const CSS_COMMON = '.{CT} {color: rgb(30, 100, 220) !important;} .{CB} {color: rgb(0, 160, 0) !important; cursor: pointer !important;} .{CB}:hover {color: rgb(0, 100, 0) !important;} .{CB}:focus {color: rgb(0, 100, 0) !important;}'.replaceAll('{CB}', CLASSNAME_BUTTON).replaceAll('{CT}', CLASSNAME_TEXT); const TEXT_TIP_COPY = '点击复制'; const TEXT_TIP_COPIED = '已复制'; const TEXT_TIP_SERVERCHANGE = '点击切换线路'; const TEXT_TIP_SEARCH_OPTION_TAG = '有关标签搜索

未完善-开发中…
官方尚未正式开放此功能
功能预览由[轻小说文库+]提供'; const TEXT_GUI_DOWNLOAD_IMAGE = '下载图片'; const TEXT_GUI_DOWNLOAD_TEXT = '下载本章'; const TEXT_GUI_DOWNLOAD_REVIEW = '[下载本帖(共A页)]'; const TEXT_GUI_DOWNLOADING_REVIEW = '[下载中...(C/A)]'; const TEXT_GUI_DOWNLOADFINISH_REVIEW = '[下载完毕]'; const TEXT_GUI_DOWNLOADALL = '下载全部分卷,请点击右边的按钮:'; const TEXT_GUI_WAITING = ' 等待中...'; const TEXT_GUI_DOWNLOADING = ' 下载中...'; const TEXT_GUI_DOWNLOADED = ' (下载完毕)'; const TEXT_GUI_NOTHINGHERE = '-Nothing Here-'; const TEXT_GUI_SDOWNLOAD = '地址三(程序重命名)'; const TEXT_GUI_DOWNLOADING_ALL = '下载中...(C/A)'; const TEXT_GUI_DOWNLOADED_ALL = '下载图片(已完成)'; const TEXT_GUI_AUTOSAVE = '(您输入的内容已保存到书评草稿中)'; const TEXT_GUI_AUTOSAVE_CLEAR = '(草稿为空)'; const TEXT_GUI_AUTOSAVE_RESTORE = '(已从书评草稿中恢复了您上次编辑的内容)'; const TEXT_GUI_BOOKCASE_GETTING = '正在搬运书架...(C/A)'; const TEXT_GUI_BOOKCASE_TOPTITLE = '您的书架可收藏 A 本,已收藏 B 本'; const TEXT_GUI_BOOKCASE_MOVEBOOK = '移动到 [N]'; const TEXT_GUI_BOOKCASE_DBLCLICK = '双击我,给我取一个好听的名字吧~'; const TEXT_GUI_BOOKCASE_WHATNAME = '呜呜呜~会是什么名字呢?'; const TEXT_GUI_SEARCH_OPTION_TAG = '标签(preview)'; const TEXT_GUI_BLOCK_TITLE_DEFULT = '操作区域'; const TEXT_GUI_USER_REVIEWSEARCH = '用户书评'; const TEXT_GUI_USER_USERINFO = '详细资料'; // Emoji smiles (not used in the script yet) const SmList = [{text:"/:O",id:"1",alt:"惊讶"}, {text:"/:~",id:"2",alt:"撇嘴"}, {text:"/:*",id:"3",alt:"色色"}, {text:"/:|",id:"4",alt:"发呆"}, {text:"/8-)",id:"5",alt:"得意"}, {text:"/:LL",id:"6",alt:"流泪"}, {text:"/:$",id:"7",alt:"害羞"}, {text:"/:X",id:"8",alt:"闭嘴"}, {text:"/:Z",id:"9",alt:"睡觉"}, {text:"/:`(",id:"10",alt:"大哭"}, {text:"/:-",id:"11",alt:"尴尬"}, {text:"/:@",id:"12",alt:"发怒"}, {text:"/:P",id:"13",alt:"调皮"}, {text:"/:D",id:"14",alt:"呲牙"}, {text:"/:)",id:"15",alt:"微笑"}, {text:"/:(",id:"16",alt:"难过"}, {text:"/:+",id:"17",alt:"耍酷"}, {text:"/:#",id:"18",alt:"禁言"}, {text:"/:Q",id:"19",alt:"抓狂"}, {text:"/:T",id:"20",alt:"呕吐"}] /* \t ┌┬┐┌─┐┏┳┓┏━┓╭─╮ ├┼┤│┼│┣╋┫┃╋┃│╳│ └┴┘└─┘┗┻┛┗━┛╰─╯ ╲╱╭╮ ╱╲╰╯ */ /* **output format: Review Name.txt** ** 轻小说文库-帖子 [ID: reviewid] ** title ** 保存自: reviewlink ** 保存时间: savetime ** By scriptname Ver. version, author authorname ** ** ────────────────────────────── ** [用户: username userid] ** 用户名: username ** 用户ID: userid ** 加入日期: 1970-01-01 ** 用户链接: userlink ** 最早出现: 1楼 ** ────────────────────────────── ** ... ** ────────────────────────────── ** [#1 2021-04-26 17:53:49] [username userid] ** ────────────────────────────── ** content - line 1 ** content - line 2 ** content - line 3 ** ────────────────────────────── ** ** ────────────────────────────── ** [#2 2021-04-26 19:28:08] [username userid] ** ────────────────────────────── ** content - line 1 ** content - line 2 ** content - line 3 ** ────────────────────────────── ** ** ... ** ** ** [THE END] */ const TEXT_SPLIT_LINE_CHAR = '━'; const TEXT_SPLIT_LINE = TEXT_SPLIT_LINE_CHAR.repeat(20) const TEXT_OUTPUT_REVIEW_HEAD = '轻小说文库-帖子 [ID: {RWID}]\n{RWTT}\n保存自: {RWLK}\n保存时间: {SVTM}\nBy {SCNM} Ver. {VRSN}, author {ATNM}' const TEXT_OUTPUT_REVIEW_USER = '{LNSPLT}\n[用户: {USERNM} {USERID}]\n用户名: {USERNM}\n用户ID: {USERID}\n加入日期: {USERJT}\n用户链接: {USERLK}\n最早出现: {USERFL}楼\n{LNSPLT}' const TEXT_OUTPUT_REVIEW_FLOOR = '{LNSPLT}\n[#{RPNUMB} {RPTIME}] [{USERNM} {USERID}]\n{LNSPLT}\n{RPTEXT}\n{LNSPLT}'; const TEXT_OUTPUT_REVIEW_END = '\n[THE END]'; /** DoLog相关函数改自 Ocrosoft 的 Pixiv Previewer * [GitHub] Ocrosoft: https://github.com/Ocrosoft/ * [GreasyFork] Ocrosoft: https://greasyfork.org/zh-CN/users/63073 * [GreasyFork] Pixiv Previewer: https://greasyfork.org/zh-CN/scripts/30766 * [GitHub] Pixiv Previewer: https://github.com/Ocrosoft/PixivPreviewer **/ let LogLevel = { None: 0, Error: 1, Success: 2, Warning: 3, Info: 4, Elements: 5, }; let g_logCount = 0; let g_logLevel = LogLevel.Success; function DoLog(level = LogLevel.Info, msgOrElement, isElement=false) { if (level <= g_logLevel) { let prefix = '%c'; let param = ''; if (level == LogLevel.Error) { prefix += '[Error]'; param = 'color:#ff0000'; } else if (level == LogLevel.Success) { prefix += '[Success]'; param = 'color:#00aa00'; } else if (level == LogLevel.Warning) { prefix += '[Warning]'; param = 'color:#ffa500'; } else if (level == LogLevel.Info) { prefix += '[Info]'; param = 'color:#888888'; } else if (level == LogLevel.Elements) { prefix += 'Elements'; param = 'color:#000000'; } if (level != LogLevel.Elements && !isElement) { console.log(prefix + msgOrElement, param); } else { console.log(msgOrElement); } if (++g_logCount > 512) { console.clear(); g_logCount = 0; } } } // Common actions const tipready = tipcheck(); addStyle(CSS_COMMON); GMXHRHook(NUMBER_MAX_XHR); // Tags search beta formSearch(); // Get tab url api part const API = window.location.href.replace(/https?:\/\/www\.wenku8\.net\//, '').replace(/\?.*/, '') .replace(/^book\/\d+\.html?/, 'book').replace(/novel\/(\d+\/?)+\.html?$/, 'novel'); if (isAPIPage()) {pageAPI(API); return;}; switch (API) { // Dwonload page case 'modules/article/packshow.php': pageDownload(); break; // ReviewList page case 'modules/article/reviews.php': areaReply(); break; // Review page case 'modules/article/reviewshow.php': areaReply(); pageReview(); break; // Bookcase page case 'modules/article/bookcase.php': pageBookcase(); break; // Tags page case 'modules/article/tags.php': pageTags(); break; case 'userpage.php': pageUser(); break; // Index page case 'index.php': pageIndex(); break; // Book page // Also: https://www.wenku8.net/modules/article/articleinfo.php?id={ID}&charset=gbk case 'book': pageBook(); break; // Novel page case 'novel': pageNovel(); break; // Other pages default: DoLog(LogLevel.Info, API); } // Book page add-on function pageBook() { // Resource const pageResource = { elements: {}, info: {} } collectPageResources(); DoLog(LogLevel.Info, pageResource, true) // Provide meta info copy metaCopy(); // Provide txtfull download for copyright book enableDownload(); // Provide tag search tagOption(); // Ctrl+Enter comment submit areaReply(); // Get page resources function collectPageResources() { collectElements(); collectInfos(); function collectElements() { const elements = pageResource.elements; elements.content = document.querySelector('#content'); elements.bookMain = elements.content.querySelector('div'); elements.header = elements.content.querySelector('div>table'); elements.bookName = elements.header.querySelector('b'); elements.metaContainer = elements.header.querySelector('tr+tr'); elements.metas = elements.metaContainer.querySelectorAll('td'); elements.info = elements.bookMain.querySelector('div+table'); elements.infoText = elements.info.querySelector('td+td'); elements.notice = elements.infoText.querySelectorAll('span.hottext>b'); elements.tags = elements.notice.length > 1 ? elements.notice[0] : null; elements.notice = elements.notice[elements.notice.length-1]; elements.introduce = elements.infoText.querySelectorAll('span'); elements.introduce = elements.introduce[elements.introduce.length-1]; } function collectInfos() { const info = pageResource.info; const elements = pageResource.elements; info.bookName = elements.bookName.innerText; info.BID = Number(location.href.match(/book\/(\d+).htm/)[1]); info.metas = []; elements.metas.forEach(function(meta){this.push(getKeyValue(meta.innerText));}, info.metas); info.notice = elements.notice.innerText; info.tags = elements.tags ? getKeyValue(elements.tags.innerText).VALUE.split(' ') : null; info.introduce = elements.introduce.innerText; info.dlEnabled = elements.content.querySelector('legend>b'); info.dlEnabled = info.dlEnabled ? info.dlEnabled.innerText : false; info.dlEnabled = info.dlEnabled ? (info.dlEnabled.indexOf('TXT') !== -1 && info.dlEnabled.indexOf('UMD') !== -1 && info.dlEnabled.indexOf('JAR') !== -1) : false; } } // Copy meta info function metaCopy() { let tip = TEXT_TIP_COPY; for (let i = -1; i < pageResource.elements.metas.length; i++) { const meta = i !== -1 ? pageResource.elements.metas[i] : pageResource.elements.bookName; const info = i !== -1 ? pageResource.info.metas[i] : pageResource.info.bookName; const value = i !== -1 ? info.VALUE : info; meta.innerHTML += HTML_BOOK_COPY; const copyBtn = meta.querySelector('.'+CLASSNAME_BUTTON); copyBtn.addEventListener('click', function() { copyText(value); showTip(TEXT_TIP_COPIED); }); if (tipready) { copyBtn.addEventListener('mouseover', function() {showTip(TEXT_TIP_COPY);}) copyBtn.addEventListener('mouseout' , tiphide); } else { copyBtn.title = TEXT_TIP_COPY; } } function showTip(text) { tip = text; tipshow(tip); } } // Download copyright book function enableDownload() { if (pageResource.info.dlEnabled) {return false;}; let div = document.createElement('div'); pageResource.elements.bookMain.appendChild(div); div.outerHTML = HTML_DOWNLOAD_LINKS .replaceAll('{ORIBOOKNAME}', pageResource.info.bookName) .replaceAll('{BOOKID}', String(pageResource.info.BID)) .replaceAll('{BOOKNAME}', encodeURIComponent(pageResource.info.bookName)); div = document.querySelector('#txtfull'); pageResource.elements.txtfull = div; pageResource.elements.notice.innerHTML = HTML_DOWNLOAD_BOARD .replaceAll('{ORIBOOKNAME}', pageResource.info.bookName); } // Tag Search function tagOption() { const tagsEle = pageResource.elements.tags; const tags = pageResource.info.tags; if (!tags) {return false;} let html = getKeyValue(tagsEle.innerText).KEY + ':'; for (const tag of tags) { html += HTML_BOOK_TAG.replace('{TU}', $URL.encode(tag)).replace('{TN}', tag) + ' '; } tagsEle.innerHTML = html; } } // Reply area add-on function areaReply() { /* ## Release title area ## */ if (document.querySelector('td > input[name="Submit"]') && !document.querySelector('#ptitle')) { const table = document.querySelector('form>table'); const titleText = table.innerHTML.match(//)[0]; const titleHTML = titleText.replace(/^$/, ''); const titleEle = document.createElement('tr'); const caption = table.querySelector('caption'); table.insertBefore(titleEle, caption); titleEle.outerHTML = titleHTML; } const commentArea = document.querySelector('#pcontent'); if (!commentArea) {return false;}; const commentForm = document.querySelector('form[action^="https://www.wenku8.net/modules/article/review"]'); const commentSbmt = document.querySelector('td > input[name="Submit"]'); const commenttitl = document.querySelector('#ptitle'); const commentbttm = commentSbmt.parentElement; /* ## Ctrl+Enter comment submit ## */ if (commentSbmt) { commentSbmt.value = '发表书评(Ctrl+Enter)'; commentSbmt.style.padding = '0.3em 0.4em 0.3em 0.4em'; commentSbmt.style.height= 'auto'; commentArea.addEventListener('keydown', hotkeyReply); commenttitl.addEventListener('keydown', hotkeyReply); } /* ## Enable https protocol for inserted url ## */ fixHTTPS(); /* ## Comment auto-save ## */ // GUI const asTip = document.createElement('span'); commentbttm.appendChild(asTip); // Review-Page: Same rid, same savekey - 'rid123456' // Book-Page & Book-Review-List-Page: Same bookid, same savekey - 'bid1234' let commentData = { rid : getUrlArgv('rid', Number), aid : getUrlArgv('aid', Number), bid : location.href.match(/\/book\/(\d+).htm/) ? Number(location.href.match(/\/book\/(\d+).htm/)[1]) : 0, page : getUrlArgv('page', Number, 1) } commentData.key = commentData.rid ? 'rid' + String(commentData.rid) : 'bid' + String(commentData.bid); restoreDraft(); const events = ['focus', 'blur', 'mousedown', 'keydown', 'keyup']; const eventEles = [commentArea, commenttitl]; for (const eventEle of eventEles) { for (const event of events) { eventEle.addEventListener(event, saveDraft); } } function saveDraft() { const content = commentArea.value; const title = commenttitl.value; if (!content && !title) { clearDraft(); return; } else if (commentData.content === content && commentData.title === title) { return; } commentData.content = content; commentData.title = title; const allCData = GM_getValue(KEY_COMMENT_DRAFTS, {}); allCData[commentData.key] = commentData; allCData[KEY_DRAFT_VERSION] = VALUE_DRAFT_VERSION; GM_setValue(KEY_COMMENT_DRAFTS, allCData); asTip.innerHTML = TEXT_GUI_AUTOSAVE; } function restoreDraft() { const allCData = GM_getValue(KEY_COMMENT_DRAFTS, {}); if (!allCData[commentData.key]) {return false;}; commentData = allCData[commentData.key]; commenttitl.value = commentData.title; commentArea.value = commentData.content; asTip.innerHTML = TEXT_GUI_AUTOSAVE_RESTORE; return true; } function clearDraft() { const allCData = GM_getValue(KEY_COMMENT_DRAFTS, {}); if (!allCData[commentData.key]) {return false;}; allCData[commentData.key] = undefined; GM_setValue(KEY_COMMENT_DRAFTS, allCData); asTip.innerHTML = TEXT_GUI_AUTOSAVE_CLEAR; return true; } function hotkeyReply() { let keycode = event.keyCode; if (keycode === 13 && event.ctrlKey && !event.altKey) { // Do not submit directly like this; we need to submit with onsubmit executed //commentForm.submit(); commentSbmt.click(); } } function fixHTTPS() { if (typeof(UBBEditor) === 'undefined') { DoLog(LogLevel.Info, 'fixHTTPS: UBBEditor not loaded, waiting...'); setTimeout(fixHTTPS, NUMBER_ELEMENT_LOADING_WAIT_INTERVAL); return false; } const eid = 'pcontent'; const menuItemInsertUrl = commentForm.querySelector('#menuItemInsertUrl'); const menuItemInsertImage = commentForm.querySelector('#menuItemInsertImage'); // Wait until menuItemInsertUrl and menuItemInsertImage is loaded if (!menuItemInsertUrl || !menuItemInsertImage) { DoLog(LogLevel.Info, 'fixHTTPS: element not loaded, waiting...'); setTimeout(fixHTTPS, NUMBER_ELEMENT_LOADING_WAIT_INTERVAL); return false; } // Wait until original onclick function is set if (!menuItemInsertUrl.onclick || !menuItemInsertImage.onclick) { DoLog(LogLevel.Info, 'fixHTTPS: defult onclick not loaded, waiting...'); setTimeout(fixHTTPS, NUMBER_ELEMENT_LOADING_WAIT_INTERVAL); return false; } menuItemInsertUrl.onclick = function () { var url = prompt("请输入超链接地址", "http://"); if (url != null && url.indexOf("http://") < 0 && url.indexOf("https://") < 0) { alert("请输入完整的超链接地址!"); return; } if (url != null) { if ((document.selection && document.selection.type == "Text") || (window.getSelection && document.getElementById(eid).selectionStart > -1 && document.getElementById(eid).selectionEnd > document.getElementById(eid).selectionStart)) {UBBEditor.InsertTag(eid, "url", url,'');} else {UBBEditor.InsertTag(eid, "url", url, url);} } }; menuItemInsertImage.onclick = function () { var imgurl = prompt("请输入图片路径", "http://"); if (imgurl != null && imgurl.indexOf("http://") < 0 && imgurl.indexOf("https://") < 0) { alert("请输入完整的图片路径!"); return; } if (imgurl != null) { UBBEditor.InsertTag(eid, "img", "", imgurl); } }; return true; } function submitHook() { const onsubmit = commentForm.onsubmit; commentForm.onsubmit = onsubmitForm; function onsubmitForm(e) { clearDraft(); return onsubmit ? onsubmit() : function() {return true;}; } } } // Review page add-on function pageReview() { // ## Save whole post ## // GUI const pageCountText = document.querySelector('#pagelink>.last').href.match(/page=(\d+)/)[1]; const main = document.querySelector('#content'); const headBars = main.querySelectorAll('tr>td[align]'); headBars[0].width = '80%'; headBars[1].width = '20%'; const saveBtn = document.createElement('span'); saveBtn.innerText = TEXT_GUI_DOWNLOAD_REVIEW.replaceAll('A', pageCountText); saveBtn.classList.add(CLASSNAME_BUTTON); saveBtn.addEventListener('click', downloadWholePost); headBars[1].appendChild(saveBtn); addQuoteBtns(); addQueryBtns(); function addQuoteBtns() { // Get content textarea const pcontent = document.querySelector('#pcontent'); const form = document.querySelector('form[action^="https://www.wenku8.net/modules/article/review"]'); // Get floor elements const avatars = main.querySelectorAll('table div img.avatar'); for (const avatar of avatars) { // do not insert the button as the first childnode. page saving function uses the first childnode as the time element. const table = avatar.parentElement.parentElement.parentElement.parentElement.parentElement; const numberEle = table.querySelector('td.even div a'); const attr = numberEle.parentElement; const btn = createQuoteBtn(attr); const spliter = document.createTextNode(' | '); attr.insertBefore(spliter, numberEle); attr.insertBefore(btn, spliter); } function createQuoteBtn() { const btn = document.createElement('span'); btn.classList.add(CLASSNAME_BUTTON); btn.addEventListener('click', quoteThisFloor); btn.innerHTML = '引用'; return btn; function quoteThisFloor() { // In DOM Events, keyword points to the Event Element. const numberEle = this.parentElement.querySelector('a[name]'); const numberText = numberEle.innerText; const url = numberEle.href; const contentEle = this.parentElement.parentElement.querySelector('hr+div'); const content = getFloorContent(contentEle); const insertPosition = pcontent.selectionEnd; const text = pcontent.value; const leftText = text.substr(0, insertPosition); const rightText = text.substr(insertPosition); /* ## Create insert value ## */ let insertValue = '[url=U]N[/url] [quote]Q[/quote]'; insertValue = insertValue.replace('U', url).replace('N', numberText).replace('Q', content); // if not at the beginning of a line then insert a whitespace before the link insertValue = ((leftText.length === 0 || /[\r\n]$/.test(leftText)) ? '' : ' ') + insertValue; // if not at the end of a line then insert a whitespace after the link insertValue += (rightText.length === 0 || /^[\r\n]/.test(leftText)) ? '' : ' '; pcontent.value = leftText + insertValue + rightText; const position = insertPosition + (pcontent.value.length - text.length); form.scrollIntoView(); pcontent.focus(); pcontent.setSelectionRange(position, position); } function getFloorContent(contentEle) { const subNodes = contentEle.childNodes; let content = '', subContent = '', size = '', color = ''; for (const node of subNodes) { const type = node.nodeName; switch (type) { case '#text': content += node.data; break; case 'IMG': content += '[img]S[/img]'.replace('S', node.src); break; case 'A': content += '[url=U]T[/url]'.replace('U', node.href).replace('T', node.innerText); break; case 'BR': // no need to add \n, because \n will be preserved in #text nodes //content += '\n'; break; case 'DIV': subContent = getFloorContent(node); if (node.classList.contains('jieqiQuote')) { subContent = '[quote]C[/quote]'.replace('C', subContent); } else if (node.classList.contains('jieqiCode')) { subContent = '[code]C[/code]'.replace('C', subContent); } content += subContent; break; case 'SPAN': case 'B': case 'I': case 'DEL': case 'CODE': case 'PRE': subContent = getFloorContent(node); content += subContent; break; /* case 'SPAN': subContent = getFloorContent(node); size = node.style.fontSize.match(/\d+/) ? node.style.fontSize.match(/\d+/)[0] : ''; color = node.style.color.match(/rgb\((\d+), ?(\d+), ?(\d+)\)/); break; */ } } return content; } } } function addQueryBtns() { // Get floor elements const avatars = main.querySelectorAll('table div img.avatar'); for (const avatar of avatars) { // Get container div const div = avatar.parentElement; // Create buttons const qBtn = document.createElement('a'); // Button for query reviews const iBtn = document.createElement('a'); // Button for query userinfo // Get UID const user = div.querySelector('a'); const UID = Number(user.href.match(/uid=(\d+)/)[1]); // Create text spliter const spliter = document.createTextNode(' | '); // Config buttons qBtn.href = URL_REVIEWSEARCH.replaceAll('{K}', String(UID)); iBtn.href = URL_USERINFO .replaceAll('{K}', String(UID)); qBtn.target = '_blank'; iBtn.target = '_blank'; qBtn.innerText = TEXT_GUI_USER_REVIEWSEARCH; iBtn.innerText = TEXT_GUI_USER_USERINFO; // Append to GUI div.appendChild(document.createElement('br')); div.appendChild(iBtn); div.appendChild(qBtn); div.insertBefore(spliter, qBtn); } } /* // Testing getAllPages(function(data) { const txt = joinTXT(data); DoLog(LogLevel.Success, txt); }); */ // ## Function: Get data from page document or join it into the given data variable ## function getDataFromPage(document, data) { let i; DoLog(LogLevel.Info, document, true); // Get Floors; avatars uses for element locating const main = document.querySelector('#content'); const avatars = main.querySelectorAll('table div img.avatar'); // init data, floors and users if need let floors = {}, users = {}; if (data) { floors = data.floors; users = data.users; } else { data = {}; initData(data, floors, users); } for (i = 0; i < avatars.length; i++) { const floor = newFloor(floors, avatars, i); const elements = getFloorElements(floor); const reply = getFloorReply(floor); const user = getFloorUser(floor); appendFloor(floors, floor); } return data; function initData(data, floors, users) { // data vars data.floors = floors; floors.data = data; data.users = users; users.data = data; // review info data.link = location.href; data.id = getUrlArgv('rid', Number, 0); data.page = getUrlArgv('page', Number, 1); data.title = main.querySelector('th strong').innerText; return data; } function newFloor(floors, avatars, i) { const floor = {}; floor.avatar = avatars[i]; floor.floors = floors; return floor; } function getFloorElements(floor) { const elements = {}; floor.elements = elements; elements.avatar = floor.avatar; elements.table = elements.avatar.parentElement.parentElement.parentElement.parentElement.parentElement; elements.tr = elements.table.querySelector('tr'); elements.tdUser = elements.table.querySelector('td.odd'); elements.tdReply = elements.table.querySelector('td.even'); elements.divUser = elements.tdUser.querySelector('div'); elements.aUser = elements.divUser.querySelector('a'); elements.attr = elements.tdReply.querySelector('div a').parentElement; elements.time = elements.attr.childNodes[0]; elements.number = elements.attr.querySelector('a[name]'); elements.title = elements.tdReply.querySelector('div>strong'); elements.content = elements.tdReply.querySelector('hr+div'); return elements; } function getFloorReply(floor) { const elements = floor.elements; const reply = {}; floor.reply = reply; reply.time = elements.time.nodeValue.match(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/)[0]; reply.number = Number(elements.number.innerText.match(/\d+/)[0]); reply.value = elements.content.innerText; reply.title = elements.title.innerText; return reply; } function getFloorUser(floor) { const elements = floor.elements; const user = {}; floor.user = user; user.id = elements.aUser.href.match(/uid=(\d+)/)[1]; user.name = elements.aUser.innerText; user.avatar = elements.avatar.src; user.link = elements.aUser.href; user.jointime = elements.divUser.innerText.match(/\d{4}-\d{2}-\d{2}/)[0]; const data = floor.floors.data; const users = data.users; if (!users.hasOwnProperty(user.id)) { users[user.id] = user; user.floors = [floor]; } else { const uFloors = users[user.id].floors; uFloors.push(floor); sortUserFloors(uFloors); } return user; } function sortUserFloors(uFloors) { uFloors.sort(function(F1, F2) { return F1.reply.number > F2.reply.number; }) } function appendFloor(floors, floor) { floors[floor.reply.number-1] = floor; } } // ## Function: Get pages and parse each pages to a data, returns data ## // callback(data, gotcount, finished) is called when xhr and parsing completed function getAllPages(callback) { let i, data, gotcount = 0; const ridMatcher = /rid=(\d+)/, pageMatcher = /page=(\d+)/; const lastpageUrl = document.querySelector('#pagelink>.last').href; const rid = Number(lastpageUrl.match(ridMatcher)[1]); const pageCount = Number(lastpageUrl.match(pageMatcher)[1]); const curPageNum = location.href.match(pageMatcher) ? Number(location.href.match(pageMatcher)[1]) : 1; for (i = 1; i <= pageCount; i++) { const url = lastpageUrl.replace(pageMatcher, 'page='+String(i)); getDocument(url, joinPageData, callback); } function joinPageData(pageDocument, callback) { data = getDataFromPage(pageDocument, data); gotcount++; // log const level = gotcount % NUMBER_LOGSUCCESS_AFTER ? LogLevel.Info : LogLevel.Success; DoLog(level, 'got ' + String(gotcount) + ' pages.'); if (gotcount === pageCount) { DoLog(LogLevel.Success, 'All pages xhr and parsing completed.'); DoLog(LogLevel.Success, data, true); } // callback if (callback) {callback(data, gotcount, gotcount === pageCount);}; } } // Function output function joinTXT(data, noSpliter=true) { const floors = data.floors; const users = data.users; // HEAD META DATA const saveTime = getTime(); const head = TEXT_OUTPUT_REVIEW_HEAD .replaceAll('{RWID}', data.id).replaceAll('{RWTT}', data.title).replaceAll('{RWLK}', data.link) .replaceAll('{SVTM}', saveTime).replaceAll('{SCNM}', GM_info.script.name) .replaceAll('{VRSN}', GM_info.script.version).replaceAll('{ATNM}', GM_info.script.author); // join userinfos let userText = ''; for (const [pname, user] of Object.entries(users)) { if (!isNumeric(pname)) {continue;}; userText += TEXT_OUTPUT_REVIEW_USER .replaceAll('{LNSPLT}', noSpliter ? '' : TEXT_SPLIT_LINE).replaceAll('{USERNM}', user.name) .replaceAll('{USERID}', user.id).replaceAll('{USERJT}', user.jointime) .replaceAll('{USERLK}', user.link).replaceAll('{USERFL}', user.floors[0].reply.number); userText += '\n'.repeat(2); } // join floors let floorText = ''; for (const [pname, floor] of Object.entries(floors)) { if (!isNumeric(pname)) {continue;}; const avatar = floor.avatar; const elements = floor.elements; const user = floor.user; const reply = floor.reply; floorText += TEXT_OUTPUT_REVIEW_FLOOR .replaceAll('{LNSPLT}', noSpliter ? '' : TEXT_SPLIT_LINE).replaceAll('{RPNUMB}', String(reply.number)) .replaceAll('{RPTIME}', reply.time).replaceAll('{USERNM}', user.name) .replaceAll('{USERID}', user.id).replaceAll('{RPTEXT}', reply.value); floorText += '\n'.repeat(2); } // End const foot = TEXT_OUTPUT_REVIEW_END; // return const txt = head + '\n'.repeat(2) + userText + '\n'.repeat(2) + floorText + '\n'.repeat(2) + foot; return txt; } // ## Function: Download the whole post ## function downloadWholePost() { // Continues only if not working if (downloadWholePost.working) {return;}; downloadWholePost.working = true; // GUI saveBtn.innerText = TEXT_GUI_DOWNLOADING_REVIEW .replaceAll('C', '0').replaceAll('A', pageCountText); // go work! getAllPages(function(data, gotCount, finished) { // GUI saveBtn.innerText = TEXT_GUI_DOWNLOADING_REVIEW .replaceAll('C', String(gotCount)).replaceAll('A', pageCountText); // Stop here if not completed if (!finished) {return;}; // Join text const TXT = joinTXT(data); // Download const blob = new Blob([TXT],{type:"text/plain;charset=utf-8"}); const url = URL.createObjectURL(blob); const name = '文库贴 - ' + String(data.id) + '.txt'; const a = document.createElement('a'); a.href = url; a.download = name; a.click(); // GUI saveBtn.innerText = TEXT_GUI_DOWNLOADFINISH_REVIEW; // Work finish downloadWholePost.working = false; }) } } // Bookcase page add-on function pageBookcase() { // Get bookcase lists const bookCaseURL = 'https://www.wenku8.net/modules/article/bookcase.php?classid={CID}'; const content = document.querySelector('#content'); const selector = document.querySelector('[name="classlist"]'); const options = selector.children; // Current bookcase const curForm = content.querySelector('#checkform'); const curClassid = Number(document.querySelector('[name="clsssid"]').value); const bookcases = readPreferences(); addTopTitle(); decorateForm(curForm, bookcases[curClassid]); // gowork showBookcases(); function readPreferences() { let bookcases = GM_getValue(KEY_BOOKCASES, null); if (!bookcases) { bookcases = initPreferences(); } return bookcases; } function initPreferences() { const lists = []; for (const option of options) { lists.push({ classid: Number(option.value), url: bookCaseURL.replace('{CID}', String(option.value)), name: option.innerText }) } savePreferences(lists); return lists; } function savePreferences(value) { GM_setValue(KEY_BOOKCASES, (value ? value : bookcases)); } function addTopTitle() { // Clone title bar const checkform = document.querySelector('#checkform') ? document.querySelector('#checkform') : document.querySelector('.'+CLASSNAME_BOOKCASE_FORM); const oriTitle = checkform.querySelector('div.gridtop'); const topTitle = oriTitle.cloneNode(true); content.insertBefore(topTitle, checkform); // Hide bookcase selector const bcSelector = topTitle.querySelector('[name="classlist"]'); bcSelector.style.display = 'none'; // Write title text const textNode = topTitle.childNodes[0]; const numMatch = textNode.nodeValue.match(/\d+/g); const text = TEXT_GUI_BOOKCASE_TOPTITLE.replace('A', numMatch[0]).replace('B', numMatch[1]); textNode.nodeValue = text; } function showBookcases() { // GUI const topTitle = content.querySelector('script+div.gridtop'); const textNode = topTitle.childNodes[0]; const oriTitleText = textNode.nodeValue; const allCount = bookcases.length; let finished = 1; textNode.nodeValue = TEXT_GUI_BOOKCASE_GETTING.replace('C', String(finished)).replace('A', String(allCount)); // Get all bookcase pages for (const bookcase of bookcases) { if (bookcase.classid === curClassid) {continue;}; getDocument(bookcase.url, appendBookcase, [bookcase]); } function appendBookcase(mDOM, bookcase) { const classid = bookcase.classid; // Get bookcase form and modify it const form = mDOM.querySelector('#checkform'); form.parentElement.removeChild(form); // Find the right place to insert it in const forms = content.querySelectorAll('.'+CLASSNAME_BOOKCASE_FORM); for (let i = 0; i < forms.length; i++) { const thisForm = forms[i]; const cid = thisForm.classid ? thisForm.classid : curClassid; if (cid > classid) { content.insertBefore(form, thisForm); break; } } if(!form.parentElement) {content.appendChild(form);}; // Decorate decorateForm(form, bookcase); // finished increase finished++; textNode.nodeValue = finished < allCount ? TEXT_GUI_BOOKCASE_GETTING.replace('C', String(finished)).replace('A', String(allCount)) : oriTitleText; } } function decorateForm(form, bookcase) { const classid = bookcase.classid; let name = bookcase.name; // Modify properties form.classList.add(CLASSNAME_BOOKCASE_FORM); form.id += String(classid); form.classid = classid; form.onsubmit = my_check_confirm; // Hide bookcase selector const bcSelector = form.querySelector('[name="classlist"]'); bcSelector.style.display = 'none'; // Change title const titleBar = bcSelector.parentElement; titleBar.childNodes[0].nodeValue = name; titleBar.addEventListener('dblclick', editName); // Show tips let tip = TEXT_GUI_BOOKCASE_DBLCLICK; if (tipready) { // tipshow and tiphide is coded inside wenku8 itself, its function is to show a text tip besides the mouse titleBar.addEventListener('mouseover', function() {tipshow(tip);}); titleBar.addEventListener('mouseout' , tiphide); } else { titleBar.title = tip; } // Change selector names renameSelectors(false); // Replaces the original check_confirm() function function my_check_confirm() { const checkform = this; let checknum = 0; for (let i = 0; i < checkform.elements.length; i++){ if (checkform.elements[i].name == 'checkid[]' && checkform.elements[i].checked == true) checknum++; } if (checknum === 0){ alert('请先选择要操作的书目!'); return false; } const newclassid = checkform.querySelector('#newclassid'); if(newclassid.value == -1){ if (confirm('确实要将选中书目移出书架么?')) {return true;} else {return false;}; } else { return true; } } // Selector name refresh function renameSelectors(renameAll) { if (renameAll) { const forms = content.querySelectorAll('.'+CLASSNAME_BOOKCASE_FORM); for (const form of forms) { renameFormSlctr(form); } } else { renameFormSlctr(form); } function renameFormSlctr(form) { const newclassid = form.querySelector('#newclassid'); const options = newclassid.children; for (let i = 0; i < options.length; i++) { const option = options[i]; const value = Number(option.value); const bc = bookcases[value]; bc ? option.innerText = TEXT_GUI_BOOKCASE_MOVEBOOK.replace('N', bc.name) : function(){}; } } } // Provide GUI to edit bookcase name function editName() { const nameInput = document.createElement('input'); const form = this; tip = TEXT_GUI_BOOKCASE_WHATNAME; tipready ? tipshow(tip) : function(){}; titleBar.childNodes[0].nodeValue = ''; titleBar.appendChild(nameInput); nameInput.value = name; nameInput.addEventListener('blur', onblur); nameInput.focus(); nameInput.setSelectionRange(0, name.length); function onblur() { tip = TEXT_GUI_BOOKCASE_DBLCLICK; tipready ? tipshow(tip) : function(){}; const value = nameInput.value.trim(); if (value) { name = value; bookcase.name = name; savePreferences(); } titleBar.childNodes[0].nodeValue = name; titleBar.removeChild(nameInput); renameSelectors(true); } } } } // Novel page add-on function pageNovel() { const title = document.querySelector('#title').textContent; const isImagePage = title.includes('插图') || title.includes('插圖'); const rightButtonDiv = document.querySelector('#linkright'); const rightButtons = rightButtonDiv.childNodes; let dlCompleted = 0; // number of completed download tasks let dlAllCount = 0; // number of all download tasks let dlAllRunning = false; // whether there is downloadAllImages running // append control buttons let i; let spliter, button = rightButtonDiv.querySelector('a').cloneNode(); for (i = 0; i < rightButtons.length; i++) { if (rightButtons[i].textContent.includes('|')) { spliter = rightButtons[i].cloneNode(); } } // Attributes & Display config let allImages, buttonText; let clickFunc; if (isImagePage) { buttonText = TEXT_GUI_DOWNLOAD_IMAGE; clickFunc = function() {downloadAllImages();}; } else { buttonText = TEXT_GUI_DOWNLOAD_TEXT; clickFunc = function() {downloadText();}; } button.href = 'javascript:void(0);'; button.target = ''; button.innerText = buttonText; button.style.color = '#00BB00'; button.addEventListener('click', clickFunc); rightButtonDiv.insertBefore(spliter, rightButtonDiv.lastChild); rightButtonDiv.insertBefore(button, rightButtonDiv.lastChild); rightButtonDiv.style.width = '500px'; // Prevent URL.revokeObjectURL in script 轻小说文库下载 const Ori_revokeObjectURL = URL.revokeObjectURL; URL.revokeObjectURL = function(arg) { if (typeof(arg) === 'string' && arg.substr(0, 5) === 'blob:') {return false;}; return Ori_revokeObjectURL(arg); } function downloadText() { const contentEle = document.querySelector('#content'); let content = contentEle.innerText//.replaceAll('\n', '\r\n'); if (content.length === 0) { return false; } // Clear spaces content = content.split('\n'); for (let i = 0; i < content.length; i++) { content[i] = content[i].trim(); } content = content.join('\r\n'); // Download const blob = new Blob([content],{type:"text/plain;charset=utf-8"}); const url = URL.createObjectURL(blob); const name = title + '.txt'; const a = document.createElement('a'); a.style.display = 'none'; a.href = url; a.download = name; a.click(); } function downloadAllImages() { if (dlAllRunning) { return false; } allImages = document.querySelectorAll('#content > div.divimage img'); dlAllCount = allImages.length; dlCompleted = 0; dlAllRunning = true; // Display button.innerText = TEXT_GUI_DOWNLOADING_ALL.replace('C', '0').replace('A', String(dlAllCount)); rightButtonDiv.style.width = '550px'; // Download const numLen = String(dlAllCount).length; for (let i = 0; i < dlAllCount; i++) { const imageName = title + '_' + fillNumber(i+1, numLen) + '.jpg'; const url = allImages[i].src; if (allImages[i].src.substr(0,5) === 'blob:') { const image = new Image(); image.onload = function() { saveBlobToFile(toImageFormatURL(image, 1), imageName); dlIncrease(button); } image.src = url; } else { download(url, imageName, button); } } } // File download function function download(url, name, displayElement) { // Check if (!url || !name) { return false; } // xmlHTTPRequest GM_xmlhttpRequest({ method : 'GET', url : url, responseType : 'blob', onloadstart : function() { DoLog(LogLevel.Info, 'Downloading ' + name + ' from ' + url); }, onload : function(request) { // DataURL let objURL = URL.createObjectURL(request.response); // toImageFormatURL const image = new Image(); image.src = objURL; image.onload = function() { //image.style.display = 'none'; //document.body.appendChild(image); const formatURL = toImageFormatURL(image, 1); //document.body.removeChild(image); saveBlobToFile(formatURL, name); dlIncrease(displayElement); }; } }) return true; } // Increase dlCompleted and judge dlAllRunning function dlIncrease(displayElement) { // Task count decrease dlCompleted++; if (dlCompleted === dlAllCount) { dlAllRunning = false; } // Display if (displayElement) { displayElement.innerText = TEXT_GUI_DOWNLOADING_ALL .replace('C', String(dlCompleted)).replace('A', String(dlAllCount)); if (!dlAllRunning) { displayElement.innerText = TEXT_GUI_DOWNLOADED_ALL; rightButtonDiv.style.width = '550px'; } } } // Blob url file saving function function saveBlobToFile(blobURL, name) { // Create const a = document.createElement('a'); a.style.display = 'none'; a.href = blobURL; a.download = name; a.click(); } // Image format changing function function toImageFormatURL(image, format) { if (typeof(format) === 'number') {format = ['image/jpeg', 'image/png', 'image/webp'][format-1]} const cvs = document.createElement('canvas'); cvs.width = image.width; cvs.height = image.height; const ctx = cvs.getContext('2d'); ctx.drawImage(image, 0, 0); return cvs.toDataURL(format); } } // Search form add-on function formSearch() { const searchForm = document.querySelector('form[name="articlesearch"]'); if (!searchForm) {return false;}; const typeSelect = searchForm.querySelector('#searchtype'); const searchText = searchForm.querySelector('#searchkey'); const searchSbmt = searchForm.querySelector('input[class="button"][type="submit"]'); let optionTags; provideTagOption(); onsubmitHOOK(); function provideTagOption() { optionTags = document.createElement('option'); optionTags.value = VALUE_STR_NULL; optionTags.innerText = TEXT_GUI_SEARCH_OPTION_TAG; typeSelect.appendChild(optionTags); if (tipready) { // tipshow and tiphide is coded inside wenku8 itself, its function is to show a text tip besides the mouse typeSelect.addEventListener('mouseover', show); searchSbmt.addEventListener('mouseover', show); typeSelect.addEventListener('mouseout' , tiphide); searchSbmt.addEventListener('mouseout' , tiphide); } else { typeSelect.title = TEXT_TIP_SEARCH_OPTION_TAG; searchSbmt.title = TEXT_TIP_SEARCH_OPTION_TAG; } function show() { optionTags.selected ? tipshow(TEXT_TIP_SEARCH_OPTION_TAG) : function() {}; } } function onsubmitHOOK() { const onsbmt = searchForm.onsubmit; searchForm.onsubmit = function() { if (optionTags.selected) { // DON'T USE window.open()! // Wenku8 has no window.open used in its own scripts, so do not use it in userscript either. // It might cause security problems. //window.open('https://www.wenku8.net/modules/article/tags.php?t=' + $URL.encode(searchText.value)); if (typeof($URL) === 'undefined' ) { $URLError(); return true; } else { GM_openInTab(URL_TAGSEARCH.replace('{TU}', $URL.encode(searchText.value)), { active: true, insert: true, setParent: true, incognito: false }); return false; } } } function $URLError() { DoLog(LogLevel.Error, '$URL(from gbk.js) is not loaded.'); DoLog(LogLevel.Warning, 'Search as plain text instead.'); // Search as plain text instead for (const node of typeSelect.childNodes) { node.selected = (node.tagName === 'OPTION' && node.value === 'articlename') ? true : false; } } } } // Tags page add-on function pageTags() { } // User page add-on function pageUser() { const UID = Number(getUrlArgv('uid')); // Provide review search option reviewButton(); // Review search option function reviewButton() { // clone button and container div const oriContainer = document.querySelectorAll('.blockcontent .userinfo')[0].parentElement; const container = oriContainer.cloneNode(true); const button = container.querySelector('a'); button.innerText = TEXT_GUI_USER_REVIEWSEARCH; button.href = URL_REVIEWSEARCH.replaceAll('{K}', String(UID)); oriContainer.parentElement.appendChild(container); } } // Index page add-on function pageIndex() { } // Download page add-on function pageDownload() { let i; let dlCount = 0; // number of active download tasks let dlAllRunning = false; // whether there is downloadAll running // Get novel info const novelInfo = {}; collectNovelInfo(); const myDlBtns = []; // Donwload GUI downloadGUI(); // Server GUI serverGUI(); /* ******************* Code ******************* */ function collectNovelInfo() { novelInfo.novelName = document.querySelector('html body div.main div#centerm div#content table.grid caption a').innerText; novelInfo.displays = getAllNameEles(); novelInfo.volumeNames = getAllNames(); novelInfo.type = getUrlArgv('type'); novelInfo.ext = novelInfo.type !== 'txtfull' ? novelInfo.type : 'txt'; } // Donwload GUI function downloadGUI() { // Only txt is really separated by volumes if (novelInfo.type !== 'txt') {return false;}; // define vars let i; const tbody = document.querySelector('table>tbody'); const header = tbody.querySelector('th').parentElement; const thead = header.querySelector('th'); // Append new th const newHead = thead.cloneNode(true); newHead.innerText = TEXT_GUI_SDOWNLOAD; thead.width = '40%'; header.appendChild(newHead); // Append new td const trs = tbody.querySelectorAll('tr'); for (i = 1; i < trs.length; i++) { /* i = 1 to trs.length-1: skip header */ const index = i-1; const tr = trs[i]; const newTd = tr.querySelector('td.even').cloneNode(true); const links = newTd.querySelectorAll('a'); for (const a of links) { a.classList.add(CLASSNAME_BUTTON); a.info = { description: 'volume download button', name: novelInfo.volumeNames[index], filename: '{NovelName} {VolumeName}.{Extension}' .replace('{NovelName}', novelInfo.novelName) .replace('{VolumeName}', novelInfo.volumeNames[index]) .replace('{Extension}', novelInfo.ext), index: index, display: novelInfo.displays[index] } a.onclick = downloadOnclick; myDlBtns.push(a); } tr.appendChild(newTd); } // Append new tr, provide batch download const newTr = trs[trs.length-1].cloneNode(true); const newTds = newTr.querySelectorAll('td'); newTds[0].innerText = TEXT_GUI_DOWNLOADALL; //clearChildnodes(newTds[1]); clearChildnodes(newTds[2]); newTds[1].innerHTML = newTds[2].innerHTML = TEXT_GUI_NOTHINGHERE; tbody.insertBefore(newTr, tbody.children[1]); const allBtns = newTds[3].querySelectorAll('a'); for (i = 0; i < allBtns.length; i++) { const a = allBtns[i]; a.href = 'javascript:void(0);'; a.info = { description: 'download all button', index: i } a.onclick = downloadAllOnclick; } } // Download button onclick function downloadOnclick() { const a = this; a.info.display.innerText = a.info.name + TEXT_GUI_WAITING; downloadFile({ url: a.href, name: a.info.filename, onloadstart: function(e) { a.info.display.innerText = a.info.name + TEXT_GUI_DOWNLOADING; }, onload: function(e) { a.info.display.innerText = a.info.name + TEXT_GUI_DOWNLOADED; } }); return false; } // DownloadAll button onclick function downloadAllOnclick() { const a = this; const index = (a.info.index+1)%3; for (let i = 0; i < myDlBtns.length; i++) { if ((i+1)%3 !== index) {continue;}; const btn = myDlBtns[i]; btn.click(); } return false; } // Get all name display elements function getAllNameEles() { return document.querySelectorAll('.grid tbody tr .odd'); } // Get all names function getAllNames() { const all = getAllNameEles() const names = []; for (let i = 0; i < all.length; i++) { names[i] = all[i].innerText; } return names; } // Server GUI function serverGUI() { let servers = document.querySelectorAll('#content>b'); let serverEles = []; for (i = 0; i < servers.length; i++) { if (servers[i].innerText.includes('wenku8.com')) { serverEles.push(servers[i]); } } for (i = 0; i < serverEles.length; i++) { serverEles[i].classList.add(CLASSNAME_BUTTON); serverEles[i].addEventListener('click', function () { changeAllServers(this.innerText); }); if (tipready) { // tipshow and tiphide is coded inside wenku8 itself, its function is to show a text tip besides the mouse serverEles[i].addEventListener('mouseover', function () { tipshow(TEXT_TIP_SERVERCHANGE); }); serverEles[i].addEventListener('mouseout', tiphide); } else { serverEles[i].title = TEXT_TIP_SERVERCHANGE; } } } // Change all server elements function changeAllServers(server) { let i; const allA = document.querySelectorAll('.even a'); for (i = 0; i < allA.length; i++) { changeServer(server, allA[i]); } } // Change server for an element function changeServer(server, element) { if (!element.href) {return false;}; element.href = element.href.replace(/\/\/dl\d?\.wenku8\.com\//g, '//' + server + '/'); } } // isAPIPage page add-on function pageAPI(API) { DoLog(LogLevel.Info, 'This is wenku API page.'); DoLog(LogLevel.Info, 'API is: [' + API + ']'); DoLog(LogLevel.Info, 'There is nothing to do. Quiting...'); } // Check if current page is an wenku API page ('处理成功', '出现错误!') function isAPIPage() { // API page has just one .block div and one close-page button const block = document.querySelectorAll('.block'); const close = document.querySelectorAll('a[href="javascript:window.close()"]'); return block.length === 1 && close.length === 1; } // Check if tipobj is ready, if not, then make it function tipcheck() { DoLog(LogLevel.Info, 'checking tipobj...'); if (typeof(tipobj) === 'object' && tipobj !== null) { DoLog(LogLevel.Info, 'tipobj ready...'); return true; } else { DoLog(LogLevel.Warning, 'tipobj not ready'); if (typeof(tipinit) === 'function') { DoLog(LogLevel.Success, 'tipinit executed'); tipinit(); return true; } else { DoLog(LogLevel.Error, 'tipinit not found'); return false; } } } // Create a left .block operatingArea function createLeftBlock(title=TEXT_GUI_BLOCK_TITLE_DEFULT) { const blockEle = document.querySelector('#left>.block').cloneNode(true); const titleEle = blockEle.querySelector('.blocktitle>.txt'); const cntntEle = blockEle.querySelector('.blockcontent'); titleEle.innerText = title; clearChildnodes(cntntEle); return blockEle; } // Remove all childnodes from an element function clearChildnodes(element) { const cns = [] for (const cn of element.childNodes) { cns.push(cn); } for (const cn of cns) { element.removeChild(cn); } } // GM_XHR HOOK: The number of running GM_XHRs in a time must under maxXHR // Returns the abort function to stop the request anyway(no matter it's still waiting, or requesting) // (If the request is invalid, such as url === '', will return false and will NOT make this request) // If the abort function called on a request that is not running(still waiting or finished), there will be NO onabort event // Requires: function delItem(){...} & function uniqueIDMaker(){...} function GMXHRHook(maxXHR=5) { const GM_XHR = GM_xmlhttpRequest; const getID = uniqueIDMaker(); let todoList = [], ongoingList = []; GM_xmlhttpRequest = safeGMxhr; function safeGMxhr() { // Get an id for this request, arrange a request object for it. const id = getID(); const request = {id: id, args: arguments, aborter: null}; // Deal onload function first dealEndingEvents(request); /* DO NOT DO THIS! KEEP ITS ORIGINAL PROPERTIES! // Stop invalid requests if (!validCheck(request)) { return false; } */ // Judge if we could start the request now or later? todoList.push(request); checkXHR(); return makeAbortFunc(id); // Decrease activeXHRCount while GM_XHR onload; function dealEndingEvents(request) { const e = request.args[0]; // onload event const oriOnload = e.onload; e.onload = function() { reqFinish(request.id); checkXHR(); oriOnload ? oriOnload.apply(null, arguments) : function() {}; } // onerror event const oriOnerror = e.onerror; e.onerror = function() { reqFinish(request.id); checkXHR(); oriOnerror ? oriOnerror.apply(null, arguments) : function() {}; } // ontimeout event const oriOntimeout = e.ontimeout; e.ontimeout = function() { reqFinish(request.id); checkXHR(); oriOntimeout ? oriOntimeout.apply(null, arguments) : function() {}; } // onabort event const oriOnabort = e.onabort; e.onabort = function() { reqFinish(request.id); checkXHR(); oriOnabort ? oriOnabort.apply(null, arguments) : function() {}; } } // Check if the request is invalid function validCheck(request) { const e = request.args[0]; if (!e.url) { return false; } return true; } // Call a XHR from todoList and push the request object to ongoingList if called function checkXHR() { if (ongoingList.length >= maxXHR) {return false;}; if (todoList.length === 0) {return false;}; const req = todoList.shift(); const reqArgs = req.args; const aborter = GM_XHR.apply(null, reqArgs); req.aborter = aborter; ongoingList.push(req); return req; } // Make a function that aborts a certain request function makeAbortFunc(id) { return function() { let i; // Check if the request haven't been called for (i = 0; i < todoList.length; i++) { const req = todoList[i]; if (req.id === id) { // found this request: haven't been called delItem(todoList, i); return true; } } // Check if the request is running now for (i = 0; i < ongoingList.length; i++) { const req = todoList[i]; if (req.id === id) { // found this request: running now req.aborter(); reqFinish(id); checkXHR(); } } // Oh no, this request is already finished... return false; } } // Remove a certain request from ongoingList function reqFinish(id) { let i; for (i = 0; i < ongoingList.length; i++) { const req = ongoingList[i]; if (req.id === id) { ongoingList = delItem(ongoingList, i); return true; } } return false; } } } // Download and parse a url page into a html document(dom). // when xhr onload: callback.apply([dom, args]) function getDocument(url, callback, args=[]) { GM_xmlhttpRequest({ method : 'GET', url : url, responseType : 'blob', onloadstart : function() { DoLog(LogLevel.Info, 'getting document, url=\'' + url + '\''); }, onload : function(response) { const htmlblob = response.response; const reader = new FileReader(); reader.onload = function(e) { const htmlText = reader.result; const dom = new DOMParser().parseFromString(htmlText, 'text/html'); args = [dom].concat(args); callback.apply(null, args); //callback(dom, htmlText); } reader.readAsText(htmlblob, 'GBK'); /* 注意!原来这里只是使用了DOMParser,DOMParser不像iframe加载Document一样拥有完整的上下文并执行所有element的功能, ** 只是按照HTML格式进行解析,所以在文库页面的GBK编码下仍然会按照UTF-8编码进行解析,导致中文乱码。 ** 所以处理dom时不要使用ASC-II字符集以外的字符! ** ** 注:现在使用了FileReader来以GBK编码解析htmlText,故编码问题已经解决,可以正常使用任何字符 */ } }) } // File download function function downloadFile(details) { if (!details.url || !details.name) {return false;}; // Configure request object const requestObj = { url: details.url, responseType: 'blob', onload: function(e) { // Save file const a = document.createElement('a'); a.download = details.name; a.href = URL.createObjectURL(e.response); a.click(); // onload callback details.onload ? details.onload(e) : function() {}; } } if (details.onloadstart ) {requestObj.onloadstart = details.onloadstart;}; if (details.onprogress ) {requestObj.onprogress = details.onprogress;}; if (details.onerror ) {requestObj.onerror = details.onerror;}; if (details.onabort ) {requestObj.onabort = details.onabort;}; if (details.onreadystatechange) {requestObj.onreadystatechange = details.onreadystatechange;}; if (details.ontimeout ) {requestObj.ontimeout = details.ontimeout;}; // Send request GM_xmlhttpRequest(requestObj); } // Get a url argument from lacation.href // also recieve a function to deal the matched string // returns defultValue if name not found function getUrlArgv(name, dealFunc=(function(a) {return a;}), defultValue=null) { const url = location.href; const matcher = new RegExp(name + '=([^&]+)'); const result = url.match(matcher); const argv = result ? dealFunc(result[1]) : defultValue; return argv; } // Get a time text like 1970-01-01 00:00:00 function getTime(dateSpliter='-', timeSpliter=':') { const d = new Date(); const fulltime = fillNumber(d.getFullYear(), 4) + dateSpliter + fillNumber((d.getMonth() + 1), 2) + dateSpliter + fillNumber(d.getDate(), 2) + ' ' + fillNumber(d.getHours(), 2) + timeSpliter + fillNumber(d.getMinutes(), 2) + timeSpliter + fillNumber(d.getSeconds(), 2); return fulltime; } // Get key-value object from text like 'key: value'/'key:value'/' key : value ' // returns: {key: value, KEY: key, VALUE: value} function getKeyValue(text, delimiters=[':', ':', ',']) { // Modify from https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Error#examples // Create a new object, that prototypally inherits from the Error constructor. function SplitError(message) { this.name = 'SplitError'; this.message = message || 'SplitError Message'; this.stack = (new Error()).stack; } SplitError.prototype = Object.create(Error.prototype); SplitError.prototype.constructor = SplitError; if (!text) {return [];}; const result = {}; let key, value; for (let i = 0; i < text.length; i++) { const char = text.charAt(i); for (const delimiter of delimiters) { if (delimiter === char) { if (!key && !value) { key = text.substr(0, i).trim(); value = text.substr(i+1).trim(); result[key] = value; result.KEY = key; result.VALUE = value; } else { throw new SplitError('Mutiple Delimiter in Text'); } } } } return result; } // Fill number text to certain length with '0' function fillNumber(number, length) { let str = String(number); for (let i = str.length; i < length; i++) { str = '0' + str; } return str; } // Judge whether the str is a number function isNumeric(str) { const result = Number(str); return !isNaN(result) && str !== ''; } // Del a item from an array using its index. Returns the array but can NOT modify the original array directly!! function delItem(arr, delIndex) { arr = arr.slice(0, delIndex).concat(arr.slice(delIndex+1)); return arr; } // Makes a function that returns a unique ID number each time function uniqueIDMaker() { let id = 0; return makeID; function makeID() { id++; return id; } } // Append a style text to document() with a