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.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);
alinktofloor(floor.table);
}
}
// Bookcase page add-on
function pageBookcase() {
// Get auto-recommend config
let arConfig = CONFIG.AutoRecommend.getConfig()
// Get bookcase lists
const bookCaseURL = 'https://www.wenku8.net/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);
const bookcases = CONFIG.bookcasePrefs.getConfig(initPreferences).bookcases;
addTopTitle();
decorateForm(curForm, bookcases[curClassid]);
// gowork
showBookcases();
recommendAllGUI();
function recommendAllGUI() {
const block = createLeftBlock(TEXT_GUI_BOOKCASE_ATRCMMD, true, {
type: 'mypage',
links: [
{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 = document.createElement('input');
txtst.classList.add(CLASSNAME_TEXT);
btnAR.classList.add(CLASSNAME_BUTTON);
btnRN.classList.add(CLASSNAME_BUTTON);
checkbox.type = 'checkbox';
checkbox.checked = arConfig.auto;
checkbox.addEventListener('click', onclick);
btnAR.addEventListener('click', onclick);
btnAR.appendChild(checkbox);
btnRN.addEventListener('click', rcmmdnow);
function onclick(e) {
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 lists = [];
for (const option of options) {
lists.push({
classid: Number(option.value),
url: bookCaseURL.replace('{CID}', String(option.value)),
name: option.innerText
})
}
return {bookcases: lists};
}
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 = thisForm.classid ? thisForm.classid : curClassid;
if (cid > classid) {
content.insertBefore(form, thisForm);
break;
}
}
if(!form.parentElement) {content.appendChild(form);};
// Decorate
decorateForm(form, bookcase);
// finished increase
finished++;
textNode.nodeValue = finished < allCount ?
TEXT_GUI_BOOKCASE_GETTING.replace('C', String(finished)).replace('A', String(allCount)) :
oriTitleText;
}
}
function decorateForm(form, bookcase) {
const classid = bookcase.classid;
let name = bookcase.name;
// Provide auto-recommand button
arBtn();
// Modify properties
form.classList.add(CLASSNAME_BOOKCASE_FORM);
form.id += String(classid);
form.classid = classid;
form.onsubmit = my_check_confirm;
// Hide bookcase selector
const bcSelector = $(form, '[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 = document.createElement('input');
const form = this;
tip = TEXT_GUI_BOOKCASE_WHATNAME;
tipready ? tipshow(tip) : function(){};
titleBar.childNodes[0].nodeValue = '';
titleBar.appendChild(nameInput);
nameInput.value = name;
nameInput.addEventListener('blur', onblur);
nameInput.addEventListener('keydown', onkeydown)
nameInput.focus();
nameInput.setSelectionRange(0, name.length);
function onblur() {
tip = TEXT_GUI_BOOKCASE_DBLCLICK;
tipready ? 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 = document.createElement('td');
const input = document.createElement('input');
newTd.classList.add('odd');
input.type = 'number';
input.inputmode = 'numeric';
input.style.width = '85%';
input.value = arConfig.books[strBookID] ? String(arConfig.books[strBookID].number) : '0';
input.addEventListener('change', onvaluechange);
input.strBookID = strBookID; input.strBookName = strBookName;
newTd.appendChild(input); tr.appendChild(newTd);
}
// Insert a new row for auto-recommend options
function decorateHeader(tr) {
const allTh = $All(tr, 'th');
const width = ARR_GUI_BOOKCASE_WIDTH;
const newTh = document.createElement('th');
newTh.innerText = TEXT_GUI_BOOKCASE_ATRCMMD;
newTh.classList.add(CLASSNAME_TEXT);
tr.appendChild(newTh);
for (let i = 0; i < allTh.length; i++) {
const th = allTh[i];
th.style.width = width[i];
}
}
// Fit the width
function decorateFooter(tr) {
const td = $(tr, '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));
}
}
}
}
}
// 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);
});
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 pageResource = {elements: {}, infos: {}, download: {}};
collectPageResources(); DoLog(LogLevel.Info, pageResource, true)
// Remove ads
removeTopAds();
// Side-Panel buttons
sideButtons();
// Provide download GUI
downloadGUI();
// Prevent URL.revokeObjectURL in script 轻小说文库下载
revokeObjectURLHOOK();
// Beautifier page
beautifier();
// More font-sizes
moreFontSizes();
// Fill content if need
fillContent();
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 = document.createElement('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.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()
{
var contentmain = pageResource.elements.contentmain;
var currentpos = contentmain.scrollTop || 0;
contentmain.scrollTo(0, ++currentpos);
var nowpos = contentmain.scrollTop || 0;
if(currentpos != nowpos) unsafeWindow.clearInterval(timer)
}
}
}
// 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 = document.createElement('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
waitUntilLoaded('loadSet', function() {
loadSet();
});
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;
// Get content xml
AndAPI.getNovelContent({
aid: unsafeWindow.article_id,
cid: unsafeWindow.chapter_id,
lang: 0,
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 = document.createElement('a');
a.href = url;
a.download = name;
a.click();
download.finished++;
updateDownloadStatus();
// Code below seems can work, but actually it doesn't work well and somtimes some file cannot be saved
// The reason is still unknown, but from what I know I can tell that mistakes happend in GM_xmlhttpRequest
// Error stack: GM_xmlhttpRequest.onload ===> image.onload ===> downloadFile ===> GM_xmlhttpRequest =X=> .onload
// This Error will also stuck the GMXHRHook.ongoingList
/*downloadFile({
url: url,
name: name,
onload: function() {
download.finished++;
DoLog(LogLevel.Info, '[' + String(i) + ']file saved: ' + name);
alert('[' + String(i) + ']file saved: ' + name);
updateDownloadStatus();
},
onerror: function() {
alert('downloadfile error! url = ' + String(url) + ', i = ' + String(i));
}
})*/
}
image.onerror = function() {
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 = 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 = $('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 = 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() {
}
// 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
link.tdlink = tr.children[0];
link.tdinfo = tr.children[1];
link.tdtime = tr.children[2];
link.tdoprt = tr.children[3];
// Inside |
link.alink = link.tdlink.children[0];
link.aedit = link.tdoprt.children[0];
link.apos = link.tdoprt.children[1];
link.adel = link.tdoprt.children[2];
// Infos
link.href = link.alink.href;
link.ulid = getUrlArgv({url: link.apos.href, name: 'ulid'});
link.name = link.alink.innerText;
link.info = link.tdinfo.innerText;
link.time = link.tdtime.innerText;
link.purl = link.apos.href;
links.push(link);
}
return links;
}
}
// 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 = $All('.blockcontent .userinfo')[0].parentElement;
const container = oriContainer.cloneNode(true);
const button = $(container, 'a');
button.innerText = TEXT_GUI_USER_REVIEWSEARCH;
button.href = URL_REVIEWSEARCH.replaceAll('{K}', String(UID));
oriContainer.parentElement.appendChild(container);
}
}
// Detail page add-on
function pageDetail() {
// Get elements
const content = $('#content');
const tbody = $(content, 'table>tbody');
insertSettings();
// Insert Settings GUI
function insertSettings() {
let elements = GUI();
function GUI() {
const review = CONFIG.BkReviewPrefs.getConfig();
const settings = [
[{html: TEXT_GUI_DETAIL_TITLE_SETTINGS, colSpan: 3, class: 'foot'}],
[{html: TEXT_GUI_DETAIL_TITLE_BGI}, {colSpan: 2, key: 'bgimage', tiptitle: TEXT_TIP_IMAGE_FIT}],
[{html: TEXT_GUI_DETAIL_BGI_UPLOAD}, {colSpan: 2, key: 'bgupload'}],
[{html: TEXT_GUI_DETAIL_GUI_IMAGER}, {colSpan: 2, key: 'imager'}],
[{html: TEXT_GUI_DETAIL_GUI_SCALE}, {colSpan: 2, key: 'scalectnr'}],
[{html: TEXT_GUI_DETAIL_BTF_NOVEL}, {colSpan: 2, key: 'btfnvlctnr'}],
[{html: TEXT_GUI_DETAIL_BTF_REVIEW}, {colSpan: 2, key: 'btfrvwctnr'}],
[{html: TEXT_GUI_DETAIL_BTF_COMMON}, {colSpan: 2, key: 'btfcmnctnr'}],
[{html: TEXT_GUI_DETAIL_FVR_LASTPAGE}, {colSpan: 2, key: 'favoropen'}],
[{html: TEXT_GUI_DETAIL_VERSION_CURVER}, {colSpan: 2, key: 'curversion'}],
[{html: TEXT_GUI_DETAIL_VERSION_CHECKUPDATE}, {colSpan: 2, key: 'updatecheck'}],
[{html: TEXT_GUI_DETAIL_FEEDBACK_TITLE, colSpan: 1, key: 'feedbackttle'}, {html: TEXT_GUI_DETAIL_FEEDBACK, colSpan: 2, key: 'feedback'}],
[{html: TEXT_GUI_DETAIL_UPDATEINFO_TITLE, colSpan: 1, key: 'feedbackttle'}, {html: TEXT_GUI_DETAIL_UPDATEINFO, colSpan: 2, key: 'updateinfo'}],
[{html: TEXT_GUI_DETAIL_CONFIG_EXPORT}, {html: TEXT_GUI_DETAIL_EXPORT_CLICK, colSpan: 2, key: 'exportcfg'}],
[{html: TEXT_GUI_DETAIL_CONFIG_EXPORT_NOPASS}, {html: TEXT_GUI_DETAIL_EXPORT_CLICK, colSpan: 2, key: 'exportcfgnp'}],
[{html: TEXT_GUI_DETAIL_CONFIG_IMPORT, colSpan: 1, key: 'importcfgttle'}, {html: TEXT_GUI_DETAIL_IMPORT_CLICK, colSpan: 2, key: 'importcfg'}],
[{html: TEXT_GUI_DETAIL_CONFIG_MANAGE, colSpan: 1, key: 'managecfgttle'}, {html: TEXT_GUI_DETAIL_MANAGE_CLICK, colSpan: 2, key: 'managecfg'}],
//[{html: TEXT_GUI_DETAIL_XXXXXX_XXXXXX, colSpan: 1, key: 'xxxxxxxx'}, {html: TEXT_GUI_DETAIL_XXXXXX_XXXXXX, colSpan: 2, key: 'xxxxxxxx'}],
]
const elements = createTableGUI(settings);
const tdBgi = elements.bgimage;
const imageinput = elements.imageinput = document.createElement('input');
const bgioprt = elements.bgioprt = document.createElement('span');
const bgiupld = elements.bgupload;
const ckbgiup = elements.ckbgiup = document.createElement('input');
ckbgiup.type = 'checkbox';
ckbgiup.checked = CONFIG.BeautifierCfg.getConfig().upload;
ckbgiup.addEventListener('change', uploadChange);
settip(ckbgiup, TEXT_GUI_DETAIL_BGI_LEGAL);
bgiupld.appendChild(ckbgiup);
imageinput.type = 'file';
imageinput.style.display = 'none';
imageinput.addEventListener('change', pictureGot);
bgioprt.innerHTML = TEXT_GUI_DETAIL_DEFAULT_BGI + '' + TEXT_GUI_DETAIL_BGI.replace('{N}', CONFIG.BeautifierCfg.getConfig().bgiName);
bgioprt.style.color = 'grey';
settip(bgioprt, TEXT_TIP_IMAGE_FIT);
tdBgi.addEventListener("dragenter", destroyEvent);
tdBgi.addEventListener("dragover", destroyEvent);
tdBgi.addEventListener('drop', pictureGot);
tdBgi.style.textAlign = 'center';
tdBgi.addEventListener('click', ()=>{elements.imageinput.click();});
tdBgi.appendChild(imageinput);
tdBgi.appendChild(bgioprt);
// Imager
const curimager = CONFIG.UserGlobalCfg.getConfig().imager;
elements.imager.style.padding = '0px 0.5em';
for (const [key, imager] of Object.entries(DATA_IMAGERS)) {
if (typeof(imager) !== 'object' || !imager.isImager) {continue;}
const span = document.createElement('span');
const radio = document.createElement('input');
const text = document.createElement('span');
radio.type = 'radio';
radio.value = '';
radio.id = 'imager_'+key;
radio.imagerkey = key;
radio.name = 'imagerselect';
radio.style.cursor = 'pointer';
radio.addEventListener('change', imagerChange);
radio.disabled = !imager.available;
text.innerText = imager.name + (imager.available ? '' : '(已失效)');
text.style.marginRight = '1em';
text.style.cursor = 'pointer';
text.addEventListener('click', function() {radio.click();});
span.style.display = 'inline-block';
span.appendChild(radio);
span.appendChild(text);
if (imager.tip) {
let tip = imager.tip;
DATA_IMAGERS.default === key && (tip += TEXT_TIP_IMAGER_DEFAULT);
!imager.available && (tip = '{T}已失效'.replace('{T}', tip));
settip(radio, tip);
settip(text, tip);
//settip(span, imager.tip);
}
elements.imager.appendChild(span);
}
$(elements.imager, '#imager_'+curimager).checked = true;
// Text scale
const textScale = CONFIG.BeautifierCfg.getConfig().textScale;
const scalectnr = elements.scalectnr;
const elmscale = elements.scale = document.createElement('input');
elmscale.type = 'number';
elmscale.id = 'textScale';
elmscale.value = textScale;
elmscale.addEventListener('change', scaleChange);
elmscale.addEventListener('keydown', (e) => {e.keyCode === 13 && scaleChange();});
scalectnr.appendChild(elmscale);
scalectnr.appendChild(document.createTextNode('%'));
// Beautifier
const btfnvlctnr = elements.btfnvlctnr;
const btfrvwctnr = elements.btfrvwctnr;
const btfcmnctnr = elements.btfcmnctnr;
const ckbtfnvl = elements.ckbtfnvl = document.createElement('input');
const ckbtfrvw = elements.ckbtfrvw = document.createElement('input');
const ckbtfcmn = elements.ckbtfcmn = document.createElement('input');
ckbtfnvl.type = ckbtfrvw.type = ckbtfcmn.type = 'checkbox';
ckbtfnvl.page = 'novel';
ckbtfrvw.page = 'reviewshow';
ckbtfcmn.page = 'common';
ckbtfnvl.checked = CONFIG.BeautifierCfg.getConfig().novel.beautiful;
ckbtfrvw.checked = CONFIG.BeautifierCfg.getConfig().reviewshow.beautiful;
ckbtfcmn.checked = CONFIG.BeautifierCfg.getConfig().common.beautiful;
ckbtfnvl.addEventListener('change', beautifulChange);
ckbtfrvw.addEventListener('change', beautifulChange);
ckbtfcmn.addEventListener('change', beautifulChange);
btfnvlctnr.appendChild(ckbtfnvl);
btfrvwctnr.appendChild(ckbtfrvw);
btfcmnctnr.appendChild(ckbtfcmn);
// Favorite open
const favoropen = elements.favoropen;
const favorlast = elements.favorlast = document.createElement('input');
favorlast.type = 'checkbox';
favorlast.checked = CONFIG.BkReviewPrefs.getConfig().favorlast;
favorlast.addEventListener('change', favorlastChange);
favoropen.appendChild(favorlast);
// Version control
const curversion = elements.curversion;
const updatecheck = elements.updatecheck;
const versiondisplay = document.createElement('span');
versiondisplay.innerText = 'v' + GM_info.script.version;
updatecheck.innerText = TEXT_GUI_DETAIL_VERSION_CHECK;
updatecheck.style.color = 'grey';
updatecheck.style.textAlign = 'center';
updatecheck.addEventListener('click', updateOnclick);
curversion.appendChild(versiondisplay);
// Feedback
const feedback = elements.feedback;
feedback.style.color = 'grey';
feedback.style.textAlign = 'center';
feedback.addEventListener('click', function() {
window.open('https://greasyfork.org/scripts/416310/feedback');
});
// Update info
const updateinfo = elements.updateinfo;
updateinfo.style.color = 'grey';
updateinfo.style.textAlign = 'center';
updateinfo.addEventListener('click', function() {
window.open('https://greasyfork.org/scripts/416310#updateinfo');
})
// Config export/import
const exportcfg = elements.exportcfg;
const exportcfgnp = elements.exportcfgnp;
const importcfg = elements.importcfg;
const configinput = elements.configinput = document.createElement('input');
configinput.type = 'file';
configinput.style.display = 'none';
importcfg.style.color = exportcfgnp.style.color = exportcfg.style.color = 'grey';
importcfg.style.textAlign = exportcfgnp.style.textAlign = exportcfg.style.textAlign = 'center';
exportcfg.addEventListener('click', exportConfig);
exportcfgnp.addEventListener('click', ()=>{exportConfig(true);});
importcfg.addEventListener('click', () => {configinput.click()});
configinput.addEventListener('change', configfileGot);
importcfg.addEventListener("dragenter", destroyEvent);
importcfg.addEventListener("dragover", destroyEvent);
importcfg.addEventListener('drop', configfileGot);
//importcfg.appendChild(configinput);
// Config management
const managecfg = elements.managecfg;
managecfg.style.color = 'grey';
managecfg.style.textAlign = 'center';
managecfg.addEventListener('click', openManagePanel);
// Paste event
window.addEventListener('paste', filePasted);
return elements;
}
function filePasted(e) {
const input = e.dataTransfer || e.clipboardData || window.clipboardData || e.target;
if (!input.files || input.files.length === 0) {return false;};
for (const file of input.files) {
switch (file.type) {
case 'image/bmp':
case 'image/gif':
case 'image/vnd.microsoft.icon':
case 'image/jpeg':
case 'image/png':
case 'image/svg+xml':
case 'image/tiff':
case 'image/webp':
confirm(TEXT_GUI_DETAIL_CONFIG_IMPORT_CONFIRM_SELECT.replace('{N}', file.name)) && pictureGot(e);
break;
case '': {
const splited = file.name.split('.');
const ext = splited[splited.length-1];
switch (ext) {
case 'wkp':
confirm(TEXT_GUI_DETAIL_CONFIG_IMPORT_CONFIRM_PASTE.replace('{N}', file.name)) && configfileGot(e);
}
}
}
}
}
function pictureGot(e) {
e.preventDefault();
// Get file
const input = e.dataTransfer || e.clipboardData || window.clipboardData || e.target;
if (!input.files || input.files.length === 0) {return false;};
const fileObj = input.files[0];
const mimetype = fileObj.type;
const name = fileObj.name;
// Create a new file input
elements.bgimage.removeChild(elements.imageinput);
const imageinput = elements.imageinput = document.createElement('input');
imageinput.type = 'file';
imageinput.style.display = 'none';
imageinput.addEventListener('change', pictureGot);
elements.bgimage.appendChild(imageinput);
if (!mimetype || mimetype.split('/')[0] !== 'image') {
alertify.error(TEXT_ALT_IMAGE_FORMATERROR);
return false;
}
elements.bgioprt.innerHTML = TEXT_GUI_DETAIL_BGI_WORKING;
// Get object url
const objurl = URL.createObjectURL(fileObj);
// Get image url(format base64)
getImageUrl(objurl, true, true, (url) => {
if (!url) {return false;};
// Save to config
const config = CONFIG.BeautifierCfg.getConfig();
config.backgroundImage = url;
config.bgiName = name;
CONFIG.BeautifierCfg.saveConfig(config);
elements.bgioprt.innerHTML = name;
URL.revokeObjectURL(objurl);
// Upload if need
if (config.upload) {
alertify.notify(TEXT_ALT_IMAGE_UPLOAD_WORKING);
elements.bgioprt.innerHTML = TEXT_GUI_DETAIL_BGI_UPLOADING.replace('{NAME}', name);
const file = dataURLtoFile(url, name);
uploadImage({
file: file,
onerror: (e) => {
alertify.error(TEXT_ALT_IMAGE_UPLOAD_ERROR);
DoLog(LogLevel.Error, ['Upload error at pictureGot:', e]);
elements.bgioprt.innerHTML = TEXT_GUI_DETAIL_BGI_UPLOADFAILED.replace('{NAME}', name);
const config = CONFIG.BeautifierCfg.getConfig();
config.upload = elements.ckbgiup.checked = /^https?:\/\//.test(config.reveiwshow.backgroundImage);
CONFIG.BeautifierCfg.saveConfig(config);
},
onload: (json) => {
const config = CONFIG.BeautifierCfg.getConfig();
config.backgroundImage = json.url;
CONFIG.BeautifierCfg.saveConfig(config);
elements.bgioprt.innerHTML = TEXT_GUI_DETAIL_DEFAULT_BGI + '' + TEXT_GUI_DETAIL_BGI.replace('{N}', CONFIG.BeautifierCfg.getConfig().bgiName);
alertify.success(TEXT_ALT_IMAGE_UPLOAD_SUCCESS.replace('{NAME}', json.name).replace('{URL}', json.url));
}
})
}
});
}
function uploadChange(e) {
e.preventDefault();
const config = CONFIG.BeautifierCfg.getConfig();
config.upload = !config.upload;
CONFIG.BeautifierCfg.saveConfig(config);
const name = config.bgiName ? config.bgiName : 'image.jpeg';
if (config.upload) {
// Upload
const url = config.backgroundImage;
if (!/^https?:\/\//.test(url)) {
alertify.notify(TEXT_ALT_IMAGE_UPLOAD_WORKING);
elements.bgioprt.innerHTML = TEXT_GUI_DETAIL_BGI_UPLOADING.replace('{NAME}', name);
const file = dataURLtoFile(url, name);
uploadImage({
file: file,
onerror: (e) => {
alertify.error(TEXT_ALT_IMAGE_UPLOAD_ERROR);
DoLog(LogLevel.Error, ['Upload error at uploadChange:', e]);
elements.bgioprt.innerHTML = TEXT_GUI_DETAIL_BGI_UPLOADFAILED.replace('{NAME}', name);
const config = CONFIG.BeautifierCfg.getConfig();
config.upload = elements.ckbgiup.checked = /^https?:\/\//.test(config.backgroundImage);
CONFIG.BeautifierCfg.saveConfig(config);
},
onload: (json) => {
const config = CONFIG.BeautifierCfg.getConfig();
config.backgroundImage = json.url;
config.bgiName = elements.bgioprt.innerHTML = json.name;
CONFIG.BeautifierCfg.saveConfig(config);
alertify.success(TEXT_ALT_IMAGE_UPLOAD_SUCCESS.replace('{NAME}', json.name).replace('{URL}', json.url));
}
});
}
} else {
// Download
const url = config.backgroundImage;
if (/^https?:\/\//.test(url)) {
alertify.notify(TEXT_ALT_IMAGE_DOWNLOAD_WORKING);
elements.bgioprt.innerHTML = TEXT_GUI_DETAIL_BGI_DOWNLOADING.replace('{NAME}', name);
getImageUrl(url, true, true, (dataurl) => {
if (!dataurl) {
const config = CONFIG.BeautifierCfg.getConfig();
config.upload = elements.ckbgiup.checked = /^https?:\/\//.test(config.backgroundImage);
CONFIG.BeautifierCfg.saveConfig(config);
return false;
};
// Save to config
const config = CONFIG.BeautifierCfg.getConfig();
config.backgroundImage = dataurl;
CONFIG.BeautifierCfg.saveConfig(config);
alertify.success(TEXT_ALT_IMAGE_DOWNLOAD_SUCCESS.replace('{NAME}', name));
elements.bgioprt.innerHTML = name;
});
}
}
setTimeout(()=>{elements.ckbgiup.checked = config.upload;}, 0);
}
function imagerChange(e) {
e.stopPropagation();
const radio = e.target;
if (radio.checked) {
const imager = DATA_IMAGERS[radio.imagerkey];
const config = CONFIG.UserGlobalCfg.getConfig();
config.imager = radio.imagerkey;
CONFIG.UserGlobalCfg.saveConfig(config);
alertify.message('图床已切换到{NAME}'.replace('{NAME}', imager.name));
imager.warning && alertify.warning(imager.warning);
}
}
function scaleChange(e) {
e.stopPropagation();
const config = CONFIG.BeautifierCfg.getConfig();
config.textScale = e.target.value;
CONFIG.BeautifierCfg.saveConfig(config);
alertify.message(TEXT_ALT_TEXTSCALE_CHANGED.replaceAll('{S}', config.textScale));
}
function beautifulChange(e) {
e.stopPropagation();
const checkbox = e.target;
const config = CONFIG.BeautifierCfg.getConfig();
config[checkbox.page].beautiful = checkbox.checked;
CONFIG.BeautifierCfg.saveConfig(config);
alertify.message(checkbox.checked ? TEXT_ALT_BEAUTIFUL_ON : TEXT_ALT_BEAUTIFUL_OFF);
}
function favorlastChange(e) {
e.stopPropagation();
const checkbox = e.target;
const config = CONFIG.BkReviewPrefs.getConfig();
config.favorlast = checkbox.checked;
CONFIG.BkReviewPrefs.saveConfig(config);
alertify.message(checkbox.checked ? TEXT_ALT_FAVORITE_LAST_ON : TEXT_ALT_FAVORITE_LAST_OFF);
}
function updateOnclick(e) {
TASK.Script.update(true);
}
function configfileGot(e) {
e.preventDefault();
// Get file
const input = e.dataTransfer || e.clipboardData || window.clipboardData || e.target;
if (!input.files || input.files.length === 0) {return false;};
const fileObj = input.files[0];
const splitedname = fileObj.name.split('.');
const ext = splitedname[splitedname.length-1].toLowerCase();
if (ext !== 'wkp') {
alertify.error(TEXT_ALT_DETAIL_CONFIG_IMPORT_ERROR_SELECT);
DoLog(LogLevel.Warning, 'pageDetail.insertSettings.GUI.configfileGot: userinput error.')
return false;
}
// Read config from file
try {
const FR = new FileReader();
FR.onload = fileOnload;
FR.readAsText(fileObj);
} catch(e) {
fileError(e);
}
function fileOnload(e) {
try {
// Get json
const json = JSON.parse(e.target.result);
// Import
importConfig(json);
alertify.success(TEXT_ALT_DETAIL_IMPORTED);
} catch(err) {
fileError(err);
}
}
function fileError(e) {
DoLog(LogLevel.Error, ['pageDetail.insertSettings.GUI.configfileGot:', e]);
alertify.error(TEXT_ALT_DETAIL_CONFIG_IMPORT_ERROR_READ);
}
}
function openManagePanel(e) {
const settings = {
id: 'ConfigPanel'
};
const SetPanel = new SettingPanel(settings);
const tblAccount = SetPanel.tables[0];
account();
drafts();
pending_tip();
SetPanel.usercss += '.settingpanel-block.sp-center {text-align: center;}'
function account() {
const userConfig = CONFIG.GlobalConfig.getConfig();
const users = userConfig.users ? userConfig.users : {};
// Create table
const table = new SetPanel.PanelTable({
rows: [{
blocks: [{
className: 'sp-center',
innerHTML: '账号管理',
colSpan: 3
}]
},{
blocks: [{
className: 'sp-center',
innerHTML: '用户名'
},{
className: 'sp-center',
innerHTML: '密码'
},{
className: 'sp-center',
innerHTML: '操作'
}]
}]
});
SetPanel.appendTable(table);
for (const [name, user] of Object.entries(users)) {
// Get account
const username = user.username;
const password = user.password;
// Row
const row = new SetPanel.PanelRow();
table.appendRow(row);
// Block username
const block_username = new SetPanel.PanelBlock({
className: 'sp-center',
innerHTML: username
});
// Block password
const spanpswd = document.createElement('span');;
spanpswd.innerHTML = '*'.repeat(password.length);
const block_password = new SetPanel.PanelBlock({
className: 'sp-center',
children: [spanpswd]
});
// Block operator
const btndel = _createBtn('删除', make_del_callback(row, username));
const elmshow = document.createElement('span'); elmshow.innerHTML = '查看';
const btnshow = _createBtn(elmshow, make_show_callback(elmshow, spanpswd, password));
const block_operator = new SetPanel.PanelBlock({
className: 'sp-center',
children: [btnshow, btndel]
});
// Append row to SettingPanel
row.appendBlock(block_username).appendBlock(block_password).appendBlock(block_operator);
}
function make_del_callback(row, username) {
return function(e) {
const userConfig = CONFIG.GlobalConfig.getConfig();
delete userConfig.users[username];
CONFIG.GlobalConfig.saveConfig(userConfig);
row.remove();
}
}
function make_show_callback(btn, span, password) {
let show = false;
let timeout;
return function toggle(e) {
show = !show;
span.innerHTML = show ? password : '*'.repeat(password.length);
btn.innerHTML = show ? '隐藏' : '查看';
}
}
}
function drafts() {
// Get config
const allCData = CONFIG.commentDrafts.getConfig();
// Create table
const table = new SetPanel.PanelTable({
rows: [{
blocks: [{
className: 'sp-center',
innerHTML: '书评草稿管理',
colSpan: 3
}]
},{
blocks: [{
className: 'sp-center',
innerHTML: '标题'
},{
className: 'sp-center',
innerHTML: '内容'
},{
className: 'sp-center',
innerHTML: '操作'
}]
}]
});
SetPanel.appendTable(table);
// Append rows
for (const [propkey, commentData] of Object.entries(allCData)) {
if (propkey === KEY_DRAFT_VERSION) {continue;}
const title = commentData.title;
const content = commentData.content;
const key = commentData.key;
// Row
const row = new SetPanel.PanelRow();
table.appendRow(row);
// Block title
const span_title = document.createElement('span');
span_title.innerHTML = _decorate(title);
const block_title = new SetPanel.PanelBlock({className: 'draft-title sp-center', children: [span_title]});
// Block content
const span_content = document.createElement('span');
span_content.innerHTML = _decorate(content);
const block_content = new SetPanel.PanelBlock({className: 'draft-content', children: [span_content]});
// Block operator
const elmshow = document.createElement('span'); elmshow.innerHTML = '展开';
const btnshow = _createBtn(elmshow, make_show_callback(elmshow, key, row, span_title, span_content));
//const btnedit = _createBtn('编辑', make_edit_callback(key, row));
const btnopen = _createBtn('打开', make_open_callback(key));
const btndel = _createBtn('删除', make_del_callback(key, row));
const block_operator = new SetPanel.PanelBlock({className: 'draft-operator sp-center', children: [btnshow, btnopen, btndel]});
// Append to row
row.appendBlock(block_title).appendBlock(block_content).appendBlock(block_operator);
// Append css
SetPanel.usercss += '.settingpanel-block.draft-title {width: 20%;} .settingpanel-block.draft-content {width: 50%;} .settingpanel-block.draft-operator {width: 30%}';
}
function make_show_callback(btn, key, row, span_title, span_content) {
let show = false;
return function() {
const allCData = CONFIG.commentDrafts.getConfig();
const data = allCData[key];
if (!data) {
alertify.warning(TEXT_ALT_DETAIL_MANAGE_NOTFOUND);
row.remove();
return false;
}
show = !show;
btn.innerHTML = show ? '收起' : '展开';
span_title.innerHTML = show ? _decorate(data.title, -1) : _decorate(data.title);
span_content.innerHTML = show ? _decorate(data.content, -1) : _decorate(data.content);
};
}
function make_edit_callback(key, row) {
return function() {
// Get data
const allCData = CONFIG.commentDrafts.getConfig();
const data = allCData[key];
if (!data) {
alertify.warning(TEXT_ALT_DETAIL_MANAGE_NOTFOUND);
row.remove();
return false;
}
// Create box gui
const box = alertify.alert();
const container = box.elements.content;
makeEditor(container, data.rid.toString());
const form = $(container, 'form');
const ptitle = $(container, '#ptitle');
const pcontent = $(container, '#pcontent');
ptitle.value = data.title;
pcontent.value = data.content;
box.setting({
maximizable: false,
resizable: true
});
box.resizeTo('80%', '60%');
box.show();
};
}
function make_open_callback(key, row) {
return function() {
const allCData = CONFIG.commentDrafts.getConfig();
const data = allCData[key];
if (!data) {
alertify.warning(TEXT_ALT_DETAIL_MANAGE_NOTFOUND);
row.remove();
return false;
}
const url = data.rid ? URL_REVIEWSHOW_1.replace('{R}', data.rid.toString()) : URL_NOVELINDEX.replace('{I}', data.bid.toString());
window.open(url);
}
}
function make_del_callback(key, row) {
return function() {
const allCData = CONFIG.commentDrafts.getConfig();
delete allCData[key];
CONFIG.commentDrafts.saveConfig(allCData);
row.remove();
};
}
}
function pending_tip() {
const span = document.createElement('span');
span.innerHTML = '*其他管理项PY-DNG尚在开发中,请耐心等待';
span.classList.add(CLASSNAME_TEXT);
SetPanel.element.appendChild(span);
}
function _createBtn(htmlorbtn, onclick) {
const innerHTML = typeof htmlorbtn === 'string' ? htmlorbtn : htmlorbtn.innerHTML;
const btn = htmlorbtn instanceof HTMLElement ? htmlorbtn : document.createElement('span');
btn.classList.add(CLASSNAME_BUTTON);
btn.innerHTML = innerHTML;
btn.style.margin = '0px 0.5em';
btn.addEventListener('click', onclick);
return btn;
}
function _decorate(text, length=16) {
const len = length > 0 ? length : 9999999999999;
const overflow = (text.length - len) > length;
const cut = overflow ? text.substr(0, len) : text;
const encoded = htmlEncode(cut).replaceAll('\n', '');
const filled = text.length === 0 ? TEXT_GUI_DETAIL_CONFIG_MANAGE_EMPTY : (overflow ? encoded + TEXT_GUI_DETAIL_CONFIG_MANAGE_MORE : encoded);
return filled;
}
}
}
function createTableGUI(lines) {
const elements = {};
for (const line of lines) {
const tr = document.createElement('tr');
for (const item of line) {
const td = document.createElement('td');
item.html && (td.innerHTML = item.html);
item.colSpan && (td.colSpan = item.colSpan);
item.class && (td.className = item.class);
item.id && (td.id = item.id);
item.tiptitle && settip(td, item.tiptitle);
item.key && (elements[item.key] = td);
td.style.padding = '3px';
tr.appendChild(td);
}
tbody.appendChild(tr);
}
return elements;
function ElementObject(element) {
const p = new Proxy(element, {
get: function(elm, id, receiver) {
return elm[id] || $(elm, '#'+id);
}
});
return p;
}
}
}
// Index page add-on
function pageIndex() {
insertStatus();
showFavorites();
// Show favorite reviews
function showFavorites() {
const links = [];
const config = CONFIG.BkReviewPrefs.getConfig();
for (const [rid, favorite] of Object.entries(config.favorites)) {
const href = favorite.href + (config.favorlast ? '&page=last' : '');
const tiptitle = favorite.tiptitle ? favorite.tiptitle : href;
const innerHTML = favorite.name.substr(0, 12) // prevent overflow
links.push({
innerHTML: innerHTML,
tiptitle: tiptitle,
href: href
});
}
const block = createLeftBlock(TEXT_GUI_INDEX_FAVORITES, true, {
type: 'toplist',
links: links
})
}
// Insert usersript inserted tip
function insertStatus() {
const blockcontent = $('#centers>.block:nth-child(1)>.blockcontent');
blockcontent.appendChild(document.createElement('br'));
const textNode = document.createElement('span');
textNode.innerText = TEXT_GUI_INDEX_STATUS;
textNode.classList.add(CLASSNAME_TEXT);
blockcontent.appendChild(textNode);
}
}
// 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 = $('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 = $('table>tbody');
const header = $(tbody, 'th').parentElement;
const thead = $(header, '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 = $All(tbody, '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, 'td.even').cloneNode(true);
const links = $All(newTd, 'a');
for (const a of links) {
a.classList.add(CLASSNAME_BUTTON);
a.info = {
description: 'volume download button',
name: novelInfo.volumeNames[index],
filename: TEXT_GUI_SDOWNLOAD_FILENAME
.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 = $All(newTr, '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 = $All(newTds[3], '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 $All('.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 = $All('#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);
});
settip(serverEles[i], TEXT_TIP_SERVERCHANGE);
}
}
// Change all server elements
function changeAllServers(server) {
let i;
const allA = $All('.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 + '/');
}
}
// Login page add-on
function pageLogin() {
const form = $('form[name="frmlogin"]');
if (!form) {return false;}
const eleUsername = $(form, 'input.text[name="username"]');
const elePassword = $(form, 'input.text[name="password"]')
catchAccount();
// Save account info
function catchAccount() {
form.addEventListener('submit', () => {
const config = CONFIG.GlobalConfig.getConfig();
const username = eleUsername.value;
const password = elePassword.value;
config.users = config.users ? config.users : {};
config.users[username] = {
username: username,
password: password
}
CONFIG.GlobalConfig.saveConfig(config);
});
}
}
// Account fast switching
function multiAccount() {
if (!$('.fl')) {return false;};
GUI();
function GUI() {
// Add switch select
const eleTopLeft = $('.fl');
const eletext = document.createElement('span');
const sltSwitch = document.createElement('select');
eletext.innerText = TEXT_GUI_ACCOUNT_SWITCH;
eletext.classList.add(CLASSNAME_TEXT);
eletext.style.marginLeft = '0.5em';
eleTopLeft.appendChild(eletext);
eleTopLeft.appendChild(sltSwitch);
// Not logged in, create and select an empty option
// Select current user's option
if (!getUserName()) {
appendOption(TEXT_GUI_ACCOUNT_NOTLOGGEDIN, '').selected = true;
};
// Add select options
const userConfig = CONFIG.GlobalConfig.getConfig();
const users = userConfig.users ? userConfig.users : {};
const names = Object.keys(users);
if (names.length === 0) {
appendOption(TEXT_GUI_ACCOUNT_NOACCOUNT, '');
settip(sltSwitch, TEXT_TIP_ACCOUNT_NOACCOUNT);
}
for (const username of names) {
appendOption(username, username)
}
// Select current user's option
if (getUserName()) {selectCurUser();};
// onchange: switch account
sltSwitch.addEventListener('change', (e) => {
const select = e.target;
if (!select.value || !confirm(TEXT_GUI_ACCOUNT_CONFIRM.replace('{N}', select.value))) {
selectCurUser();
destroyEvent(e);
return;
}
switchAccount(select.value);
});
function appendOption(text, value) {
const option = document.createElement('option');
option.innerText = text;
option.value = value;
sltSwitch.appendChild(option);
return option;
}
function selectCurUser() {
for (const option of $All(sltSwitch, 'option')) {
option.selected = getUserName().toLowerCase() === option.value.toLowerCase();
}
}
}
function switchAccount(username) {
// Logout
alertify.notify(TEXT_ALT_ACCOUNT_WORKING_LOGOFF);
GM_xmlhttpRequest({
method: 'GET',
url: URL_USRLOGOFF,
onload: function(response) {
// Login
alertify.notify(TEXT_ALT_ACCOUNT_WORKING_LOGIN);
const account = CONFIG.GlobalConfig.getConfig().users[username];
const data = DATA_XHR_LOGIN
.replace('{U}', $URL.encode(account.username))
.replace('{P}', $URL.encode(account.password))
.replace('{C}', $URL.encode('315360000')) // Expire time: 1 year
GM_xmlhttpRequest({
method: 'POST',
url: URL_USRLOGIN,
data: data,
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
onload: function() {
let box = alertify.success(TEXT_ALT_ACCOUNT_SWITCHED.replace('{N}', username));
redirectGMStorage(getUserID());
DoLog(LogLevel.Info, 'GM_storage redirected to ' + String(getUserID()));
const timeout = setTimeout(()=>{location.href=location.href;}, 3000);
box.callback = (isClicked) => {
isClicked && clearTimeout(timeout);
};
}
})
}
})
}
}
// API page and its sub pages add-on
function pageAPI(API) {
addStyle(CSS_PAGE_API, 'plus_api_css');
//logAPI();
let result;
switch(API) {
case 'modules/article/addbookcase.php':
result = pageAddbookcase();
break;
case 'modules/article/packshow.php':
result = pagePackshow();
break;
default:
result = logAPI();
}
return result;
function logAPI() {
DoLog('This is wenku API page.');
DoLog('API is: [' + API + ']');
DoLog('There is nothing to do. Quiting...');
}
function pageAddbookcase() {
// Append link to bookcase page
addBottomButton({
href: 'https://www.wenku8.net/modules/article/bookcase.php',
innerHTML: TEXT_GUI_API_ADDBOOKCASE_TOBOOKCASE
});
// Append link to remove from bookcase (not finished)
/*addBottomButton({
href: 'https://www.wenku8.net/modules/article/bookcase.php?delid=' + getUrlArgv('bid'),
innerHTML: TEXT_GUI_API_ADDBOOKCASE_REMOVE,
onclick: function() {
confirm('确实要将本书移出书架么?')
}
});*/
}
function pagePackshow() {
// Load packshow page
loadPage();
// Packshow page loader
function loadPage() {
// Data
const language = getLang();
const aid = getUrlArgv('id');
const type = getUrlArgv('type');
if (!['txt', 'txtfull', 'umd', 'jar'].includes(type)) {
return false;
}
// Hide api box
const apiBox = $('body>div:nth-child(1)');
apiBox.style.display = 'none';
// Disable api css
addStyle('', 'plus_api_css');
// AsyncManager
const resource = {xmlIndex: null, xmlInfo: null, oDoc: null};
const AM = new AsyncManager();
AM.onfinish = fetchFinish;
// Show soft alert
alertify.message(TEXT_TIP_API_PACKSHOW_LOADING);
// Set Title
document.title = TEXT_GUI_API_PACKSHOW_TITLE_LOADING;
// Load model page
const bgImage = $('body>.plus_cbty_image');
AM.add();
getDocument(URL_PACKSHOW.replace('{A}', "1").replace('{T}', type), function(oDoc) {
resource.oDoc = oDoc;
// Insert body elements
const nodes = Array.prototype.map.call(oDoc.body.childNodes, (elm) => (elm));
for (const node of nodes) {
document.body.insertBefore(node, bgImage);
}
// Insert css link and scripts
const links = Array.prototype.map.call($All(oDoc, 'link[rel="stylesheet"][href]'), (elm) => (elm));
const olinks = Array.prototype.map.call($All('link[rel="stylesheet"][href]'), (elm) => (elm));
for (const link of links) {
if (!link.href.startsWith('http')) {continue;}
for (const olink of Array.prototype.filter.call(olinks, (l) => (l.href === link.href))) {olink.parentElement.removeChild(olink);}
document.head.appendChild(link);
}
const scripts = Array.prototype.map.call($All(oDoc, 'script[src]'), (elm) => (elm));
for (const script of scripts) {
if (!script.src.startsWith('http')) {continue;}
if (Array.prototype.filter.call($All('script[src]'), (s) => (s.src === script.src)).length > 0) {continue;}
document.head.appendChild(script);
}
AM.finish();
});
// Load novel index
AM.add();
AndAPI.getNovelIndex({
aid: aid,
lang: language,
callback: function(xml) {
resource.xmlIndex = xml;
AM.finish();
}
});
AM.add();
AndAPI.getNovelShortInfo({
aid: aid,
lang: language,
callback: function(xml) {
resource.xmlInfo = xml;
AM.finish();
}
});
AM.finishEvent = true;
function fetchFinish() {
// Resources
const xmlIndex = resource.xmlIndex;
const xmlInfo = resource.xmlInfo;
const oDoc = resource.oDoc;
// Elements
const content = $('#content');
const table = $(content, 'table');
const tbody = $(table, 'tbody');
// Data
const name = $(xmlInfo, 'data[name="Title"]').childNodes[0].nodeValue;
const lastupdate = $(xmlInfo, 'data[name="LastUpdate"]').getAttribute('value');
const aBook = $(table, 'caption>a:first-child');
const charsets = ['gbk', 'utf-8', 'big5', 'gbk', 'utf-8', 'big5'];
const innerTexts = ['简体(G)', '简体(U)', '繁体(U)', '简体(G)', '简体(U)', '繁体(U)'];
// Set Title
document.title = TEXT_GUI_API_PACKSHOW_TITLE.replace('{N}', name);
// Set book
aBook.innerText = name;
aBook.href = URL_BOOKINTRO.replace('{A}', aid);
// Load book index
loadIndex();
// Soft alert
alertify.success(TEXT_TIP_API_PACKSHOW_LOADED);
// Enter common download page enhance
pageDownload();
// Book index loader
function loadIndex() {
switch (type) {
case 'txt':
loadIndex_txt();
break;
case 'txtfull':
loadIndex_txtfull();
break;
case 'umd':
loadIndex_umd();
break;
case 'jar':
loadIndex_jar();
break;
}
}
// Book index loader for type txt
function loadIndex_txt() {
// Clear tbody trs
for (const tr of $All(table, 'tr+tr')) {
tbody.removeChild(tr);
}
// Make new trs
for (const volume of $All(xmlIndex, 'volume')) {
const tr = makeTr(volume);
tbody.appendChild(tr);
}
function makeTr(volume) {
const tr = $CrE('tr');
const [tdName, td1, td2] = [$CrE('td'), $CrE('td'), $CrE('td')];
const a = Array(6);
const vid = volume.getAttribute('vid');
const vname = volume.childNodes[0].nodeValue;
// init tds
tdName.classList.add('odd');
td1.classList.add('even');
td2.classList.add('even');
td1.align = td2.align = 'center';
// Set volume name
tdName.innerText = vname;
// Make links
for (let i = 0; i < a.length; i++) {
a[i] = $CrE('a');
a[i].target = '_blank';
a[i].href = 'http://dl.wenku8.com/packtxt.php?aid=' + aid +
'&vid=' + vid +
(i >= 3 ? '&aname=' + $URL.encode(name) : '') +
(i >= 3 ? '&vname=' + $URL.encode(vname) : '') +
'&charset=' + charsets[i];
a[i].innerText = innerTexts[i];
(i < 3 ? td1 : td2).appendChild(a[i]);
}
// Insert whitespace textnode
for (const i of [1, 2, 4, 5]) {
(i < 3 ? td1 : td2).insertBefore(document.createTextNode('\n'), a[i]);
}
tr.appendChild(tdName);
tr.appendChild(td1);
tr.appendChild(td2);
return tr;
}
}
// Book index loader for type txtfull
function loadIndex_txtfull() {
const tr = $(tbody, 'tr+tr');
const tds = Array.prototype.map.call(tr.children, (elm) => (elm));
tds[0].innerText = lastupdate;
tds[1].innerText = TEXT_GUI_UNKNOWN;
for (const a of $All(tds[2], 'a')) {
a.href = a.href.replace(/id=\d+/, 'id='+aid).replace(/fname=[^&]+/, 'fname='+$URL.encode(name));
}
}
// Book index loader for type umd
function loadIndex_umd() {
const tr = $(tbody, 'tr+tr');
const tds = toArray(tr.children);
tds[0].innerText = tds[1].innerText = TEXT_GUI_UNKNOWN;
tds[2].innerText = lastupdate;
tds[3].innerText = $(xmlIndex, 'volume:first-child').childNodes[0].nodeValue + '—' + $(xmlIndex, 'volume:last-child').childNodes[0].nodeValue;
const as = [].concat(toArray($All(tds[4], 'a'))).concat(toArray($All(table, 'caption>a+a')));
for (const a of as) {
a.href = a.href.replace(/id=\d+/, 'id='+aid);
}
}
// Book index loader for type jar
function loadIndex_jar() {
// Currently type jar is the same as type umd
loadIndex_umd();
}
function toArray(_arr) {
return Array.prototype.map.call(_arr, (elm) => (elm));
}
}
}
}
// Add a bottom-styled botton into bottom line, to the first place
function addBottomButton(details) {
const aClose = $('a[href="javascript:window.close()"]');
const bottom = aClose.parentElement;
const a = document.createElement('a');
const t1 = document.createTextNode('[');
const t2 = document.createTextNode(']');
const blank = document.createElement('span');
blank.innerHTML = ' ';
blank.style.width = '0.5em';
a.href = details.href;
a.innerHTML = details.innerHTML;
a.onclick = details.onclick;
[blank, t2, a, t1].forEach((elm) => {bottom.insertBefore(elm, bottom.childNodes[0]);});
}
}
// 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 = $All('.block');
const close = $All('a[href="javascript:window.close()"]');
return block.length === 1 && close.length === 1;
}
// Basic functions
// querySelector
function $() {
switch(arguments.length) {
case 2:
return arguments[0].querySelector(arguments[1]);
break;
default:
return document.querySelector(arguments[0]);
}
}
// querySelectorAll
function $All() {
switch(arguments.length) {
case 2:
return arguments[0].querySelectorAll(arguments[1]);
break;
default:
return document.querySelectorAll(arguments[0]);
}
}
// createElement
function $CrE() {
switch(arguments.length) {
case 2:
return arguments[0].createElement(arguments[1]);
break;
default:
return document.createElement(arguments[0]);
}
}
// Send reply for bookreview
// Arg: {rid, title, content, onload:(oDoc)=>{}, onerror:()=>{}}
function sendReviewReply(detail) {
if (typeof($URL) !== 'object') {
DoLog(LogLevel.Error, 'sendReviewReply: $URL not found.');
return false;
}
const data = '&ptitle=' + $URL.encode(detail.title) + '&pcontent=' + $URL.encode(detail.content);
const url = 'https://www.wenku8.net/modules/article/reviewshow.php?rid=' + detail.rid.toString();
GM_xmlhttpRequest({
method: 'POST',
url: url,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: data,
responseType: 'blob',
onload: function (response) {
if (!detail.onload) {return false;}
parseDocument(response.response, detail.onload);
},
onerror: function (e) {
detail.onerror && detail.onerror(e);
}
});
}
// Android API set
function AndriodAPI() {
const AA = this;
const DParser = new DOMParser();
const encode = AA.encode = function(str) {
return '&appver=1.13&request=' + btoa(str) + '&timetoken=' + (new Date().getTime());
};
const request = AA.request = function(details) {
const url = details.url;
const type = details.type || 'text';
const callback = details.callback || function() {};
const args = details.args || [];
GM_xmlhttpRequest({
method: 'POST',
url: 'http://app.wenku8.com/android.php',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': 'Dalvik/2.1.0 (Linux; U; Android 7.1.2; unknown Build/NZH54D)'
},
data: encode(url),
onload: function(e) {
let result;
switch (type) {
case 'xml':
result = DParser.parseFromString(e.responseText, 'text/xml');
break;
case 'text':
result = e.responseText;
break;
}
callback.apply(null, [result].concat(args));
},
onerror: function(e) {
DoLog(LogLevel.Error, 'AndriodAPI.request Error while requesting "' + url + '"');
DoLog(LogLevel.Error, e);
}
});
};
// aid, lang, callback, args
AA.getNovelShortInfo = function(details) {
const aid = details.aid;
const lang = details.lang;
const callback = details.callback || function() {};
const args = details.args || [];
const url = 'action=book&do=info&aid=' + aid + '&t=' + lang;
request({
url: url,
callback: callback,
args: args,
type: 'xml'
});
}
// aid, lang, callback, args
AA.getNovelIndex = function(details) {
const aid = details.aid;
const lang = details.lang;
const callback = details.callback || function() {};
const args = details.args || [];
const url = 'action=book&do=list&aid=' + aid + '&t=' + lang;
request({
url: url,
callback: callback,
args: args,
type: 'xml'
});
};
// aid, cid, lang, callback, args
AA.getNovelContent = function(details) {
const aid = details.aid;
const cid = details.cid;
const lang = details.lang;
const callback = details.callback || function() {};
const args = details.args || [];
const url = 'action=book&do=text&aid=' + aid + '&cid=' + cid + '&t=' + lang;
request({
url: url,
callback: callback,
args: args,
type: 'text'
});
};
}
// Create reply-area with enhanced UBBEditor
function makeEditor(parent, rid, aid) {
parent.innerHTML = ``.replace('{RID}', rid).replace('{AID}', aid);
const script = document.createElement('script');
script.innerHTML = 'loadJs("https://www.wenku8.net/scripts/ubbeditor_gbk.js", function(){UBBEditor.Create("pcontent");});';
$(parent, '#pcontent').parentElement.appendChild(script);
areaReply();
}
// getMyUserDetail with soft alerts
function refreshMyUserDetail(callback, args=[]) {
alertify.notify(TEXT_ALT_USRDTL_REFRESH);
getMyUserDetail(function() {
const alertBox = alertify.success(TEXT_ALT_USRDTL_REFRESHED);
// rewrite onclick function from copying to showing details
alertBox.callback = function(isClicked) {
isClicked && alertify.message(JSON.stringify(getMyUserDetail()));
}
// callback if exist
callback ? callback.apply(args) : function() {};
})
}
// Get my user info detail
// if no argument provided, this function will just read userdetail from gm_storage
// otherwise, the function will make a http request to get the latest userdetail
// if no argument provided and no gm_storage record, then will just return false
// if not logged in, return false
// if callback is not a function, then will just request&store but not callback
function getMyUserDetail(callback, args=[]) {
if (getUserID() === null) {
return false;
}
if (callback) {
requestWeb();
return true;
} else {
const storage = CONFIG.userDtlePrefs.getConfig();
if (!storage.userDetail && !storage.userFriends) {
DoLog(LogLevel.Warning, 'Attempt to read userDetail from gm_storage but no record found');
return false;
};
const userDetail = storage;
return userDetail;
}
function requestWeb() {
const lastStorage = CONFIG ? CONFIG.userDtlePrefs.getConfig() : undefined;
let restXHR = 2;
let storage = {};
// Request userDetail
getDocument(URL_USRDETAIL, detailLoaded)
// Request userFriends
getDocument(URL_USRFRIEND, friendLoaded)
function detailLoaded(oDoc) {
const content = $(oDoc, '#content');
storage.userDetail = {
userID: Number($(content, 'tr:nth-child(1)>.even').innerText), // '用户ID'
userLink: $(content, 'tr:nth-child(2)>.even').innerText, // '推广链接'
userName: $(content, 'tr:nth-child(3)>.even').innerText, // '用户名'
displayName: $(content, 'tr:nth-child(4)>.even').innerText, // '用户昵称'
userType: $(content, 'tr:nth-child(5)>.even').innerText, // '等级'
userGrade: $(content, 'tr:nth-child(6)>.even').innerText, // '头衔'
gender: $(content, 'tr:nth-child(7)>.even').innerText, // '性别'
email: $(content, 'tr:nth-child(8)>.even').innerText, // 'Email'
qq: $(content, 'tr:nth-child(9)>.even').innerText, // 'QQ'
msn: $(content, 'tr:nth-child(10)>.even').innerText, // 'MSN'
site: $(content, 'tr:nth-child(11)>.even').innerText, // '网站'
signupDate: $(content, 'tr:nth-child(13)>.even').innerText, // '注册日期'
contibute: $(content, 'tr:nth-child(14)>.even').innerText, // '贡献值'
exp: $(content, 'tr:nth-child(15)>.even').innerText, // '经验值'
credit: $(content, 'tr:nth-child(16)>.even').innerText, // '现有积分'
friends: $(content, 'tr:nth-child(17)>.even').innerText, // '最多好友数'
mailbox: $(content, 'tr:nth-child(18)>.even').innerText, // '信箱最多消息数'
bookcase: $(content, 'tr:nth-child(19)>.even').innerText, // '书架最大收藏量'
vote: $(content, 'tr:nth-child(20)>.even').innerText, // '每天允许推荐次数'
sign: $(content, 'tr:nth-child(22)>.even').innerText, // '用户签名'
intoduction: $(content, 'tr:nth-child(23)>.even').innerText, // '个人简介'
userImage: $(content, 'tr>td>img').src // '头像'
}
loaded();
}
function friendLoaded(oDoc) {
const content = $(oDoc, '#content');
const trs = $(content, 'tr');
const friends = [];
const lastFriends = lastStorage ? lastStorage.userFriends : undefined;
for (let i = 1; i < trs.length; i++) {
getFriends(trs[i]);
}
storage.userFriends = friends;
loaded();
function getFriends(tr) {
// Check if userID exist
if (isNaN(Number($(tr.children[2], 'a').href.match(/\?uid=(\d+)/)[1]))) {return false;};
// Collect information
let friend = {
userID: Number($(tr.children[2], 'a').href.match(/\?uid=(\d+)/)[1]),
userName: tr.children[0].innerText,
signupDate: tr.children[1].innerText
}
friend = fillLocalInfo(friend)
friends.push(friend);
}
function fillLocalInfo(friend) {
if (!lastFriends) {return friend;};
for (const f of lastFriends) {
if (f.userID === friend.userID) {
for (const [key, value] of Object.entries(f)) {
if (friend.hasOwnProperty(key)) {continue;};
friend[key] = value;
}
break;
}
}
return friend;
}
}
function loaded() {
restXHR--;
if (restXHR === 0) {
// Save to gm_storage
if (CONFIG) {
storage.lasttime = getTime('-', false);
CONFIG.userDtlePrefs.saveConfig(storage);
}
// Callback
typeof(callback) === 'function' ? callback.apply(null, [storage].concat(args)) : function() {};
}
}
}
}
function exportConfig(noPass=false) {
// Get config
const config = {};
const getValue = window.getValue ? window.getValue : GM_getValue;
const listValues = window.listValues ? window.listValues : GM_listValues;
for (const key of listValues()) {
config[key] = getValue(key);
}
// Remove username and password if required
noPass && (config[KEY_CM].users = {});
// Download
const text = JSON.stringify(config);
const name = '轻小说文库+_配置文件({P})_v{V}_{T}.wkp'.replace('{P}', noPass ? '无账号密码' : '含账号密码').replace('{V}', GM_info.script.version).replace('{T}', getTime());
downloadText(text, name);
alertify.success(TEXT_ALT_CONFIG_EXPORTED.replace('{N}', name));
}
function importConfig(json) {
// Redirect
redirectGMStorage();
// Preserve users
const users = GM_getValue('Config-Manager').users;
// Delete json
for (const [key, value] of GM_listValues()) {
GM_deleteValue(key, value);
}
// Set json
for (const [key, value] of Object.entries(json)) {
GM_setValue(key, value);
}
// Preserve users
const config = GM_getValue('Config-Manager', {});
if (!config.users) {config.users = {}}
for (const [name, user] of Object.entries(users)) {
config.users[name] = user;
}
GM_setValue('Config-Manager', config);
// Reload
location.reload();
}
function getUserID() {
const match = $URL.decode(document.cookie).match(/jieqiUserId=(\d+)/);
const id = match && match[1] ? Number(match[1]) : null;
return isNaN(id) ? null : id;
}
function getUserName() {
const match = $URL.decode(document.cookie).match(/jieqiUserName=([^, ;]+)/);
const name = match ? match[1] : null;
return name;
}
// Reload page without re-sending form data, and keeps reviewshow-page
function reloadPage() {
const url = /^https?:\/\/www\.wenku8\.net\/modules\/article\/reviewshow\.php/.test(location.href) ? URL_REVIEWSHOW_2.replace('{R}', getUrlArgv('rid')).replace('{P}', $('#pagelink>strong').innerText) : location.href;
location.href = url;
}
// 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;
}
}
}
// New tipobj movement method. Makes sure the tipobj stay close with the mouse.
function tipscroll() {
if (!tipready) {return false;}
DoLog('tipscroll executed. ')
tipobj.style.position = 'fixed';
document.onmousemove = unsafeWindow.tipmove = function tipmoveplus(e) {
tipobj.style.left = e.clientX + tipx + 'px';
tipobj.style.top = e.clientY + tipy + 'px';
}
return true;
}
// show & hide tip when mouse in & out. accepts tip as a string or a function that returns the tip string
function settip(elm, tip) {
typeof(tip) === 'string' && (elm.tiptitle = tip);
typeof(tip) === 'function' && (elm.tipgetter = tip);
elm.removeEventListener('mouseover', showtip);
elm.removeEventListener('mouseout', hidetip);
elm.addEventListener('mouseover', showtip);
elm.addEventListener('mouseout', hidetip);
}
function showtip(e) {
if (e && e.target && (e.target.tiptitle || e.target.tipgetter)) {
const tip = e.target.tiptitle || e.target.tipgetter();
if (tipready) {
tipshow(tip);
e.target.title && e.target.removeAttribute('title');
} else {
e.target.title = e.target.tiptitle;
}
} else if (typeof(e) === 'string') {
tipready && tipshow(e);
}
}
function hidetip() {
tipready && tiphide();
}
// Side-located control panel
// Require: FontAwesome, tooltip.css(from https://github.com/felipefialho/css-components/blob/main/build/tooltip/tooltip.css)
// Use 'new' keyword
function SidePanel() {
// Public SP
const SP = this;
const elms = SP.elements = {};
// Private _SP
// keys start with '_' shouldn't be modified
const _SP = {
_id: {
css: 'sidepanel-style',
usercss: 'sidepanel-style-user',
panel: 'sidepanel-panel'
},
_class: {
button: 'sidepanel-button'
},
_css: '#sidepanel-panel {position: fixed; background-color: #00000000; padding: 0.5vmin; line-height: 3.5vmin; height: auto; display: flex; transition-duration: 0.3s; z-index: 9999999999;} #sidepanel-panel.right {right: 3vmin;} #sidepanel-panel.bottom {bottom: 3vmin; flex-direction: column-reverse;} #sidepanel-panel.left {left: 3vmin;} #sidepanel-panel.top {top: 3vmin; flex-direction: column;} .sidepanel-button {padding: 1vmin; margin: 0.5vmin; font-size: 3.5vmin; border-radius: 10%; text-align: center; color: #00000088; background-color: #FFFFFF88; box-shadow:3px 3px 2px #00000022; user-select: none; transition-duration: inherit;} .sidepanel-button:hover {color: #FFFFFFDD; background-color: #000000DD;}',
_directions: ['left', 'right', 'top', 'bottom']
};
Object.defineProperty(SP, 'css', {
configurable: false,
enumerable: true,
get: () => (_SP.css),
set: (css) => {
_SP.css = css;
spAddStyle(css, _SP._id.css);
}
});
Object.defineProperty(SP, 'usercss', {
configurable: false,
enumerable: true,
get: () => (_SP.usercss),
set: (usercss) => {
_SP.usercss = usercss;
spAddStyle(usercss, _SP._id.usercss);
}
});
SP.css = _SP._css;
SP.create = function() {
// Create panel
const panel = elms.panel = document.createElement('div');
panel.id = _SP._id.panel;
SP.setPosition('bottom-right');
document.body.appendChild(panel);
// Prepare buttons
elms.buttons = [];
}
// Insert a button to given index
// details = {index, text, faicon, id, tip, className, onclick, listeners}, all optional
// listeners = [..[..args]]. [..args] will be applied as button.addEventListener's args
// faicon = 'fa-icon-name-classname fa-icon-style-classname', this arg stands for a FontAwesome icon to be inserted inside the botton
// Returns the button(HTMLDivElement), including button.faicon(HTMLElement/HTMLSpanElement in firefox, ) if faicon is set
SP.insert = function(details) {
const index = details.index;
const text = details.text;
const faicon = details.faicon;
const id = details.id;
const tip = details.tip;
const className = details.className;
const onclick = details.onclick;
const listeners = details.listeners || [];
const button = document.createElement('div');
text && (button.innerHTML = text);
id && (button.id = id);
tip && setTooltip(button, tip); //settip(button, tip);
className && (button.className = className);
onclick && (button.onclick = onclick);
if (faicon) {
const i = document.createElement('i');
i.className = faicon;
button.faicon = i;
button.appendChild(i);
}
for (const listener of listeners) {
button.addEventListener.apply(button, listener);
}
button.classList.add(_SP._class.button);
elms.buttons = insertItem(elms.buttons, button, index);
index < elms.buttons.length ? elms.panel.insertBefore(button, elms.panel.children[index]) : elms.panel.appendChild(button);
return button;
}
// Append a button
SP.add = function(details) {
details.index = elms.buttons.length;
return SP.insert(details);
}
// Remove a button
SP.remove = function(arg) {
let index, elm;
if (arg instanceof HTMLElement) {
elm = arg;
index = elms.buttons.indexOf(elm);
} else if (typeof(arg) === 'number') {
index = arg;
elm = elms.buttons[index];
} else if (typeof(arg) === 'string') {
elm = $(elms.panel, arg);
index = elms.buttons.indexOf(elm);
}
elms.buttons = delItem(elms.buttons, index);
elm.parentElement.removeChild(elm);
}
// Sets the display position by texts like 'right-bottom'
SP.setPosition = function(pos) {
const poses = _SP.direction = pos.split('-');
const avails = _SP._directions;
// Available check
if (poses.length !== 2) {return false;}
for (const p of poses) {
if (!avails.includes(p)) {return false;}
}
// remove all others
for (const p of avails) {
elms.panel.classList.remove(p);
}
// add new pos
for (const p of poses) {
elms.panel.classList.add(p);
}
// Change tooltips' direction
elms.buttons && elms.buttons.forEach(function(button) {
if (button.getAttribute('role') === 'tooltip') {
setTooltipDirection(button)
}
});
}
// Gets the current display position
SP.getPosition = function() {
return _SP.direction.join('-');
}
// Append a style text to document() with a |