content Element
function getFloorContent(contentEle) {
const subNodes = contentEle.childNodes;
let content = '';
for (const node of subNodes) {
const type = node.nodeName;
switch (type) {
case '#text':
// Prevent 'Quote:' repeat
content += node.data.replace(/^\s*Quote:\s*$/, ' ');
break;
case 'IMG':
content += '[img]S[/img]'.replace('S', node.src);
break;
case 'A':
content += '[url=U]T[/url]'.replace('U', node.href).replace('T', getFloorContent(node));
break;
case 'BR':
// no need to add \n, because \n will be preserved in #text nodes
//content += '\n';
break;
case 'DIV':
if (node.classList.contains('jieqiQuote')) {
content += getTagedSubcontent('quote', node);
} else if (node.classList.contains('jieqiCode')) {
content += getTagedSubcontent('code', node);
} else if (node.classList.contains('divimage')) {
content += getFloorContent(node);
} else {
content += getFloorContent(node);
}
break;
case 'CODE': content += getFloorContent(node); break; // Just ignore
case 'PRE': content += getFloorContent(node); break; // Just ignore
case 'SPAN': content += getFontedSubcontent(node); break; // Size and color
case 'P': content += 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); break;
/*
case 'SPAN':
subContent = getFloorContent(node);
size = node.style.fontSize.match(/\d+/) ? node.style.fontSize.match(/\d+/)[0] : '';
color = node.style.color.match(/rgb\((\d+), ?(\d+), ?(\d+)\)/);
break;
*/
}
}
return content;
function getTagedSubcontent(tag, node) {
const subContent = getFloorContent(node);
return '[{T}]{S}[/{T}]'.replaceAll('{T}', tag).replaceAll('{S}', subContent);
}
function getFontedSubcontent(node) {
let 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);
if (tag && value) {
return '[{T}={V}]{S}[/{T}]'.replaceAll('{T}', tag).replaceAll('{V}', value).replaceAll('{S}', subContent);
} else {
return subContent;
}
}
}
}
// 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 = 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 = 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.querySelector('.ulitem');
const txtst = block.querySelector('#arstatus');
const btnAR = block.querySelector('#autorcmmd');
const btnRN = block.querySelector('#rcmmdnow');
const txtAR = btnAR.querySelector('span');
const checkbox = document.createElement('input');
txtst.classList.add(CLASSNAME_TEXT);
btnAR.classList.add(CLASSNAME_BUTTON);
btnRN.classList.add(CLASSNAME_BUTTON);
checkbox.type = 'checkbox';
checkbox.checked = arConfig.auto;
checkbox.addEventListener('click', onclick);
btnAR.addEventListener('click', onclick);
btnAR.appendChild(checkbox);
btnRN.addEventListener('click', rcmmdnow);
function onclick(e) {
e.preventDefault();
e.stopPropagation();
arConfig.auto = !arConfig.auto;
setTimeout(function() {checkbox.checked = arConfig.auto;}, 0);
CONFIG.AutoRecommend.saveConfig(arConfig);
new ElegantAlertBox(arConfig.auto ? TEXT_ALT_ATRCMMDS_AUTO : TEXT_ALT_ATRCMMDS_NOAUTO);
}
function rcmmdnow() {
if (TASK.AutoRecommend.checkRcmmd() && !confirm(TEXT_GUI_BOOKCASE_RCMMDNW_CONFIRM)) {return false;}
if (arConfig.allCount === 0) {new ElegantAlertBox(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 = document.querySelector('#checkform') ? document.querySelector('#checkform') : document.querySelector('.'+CLASSNAME_BOOKCASE_FORM);
const oriTitle = checkform.querySelector('div.gridtop');
const topTitle = oriTitle.cloneNode(true);
content.insertBefore(topTitle, checkform);
// Hide bookcase selector
const bcSelector = topTitle.querySelector('[name="classlist"]');
bcSelector.style.display = 'none';
// Write title text
const textNode = topTitle.childNodes[0];
const numMatch = textNode.nodeValue.match(/\d+/g);
const text = TEXT_GUI_BOOKCASE_TOPTITLE.replace('A', numMatch[0]).replace('B', numMatch[1]);
textNode.nodeValue = text;
}
function showBookcases() {
// GUI
const topTitle = content.querySelector('script+div.gridtop');
const textNode = topTitle.childNodes[0];
const oriTitleText = textNode.nodeValue;
const allCount = bookcases.length;
let finished = 1;
textNode.nodeValue = TEXT_GUI_BOOKCASE_GETTING.replace('C', String(finished)).replace('A', String(allCount));
// Get all bookcase pages
for (const bookcase of bookcases) {
if (bookcase.classid === curClassid) {continue;};
getDocument(bookcase.url, appendBookcase, [bookcase]);
}
function appendBookcase(mDOM, bookcase) {
const classid = bookcase.classid;
// Get bookcase form and modify it
const form = mDOM.querySelector('#checkform');
form.parentElement.removeChild(form);
// Find the right place to insert it in
const forms = content.querySelectorAll('.'+CLASSNAME_BOOKCASE_FORM);
for (let i = 0; i < forms.length; i++) {
const thisForm = forms[i];
const cid = thisForm.classid ? thisForm.classid : curClassid;
if (cid > classid) {
content.insertBefore(form, thisForm);
break;
}
}
if(!form.parentElement) {content.appendChild(form);};
// Decorate
decorateForm(form, bookcase);
// finished increase
finished++;
textNode.nodeValue = finished < allCount ?
TEXT_GUI_BOOKCASE_GETTING.replace('C', String(finished)).replace('A', String(allCount)) :
oriTitleText;
}
}
function decorateForm(form, bookcase) {
const classid = bookcase.classid;
let name = bookcase.name;
// Provide auto-recommand button
arBtn();
// Modify properties
form.classList.add(CLASSNAME_BOOKCASE_FORM);
form.id += String(classid);
form.classid = classid;
form.onsubmit = my_check_confirm;
// Hide bookcase selector
const bcSelector = form.querySelector('[name="classlist"]');
bcSelector.style.display = 'none';
// 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.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.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.querySelector('table');
for (const tr of table.querySelectorAll('tr')) {
tr.querySelector('.odd') ? decorateRow(tr) : function() {};
tr.querySelector('th') ? decorateHeader(tr) : function() {};
tr.querySelector('td.foot') ? decorateFooter(tr) : function() {};
}
// Insert auto-recommend option for given row
function decorateRow(tr) {
const eleBookLink = tr.querySelector('td:nth-child(2)>a');
const strBookID = eleBookLink.href.match(/aid=(\d+)/)[1];
const strBookName = eleBookLink.innerText;
const newTd = document.createElement('td');
const input = document.createElement('input');
newTd.classList.add('odd');
input.type = 'number';
input.inputmode = 'numeric';
input.style.width = '85%';
input.value = arConfig.books[strBookID] ? String(arConfig.books[strBookID].number) : '0';
input.addEventListener('change', onvaluechange);
input.strBookID = strBookID; input.strBookName = strBookName;
newTd.appendChild(input); tr.appendChild(newTd);
}
// Insert a new row for auto-recommend options
function decorateHeader(tr) {
const allTh = tr.querySelectorAll('th');
const width = ARR_GUI_BOOKCASE_WIDTH;
const newTh = document.createElement('th');
newTh.innerText = TEXT_GUI_BOOKCASE_ATRCMMD;
newTh.classList.add(CLASSNAME_TEXT);
tr.appendChild(newTh);
for (let i = 0; i < allTh.length; i++) {
const th = allTh[i];
th.style.width = width[i];
}
}
// Fit the width
function decorateFooter(tr) {
const td = tr.querySelector('td.foot');
td.colSpan = ARR_GUI_BOOKCASE_WIDTH.length;
}
// auto-recommend onvaluechange
function onvaluechange(e) {
arConfig = 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
new ElegantAlertBox(
TEXT_ALT_ATRCMMDS_SAVED
.replaceAll('{B}', strBookName)
.replaceAll('{N}', value)
.replaceAll('{R}', userDetail.vote-arConfig.allCount)
);
if (userDetail && arConfig.allCount > userDetail.vote) {
const alertBox = new ElegantAlertBox(
TEXT_ALT_ATRCMMDS_OVERFLOW
.replace('{V}', String(userDetail.vote))
.replace('{C}', String(arConfig.allCount))
);
alertBox.elm.onclick = function() {
alertBox.close.call(alertBox);
refreshMyUserDetail();
}
};
} else {
// invalid input value, alert
new ElegantAlertBox(TEXT_ALT_ATRCMMDS_INVALID.replaceAll('{N}', value));
}
}
}
}
}
// Novel ads remover
function removeTopAds() {
const ads = []; document.querySelectorAll('div>script+script+a').forEach(function(a) {ads.push(a.parentElement);});
for (const ad of ads) {
ad.parentElement.removeChild(ad);
}
}
// Novel index page add-on
function pageNovelIndex() {
removeTopAds();
}
// Novel page add-on
function pageNovel() {
const pageResource = {elements: {}, infos: {}, download: {}};
collectPageResources(); DoLog(LogLevel.Info, pageResource, true)
// Remove ads
removeTopAds();
// Provide download GUI
downloadGUI();
// Prevent URL.revokeObjectURL in script 轻小说文库下载
revokeObjectURLHOOK();
function collectPageResources() {
collectElements();
collectInfos();
initDownload();
function collectElements() {
const elements = pageResource.elements;
elements.title = document.querySelector('#title');
elements.images = document.querySelectorAll('.imagecontent');
elements.rightButtonDiv = document.querySelector('#linkright');
elements.rightNodes = elements.rightButtonDiv.childNodes;
elements.rightBlank = elements.rightNodes[elements.rightNodes.length-1];
elements.content = document.querySelector('#content');
elements.spliterDemo = document.createTextNode(' | ');
}
function collectInfos() {
const elements = pageResource.elements;
const infos = pageResource.infos;
infos.title = elements.title.innerText;
infos.isImagePage = elements.images.length > 0;
infos.content = infos.isImagePage ? null : elements.content.innerText;
}
function initDownload() {
const elements = pageResource.elements;
const download = pageResource.download;
download.running = false;
download.finished = 0;
download.all = elements.images.length;
download.error = 0;
}
}
// Prevent URL.revokeObjectURL in script 轻小说文库下载
function revokeObjectURLHOOK() {
const Ori_revokeObjectURL = URL.revokeObjectURL;
URL.revokeObjectURL = function(arg) {
if (typeof(arg) === 'string' && arg.substr(0, 5) === 'blob:') {return false;};
return Ori_revokeObjectURL(arg);
}
}
// Provide download GUI
function downloadGUI() {
const elements = pageResource.elements;
const infos = pageResource.infos;
const download = pageResource.download;
// Create donwload button
const dlBtn = elements.downloadBtn = document.createElement('span');
dlBtn.classList.add(CLASSNAME_BUTTON);
dlBtn.addEventListener('click', infos.isImagePage ? dlNovelImages : dlNovelText);
dlBtn.innerText = infos.isImagePage ? TEXT_GUI_DOWNLOAD_IMAGE : TEXT_GUI_DOWNLOAD_TEXT;
// Create spliter
const spliter = elements.spliterDemo.cloneNode();
// Append to rightButtonDiv
elements.rightButtonDiv.style.width = '550px';
elements.rightButtonDiv.insertBefore(spliter, elements.rightBlank);
elements.rightButtonDiv.insertBefore(dlBtn, elements.rightBlank);
function dlNovelImages() {
if (download.running) {return false;};
download.running = true; download.finished = 0; download.error = 0;
updateDownloadStatus();
const lenNumber = String(elements.images.length).length;
for (let i = 0; i < elements.images.length; i++) {
const img = elements.images[i];
const name = infos.title + '_' + fillNumber(i+1, lenNumber) + '.jpg';
GM_xmlhttpRequest({
url: img.src,
responseType: 'blob',
onloadstart: function() {
DoLog(LogLevel.Info, '[' + String(i) + ']downloading novel image from ' + img.src);
},
onload: function(e) {
DoLog(LogLevel.Info, '[' + String(i) + ']image got: ' + img.src);
const image = new Image();
image.onload = function() {
const url = toImageFormatURL(image, 1);
DoLog(LogLevel.Info, '[' + String(i) + ']image transformed: ' + img.src);
const a = document.createElement('a');
a.href = url;
a.download = name;
a.click();
download.finished++;
updateDownloadStatus();
// Code below seems can work, but actually it doesn't work well and somtimes some file cannot be saved
// The reason is still unknown, but from what I know I can tell that mistakes happend in GM_xmlhttpRequest
// Error stack: GM_xmlhttpRequest.onload ===> image.onload ===> downloadFile ===> GM_xmlhttpRequest =X=> .onload
// This Error will also stuck the GMXHRHook.ongoingList
/*downloadFile({
url: url,
name: name,
onload: function() {
download.finished++;
DoLog(LogLevel.Info, '[' + String(i) + ']file saved: ' + name);
alert('[' + String(i) + ']file saved: ' + name);
updateDownloadStatus();
},
onerror: function() {
alert('downloadfile error! url = ' + String(url) + ', i = ' + String(i));
}
})*/
}
image.onerror = function() {
alert('image load error! image.src = ' + String(image.src) + ', i = ' + String(i));
}
image.src = URL.createObjectURL(e.response);
},
onerror: function(e) {
// Error dealing need...
DoLog(LogLevel.Error, '[' + String(i) + ']image fetch error: ' + img.src);
download.error++;
}
})
}
function updateDownloadStatus() {
elements.downloadBtn.innerText = TEXT_GUI_DOWNLOADING_ALL.replaceAll('C', String(download.finished)).replaceAll('A', String(download.all));
if (download.finished === download.all) {
DoLog(LogLevel.Success, 'All images got.');
elements.downloadBtn.innerText = TEXT_GUI_DOWNLOADED_ALL;
download.running = false;
}
}
}
function dlNovelText() {
const name = infos.title + '.txt';
const text = infos.content.replaceAll(/[\r\n]+/g, '\r\n');
downloadText(text, name);
}
}
// Image format changing function
// image:
![]()
or Image(); format: 1 for jpeg, 2 for png, 3 for webp
function toImageFormatURL(image, format) {
if (typeof(format) === 'number') {format = ['image/jpeg', 'image/png', 'image/webp'][format-1]}
const cvs = document.createElement('canvas');
cvs.width = image.width;
cvs.height = image.height;
const ctx = cvs.getContext('2d');
ctx.drawImage(image, 0, 0);
return cvs.toDataURL(format);
}
}
// Search form add-on
function formSearch() {
const searchForm = document.querySelector('form[name="articlesearch"]');
if (!searchForm) {return false;};
const typeSelect = searchForm.querySelector('#searchtype');
const searchText = searchForm.querySelector('#searchkey');
const searchSbmt = searchForm.querySelector('input[class="button"][type="submit"]');
let optionTags;
provideTagOption();
onsubmitHOOK();
function provideTagOption() {
optionTags = document.createElement('option');
optionTags.value = VALUE_STR_NULL;
optionTags.innerText = TEXT_GUI_SEARCH_OPTION_TAG;
typeSelect.appendChild(optionTags);
if (tipready) {
// tipshow and tiphide is coded inside wenku8 itself, its function is to show a text tip besides the mouse
typeSelect.addEventListener('mouseover', show);
searchSbmt.addEventListener('mouseover', show);
typeSelect.addEventListener('mouseout' , tiphide);
searchSbmt.addEventListener('mouseout' , tiphide);
} else {
typeSelect.title = TEXT_TIP_SEARCH_OPTION_TAG;
searchSbmt.title = TEXT_TIP_SEARCH_OPTION_TAG;
}
function show() {
optionTags.selected ? tipshow(TEXT_TIP_SEARCH_OPTION_TAG) : function() {};
}
}
function onsubmitHOOK() {
const onsbmt = searchForm.onsubmit;
searchForm.onsubmit = function() {
if (optionTags.selected) {
// DON'T USE window.open()!
// Wenku8 has no window.open used in its own scripts, so do not use it in userscript either.
// It might cause security problems.
//window.open('https://www.wenku8.net/modules/article/tags.php?t=' + $URL.encode(searchText.value));
if (typeof($URL) === 'undefined' ) {
$URLError();
return true;
} else {
GM_openInTab(URL_TAGSEARCH.replace('{TU}', $URL.encode(searchText.value)), {
active: true, insert: true, setParent: true, incognito: false
});
return false;
}
}
}
function $URLError() {
DoLog(LogLevel.Error, '$URL(from gbk.js) is not loaded.');
DoLog(LogLevel.Warning, 'Search as plain text instead.');
// Search as plain text instead
for (const node of typeSelect.childNodes) {
node.selected = (node.tagName === 'OPTION' && node.value === 'articlename') ? true : false;
}
}
}
}
// Tags page add-on
function pageTags() {
}
// User page add-on
function pageUser() {
const UID = Number(getUrlArgv('uid'));
// Provide review search option
reviewButton();
// Review search option
function reviewButton() {
// clone button and container div
const oriContainer = document.querySelectorAll('.blockcontent .userinfo')[0].parentElement;
const container = oriContainer.cloneNode(true);
const button = container.querySelector('a');
button.innerText = TEXT_GUI_USER_REVIEWSEARCH;
button.href = URL_REVIEWSEARCH.replaceAll('{K}', String(UID));
oriContainer.parentElement.appendChild(container);
}
}
// Index page add-on
function pageIndex() {
showFavorites();
// Show favorite reviews
function showFavorites() {
const links = [];
const favorites = CONFIG.BkReviewPrefs.getConfig().favorites;
for (const [rid, favorite] of Object.entries(favorites)) {
links.push({
innerHTML: favorite.name.substr(0, 12), // prevent overflow
tiptitle: favorite.tiptitle ? favorite.tiptitle : favorite.href,
href: favorite.href
});
}
const block = createLeftBlock(TEXT_GUI_INDEX_FAVORITES, true, {
type: 'toplist',
links: links
})
}
}
// 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 + '/');
}
}
// Login page add-on
function pageLogin() {
const form = document.querySelector('form[name="frmlogin"]');
if (!form) {return false;}
const eleUsername = form.querySelector('input.text[name="username"]');
const elePassword = form.querySelector('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 (!document.querySelector('.fl')) {return false;};
GUI();
function GUI() {
// Add switch select
const eleTopLeft = document.querySelector('.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, '');
if (tipready) {
sltSwitch.addEventListener('mouseover', ()=>{tipshow(TEXT_TIP_ACCOUNT_NOACCOUNT);});
sltSwitch.addEventListener('mouseout', tiphide);
} else {
sltSwitch.title = 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();
e.preventDefault();
e.stopPropagation();
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 sltSwitch.querySelectorAll('option')) {
option.selected = getUserName().toLowerCase() === option.value.toLowerCase();
}
}
}
function switchAccount(username) {
// Logout
new ElegantAlertBox(TEXT_ALT_ACCOUNT_WORKING_LOGOFF);
GM_xmlhttpRequest({
method: 'GET',
url: URL_USRLOGOFF,
onload: function(response) {
// Login
new ElegantAlertBox(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 = new ElegantAlertBox(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.elm.onclick = () => {
clearTimeout(timeout);
box.close.call(box);
};
}
})
}
})
}
}
// isAPIPage page add-on
function pageAPI(API) {
DoLog(LogLevel.Info, 'This is wenku API page.');
DoLog(LogLevel.Info, 'API is: [' + API + ']');
DoLog(LogLevel.Info, 'There is nothing to do. Quiting...');
}
// Check if current page is an wenku API page ('处理成功', '出现错误!')
function isAPIPage() {
// API page has just one .block div and one close-page button
const block = document.querySelectorAll('.block');
const close = document.querySelectorAll('a[href="javascript:window.close()"]');
return block.length === 1 && close.length === 1;
}
// getMyUserDetail with soft alerts
function refreshMyUserDetail(callback, args=[]) {
new ElegantAlertBox(TEXT_ALT_USRDTL_REFRESH);
getMyUserDetail(function() {
const alertBox = new ElegantAlertBox(TEXT_ALT_USRDTL_REFRESHED);
// rewrite onclick function from copying to showing details
alertBox.elm.onclick = function() {
alertBox.close.call(alertBox);
new ElegantAlertBox(JSON.stringify(getMyUserDetail()));
}
// 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 callback is not a function, then will just request&store but not callback
function getMyUserDetail(callback, args=[]) {
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.querySelector('#content');
storage.userDetail = {
userID: Number(content.querySelector('tr:nth-child(1)>.even').innerText), // '用户ID'
userLink: content.querySelector('tr:nth-child(2)>.even').innerText, // '推广链接'
userName: content.querySelector('tr:nth-child(3)>.even').innerText, // '用户名'
displayName: content.querySelector('tr:nth-child(4)>.even').innerText, // '用户昵称'
userType: content.querySelector('tr:nth-child(5)>.even').innerText, // '等级'
userGrade: content.querySelector('tr:nth-child(6)>.even').innerText, // '头衔'
gender: content.querySelector('tr:nth-child(7)>.even').innerText, // '性别'
email: content.querySelector('tr:nth-child(8)>.even').innerText, // 'Email'
qq: content.querySelector('tr:nth-child(9)>.even').innerText, // 'QQ'
msn: content.querySelector('tr:nth-child(10)>.even').innerText, // 'MSN'
site: content.querySelector('tr:nth-child(11)>.even').innerText, // '网站'
signupDate: content.querySelector('tr:nth-child(13)>.even').innerText, // '注册日期'
contibute: content.querySelector('tr:nth-child(14)>.even').innerText, // '贡献值'
exp: content.querySelector('tr:nth-child(15)>.even').innerText, // '经验值'
credit: content.querySelector('tr:nth-child(16)>.even').innerText, // '现有积分'
friends: content.querySelector('tr:nth-child(17)>.even').innerText, // '最多好友数'
mailbox: content.querySelector('tr:nth-child(18)>.even').innerText, // '信箱最多消息数'
bookcase: content.querySelector('tr:nth-child(19)>.even').innerText, // '书架最大收藏量'
vote: content.querySelector('tr:nth-child(20)>.even').innerText, // '每天允许推荐次数'
sign: content.querySelector('tr:nth-child(22)>.even').innerText, // '用户签名'
intoduction: content.querySelector('tr:nth-child(23)>.even').innerText, // '个人简介'
userImage: content.querySelector('tr>td>img').src // '头像'
}
loaded();
}
function friendLoaded(oDoc) {
const content = oDoc.querySelector('#content');
const trs = content.querySelectorAll('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].querySelector('a').href.match(/\?uid=(\d+)/)[1]))) {return false;};
// Collect information
let friend = {
userID: Number(tr.children[2].querySelector('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 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;
}
// Check if tipobj is ready, if not, then make it
function tipcheck() {
DoLog(LogLevel.Info, 'checking tipobj...');
if (typeof(tipobj) === 'object' && tipobj !== null) {
DoLog(LogLevel.Info, 'tipobj ready...');
return true;
} else {
DoLog(LogLevel.Warning, 'tipobj not ready');
if (typeof(tipinit) === 'function') {
DoLog(LogLevel.Success, 'tipinit executed');
tipinit();
return true;
} else {
DoLog(LogLevel.Error, 'tipinit not found');
return false;
}
}
}
// Create a left .block operatingArea
// options = {type: '', ...opts}
// Supported type: 'mypage', 'toplist'
function createLeftBlock(title=TEXT_GUI_BLOCK_TITLE_DEFULT, append=false, options) {
const leftEle = document.querySelector('#left');
const blockEle = document.querySelector('#left>.block').cloneNode(true);
const titleEle = blockEle.querySelector('.blocktitle>.txt');
const cntntEle = blockEle.querySelector('.blockcontent');
titleEle.innerText = title;
clearChildnodes(cntntEle);
const type = options ? options.type.toLowerCase() : null;
switch (type.toLowerCase()) {
case 'mypage': typeMypage(); break;
case 'toplist': typeToplist(); break;
default: DoLog(LogLevel.Error, 'createLeftBlock: Invalid block type');
}
append && leftEle.appendChild(blockEle);
return blockEle;
// Links such as https://www.wenku8.net/userdetail.php
// options = {type: 'mypage', links = [...{href: '', innerHTML: '', tiptitle: '', id: ''}]}
function typeMypage() {
const ul = document.createElement('ul');
ul.classList.add('ulitem');
for (const link of options.links) {
const li = document.createElement('li');
const a = document.createElement('a');
a.href = link.href ? link.href : 'javascript: void(0);';
link.href && (a.target = '_blank');
link.tiptitle && (a.tiptitle = a.title = link.tiptitle);
tipready && a.addEventListener('mouseover', showtip);
tipready && a.addEventListener('mouseout', tiphide);
a.innerHTML = link.innerHTML;
a.id = link.id ? link.id : '';
li.appendChild(a);
ul.appendChild(li);
}
blockEle.appendChild(ul);
}
// Links such as top-books-list inside #right in index page
// options = {type: 'toplist', links = [...{href: '', innerHTML: '', tiptitle: '', id: ''}]}
function typeToplist() {
const ul = document.createElement('ul');
ul.classList.add('ultop');
for (const link of options.links) {
const li = document.createElement('li');
const a = document.createElement('a');
a.href = link.href ? link.href : 'javascript: void(0);';
link.href && (a.target = '_blank');
link.tiptitle && (tipready ? a.tiptitle = link.tiptitle : a.title = link.tiptitle);
tipready && a.addEventListener('mouseover', showtip);
tipready && a.addEventListener('mouseout', tiphide);
a.innerHTML = link.innerHTML;
a.id = link.id ? link.id : '';
li.appendChild(a);
ul.appendChild(li);
}
blockEle.appendChild(ul);
}
function showtip(e) {
tipready && e && tipshow(e.target.tiptitle);
}
}
// Get a review's last page url
function getLatestReviewPageUrl(rid, callback, args = []) {
const reviewUrl = 'https://www.wenku8.net/modules/article/reviewshow.php?rid=' + String(rid);
getDocument(reviewUrl, firstPage, args);
function firstPage(oDoc, ...args) {
const url = oDoc.querySelector('#pagelink>a.last').href;
args = [url].concat(args);
callback.apply(null, args);
};
};
// 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;
}
}
}
// Redirect GM_storage API
// Each key points to a different storage area
// Original GM_functions will be backuped in window object
// PS: No worry for GM_functions leaking, because Tempermonkey's Sandboxing
function redirectGMStorage(key) {
// Recover if redirected before
GM_setValue = typeof(window.setValue) === 'function' ? window.setValue : GM_setValue;
GM_getValue = typeof(window.getValue) === 'function' ? window.getValue : GM_getValue;
GM_listValues = typeof(window.listValues) === 'function' ? window.listValues : GM_listValues;
GM_deleteValue = typeof(window.deleteValue) === 'function' ? window.deleteValue : GM_deleteValue;
// Stop if no key
if (!key) {return;};
// Save original GM_functions
window.setValue = typeof(GM_setValue) === 'function' ? GM_setValue : function() {};
window.getValue = typeof(GM_getValue) === 'function' ? GM_getValue : function() {};
window.listValues = typeof(GM_listValues) === 'function' ? GM_listValues : function() {};
window.deleteValue = typeof(GM_deleteValue) === 'function' ? GM_deleteValue : function() {};
// Redirect GM_functions
typeof(GM_setValue) === 'function' ? GM_setValue = RD_GM_setValue : function() {};
typeof(GM_getValue) === 'function' ? GM_getValue = RD_GM_getValue : function() {};
typeof(GM_listValues) === 'function' ? GM_listValues = RD_GM_listValues : function() {};
typeof(GM_deleteValue) === 'function' ? GM_deleteValue = RD_GM_deleteValue : function() {};
// Get global storage
//const storage = getStorage();
function getStorage() {
return window.getValue(key, {});
}
function saveStorage(storage) {
return window.setValue(key, storage);
}
function RD_GM_setValue(key, value) {
const storage = getStorage();
storage[key] = value;
saveStorage(storage);
}
function RD_GM_getValue(key, defaultValue) {
const storage = getStorage();
return storage[key] || defaultValue;
}
function RD_GM_listValues() {
const storage = getStorage();
return Object.keys(storage);
}
function RD_GM_deleteValue(key) {
const storage = getStorage();
delete storage[key];
saveStorage(storage);
}
}
// Download and parse a url page into a html document(dom).
// when xhr onload: callback.apply([dom, args])
function getDocument(url, callback, args=[]) {
GM_xmlhttpRequest({
method : 'GET',
url : url,
responseType : 'blob',
onloadstart : function() {
DoLog(LogLevel.Info, 'getting document, url=\'' + url + '\'');
},
onload : function(response) {
const htmlblob = response.response;
const reader = new FileReader();
reader.onload = function(e) {
const htmlText = reader.result;
const dom = new DOMParser().parseFromString(htmlText, 'text/html');
args = [dom].concat(args);
callback.apply(null, args);
//callback(dom, htmlText);
}
reader.readAsText(htmlblob, 'GBK');
/* 注意!原来这里只是使用了DOMParser,DOMParser不像iframe加载Document一样拥有完整的上下文并执行所有element的功能,
** 只是按照HTML格式进行解析,所以在文库页面的GBK编码下仍然会按照UTF-8编码进行解析,导致中文乱码。
** 所以处理dom时不要使用ASC-II字符集以外的字符!
**
** 注:现在使用了FileReader来以GBK编码解析htmlText,故编码问题已经解决,可以正常使用任何字符
*/
}
})
}
// Save dataURL to file
function saveFile(dataURL, filename) {
const a = document.createElement('a');
a.href = dataURL;
a.download = filename;
a.click();
}
// File download function
// details looks like the detail of GM_xmlhttpRequest
// onload function will be called after file saved to disk
function downloadFile(details) {
if (!details.url || !details.name) {return false;};
// Configure request object
const requestObj = {
url: details.url,
responseType: 'blob',
onload: function(e) {
// Save file
saveFile(URL.createObjectURL(e.response), details.name);
// onload callback
details.onload ? details.onload(e) : function() {};
}
}
if (details.onloadstart ) {requestObj.onloadstart = details.onloadstart;};
if (details.onprogress ) {requestObj.onprogress = details.onprogress;};
if (details.onerror ) {requestObj.onerror = details.onerror;};
if (details.onabort ) {requestObj.onabort = details.onabort;};
if (details.onreadystatechange) {requestObj.onreadystatechange = details.onreadystatechange;};
if (details.ontimeout ) {requestObj.ontimeout = details.ontimeout;};
// Send request
GM_xmlhttpRequest(requestObj);
}
// Save text to textfile
function downloadText(text, name) {
if (!text || !name) {return false;};
// Get blob url
const blob = new Blob([text],{type:"text/plain;charset=utf-8"});
const url = URL.createObjectURL(blob);
// Create
and download
const a = document.createElement('a');
a.href = url;
a.download = name;
a.click();
}
// Load javascript from given url
function loadJS(url, callback, oDoc=document) {
var script = document.createElement('script'),
fn = callback || function () {};
script.type = 'text/javascript';
//IE
if (script.readyState) {
script.onreadystatechange = function () {
if (script.readyState == 'loaded' || script.readyState == 'complete') {
script.onreadystatechange = null;
fn();
}
};
} else {
//其他浏览器
script.onload = function () {
fn();
};
}
script.src = url;
oDoc.getElementsByTagName('head')[0].appendChild(script);
}
// Load/Read and Save javascript from given url
// Auto reties when xhr fails.
// If load success then callback(true), else callback(false)
function loadJSPlus(url, callback, oDoc=document, maxRetry=3) {
const fn = callback || function () {};
const localCDN = GM_getValue(KEY_LOCALCDN, {});
if (localCDN[url]) {
DoLog(LogLevel.Info, 'Loading js from localCDN: ' + url);
const js = localCDN[url];
appendScript(js);
fn(true);
return;
}
DoLog(LogLevel.Info, 'Loading js from web: ' + url);
loadJSPlus.retryCount = loadJSPlus.retryCount !== undefined ? loadJSPlus.retryCount : 0;
GM_xmlhttpRequest({
method : 'GET',
url : url,
responseType : 'text',
onload : function(e) {
if (e.status === 200) {
const js = e.responseText;
localCDN[url] = js;
localCDN[KEY_LOCALCDN_VERSION] = VALUE_LOCALCDN_VERSION;
GM_setValue(KEY_LOCALCDN, localCDN);
appendScript(js);
fn(true);
} else {
retry();
}
},
onerror : retry
})
function appendScript(code) {
const script = oDoc.createElement('script');
script.type = 'text/javascript';
script.innerHTML = code;
oDoc.head.appendChild(script);
}
function retry() {
loadJSPlus.retryCount++;
if (loadJSPlus.retryCount <= maxRetry) {
loadJSPlus(url, callback, oDoc, maxRetry);
} else {
fn(false);
}
}
}
// Get a url argument from lacation.href
// also recieve a function to deal the matched string
// returns defaultValue if name not found
// Args: name, dealFunc=(function(a) {return a;}), defaultValue=null
function getUrlArgv(details) {
typeof(details) === 'string' && (details = {name: details});
typeof(details) === 'undefined' && (details = {});
if (!details.name) {return null;};
const url = details.url ? details.url : location.href;
const name = details.name ? details.name : '';
const dealFunc = details.dealFunc ? details.dealFunc : ((a)=>{return a;});
const defaultValue = details.defaultValue ? details.defaultValue : null;
const matcher = new RegExp(name + '=([^&]+)');
const result = url.match(matcher);
const argv = result ? dealFunc(result[1]) : defaultValue;
return argv;
}
// Get a time text like 1970-01-01 00:00:00
// if dateSpliter provided false, there will be no date part. The same for timeSpliter.
function getTime(dateSpliter='-', timeSpliter=':') {
const d = new Date();
let fulltime = ''
fulltime += dateSpliter ? fillNumber(d.getFullYear(), 4) + dateSpliter + fillNumber((d.getMonth() + 1), 2) + dateSpliter + fillNumber(d.getDate(), 2) : '';
fulltime += dateSpliter && timeSpliter ? ' ' : '';
fulltime += timeSpliter ? fillNumber(d.getHours(), 2) + timeSpliter + fillNumber(d.getMinutes(), 2) + timeSpliter + fillNumber(d.getSeconds(), 2) : '';
return fulltime;
}
// Get key-value object from text like 'key: value'/'key:value'/' key : value '
// returns: {key: value, KEY: key, VALUE: value}
function getKeyValue(text, delimiters=[':', ':', ',']) {
// Modify from https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Error#examples
// Create a new object, that prototypally inherits from the Error constructor.
function SplitError(message) {
this.name = 'SplitError';
this.message = message || 'SplitError Message';
this.stack = (new Error()).stack;
}
SplitError.prototype = Object.create(Error.prototype);
SplitError.prototype.constructor = SplitError;
if (!text) {return [];};
const result = {};
let key, value;
for (let i = 0; i < text.length; i++) {
const char = text.charAt(i);
for (const delimiter of delimiters) {
if (delimiter === char) {
if (!key && !value) {
key = text.substr(0, i).trim();
value = text.substr(i+1).trim();
result[key] = value;
result.KEY = key;
result.VALUE = value;
} else {
throw new SplitError('Mutiple Delimiter in Text');
}
}
}
}
return result;
}
// Convert rgb color(e.g. 51,51,153) to hex color(e.g. '333399')
function rgbToHex(r, g, b) {return fillNumber(((r << 16) | (g << 8) | b).toString(16), 6);}
// Fill number text to certain length with '0'
function fillNumber(number, length) {
let str = String(number);
for (let i = str.length; i < length; i++) {
str = '0' + str;
}
return str;
}
// Judge whether the str is a number
function isNumeric(str, disableFloat=false) {
const result = Number(str);
return !isNaN(result) && str !== '' && (!disableFloat || result===Math.floor(result));
}
// Del a item from an array using its index. Returns the array but can NOT modify the original array directly!!
function delItem(arr, delIndex) {
arr = arr.slice(0, delIndex).concat(arr.slice(delIndex+1));
return arr;
}
// Clone(deep) an object variable
// Returns the new object
function deepclone(obj) {
if (obj === null) return null;
if (typeof(obj) !== 'object') return obj;
if (obj.constructor === Date) return new Date(obj);
if (obj.constructor === RegExp) return new RegExp(obj);
var newObj = new obj.constructor(); //保持继承的原型
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
const val = obj[key];
newObj[key] = typeof val === 'object' ? deepclone(val) : val;
}
}
return newObj;
}
// Makes a function that returns a unique ID number each time
function uniqueIDMaker() {
let id = 0;
return makeID;
function makeID() {
id++;
return id;
}
}
// Load required javascript files for non-GM/TM environments (such as Alook javascript extension)
// Please @require https://greasyfork.org/scripts/429557-gmrequirechecker/code/GMRequireChecker.js?version=951692 before using this function
function loadRequires(callback) {
if (typeof(GMRequiredJSLoaded) === 'boolean') {
callback();
return;
};
const requires = [
'https://cdn.jsdelivr.net/gh/PYUDNG/CDN@eed1fcf0e901348bc4e752fd483bcb571ebe0408/js/GBK_URL/GBK.js',
'https://cdn.jsdelivr.net/gh/PYUDNG/CDN@058b97a4c86980fa3de3d9ee9bc9f2f787e11c84/js/gui/elegant%20alert.js',
'https://cdn.jsdelivr.net/gh/PYUDNG/CDN@94fc2bdd313f7bf2af6db5b8699effee8dd0b18d/js/ajax/GreasyForkScriptUpdate.js'
]
let rest = requires.length;
for (const js of requires) {
DoLog(LogLevel.Info, 'Loading required js: ' + js);
loadJSPlus(js, jsLoaded);
}
function jsLoaded(success) {
if (success) {
rest--;
DoLog(LogLevel.Info, 'Required js loaded. {N} requires left.'.replaceAll('{N}', String(rest)));
rest === 0 ? callback() : function() {};
} else {
DoLog(LogLevel.Error, 'Required js load failed. {N} requires left.'.replaceAll('{N}', String(rest)));
alert(TEXT_GUI_REQUIRE_FAILED);
}
}
}
// GM_Polyfill By PY-DNG
// 2021.07.18 - 2021.07.19
// Simply provides the following GM_functions using localStorage, XMLHttpRequest and window.open:
// Returns object GM_POLYFILLED which has the following properties that shows you which GM_functions are actually polyfilled:
// GM_setValue, GM_getValue, GM_deleteValue, GM_listValues, GM_xmlhttpRequest, GM_openInTab, GM_setClipboard, unsafeWindow(object)
// All polyfilled GM_functions are accessable in window object/Global_Scope(only without Tempermonkey Sandboxing environment)
function GM_PolyFill(name='default') {
const GM_POLYFILL_KEY_STORAGE = 'GM_STORAGE_POLYFILL';
const GM_POLYFILL_storage = GM_POLYFILL_getStorage();
const GM_POLYFILLED = {
GM_setValue: true,
GM_getValue: true,
GM_deleteValue: true,
GM_listValues: true,
GM_xmlhttpRequest: true,
GM_openInTab: true,
GM_setClipboard: true,
unsafeWindow: true
}
GM_setValue_polyfill();
GM_getValue_polyfill();
GM_deleteValue_polyfill();
GM_listValues_polyfill();
GM_xmlhttpRequest_polyfill();
GM_openInTab_polyfill();
GM_setClipboard_polyfill();
unsafeWindow_polyfill();
function GM_POLYFILL_getStorage() {
let gstorage = localStorage.getItem(GM_POLYFILL_KEY_STORAGE);
gstorage = gstorage ? JSON.parse(gstorage) : {};
let storage = gstorage[name] ? gstorage[name] : {};
return storage;
}
function GM_POLYFILL_saveStorage() {
let gstorage = localStorage.getItem(GM_POLYFILL_KEY_STORAGE);
gstorage = gstorage ? JSON.parse(gstorage) : {};
gstorage[name] = GM_POLYFILL_storage;
localStorage.setItem(GM_POLYFILL_KEY_STORAGE, JSON.stringify(gstorage));
}
// GM_setValue
function GM_setValue_polyfill() {
typeof (GM_setValue) === 'function' ? GM_POLYFILLED.GM_setValue = false: window.GM_setValue = PF_GM_setValue;;
function PF_GM_setValue(name, value) {
name = String(name);
GM_POLYFILL_storage[name] = value;
GM_POLYFILL_saveStorage();
}
}
// GM_getValue
function GM_getValue_polyfill() {
typeof (GM_getValue) === 'function' ? GM_POLYFILLED.GM_getValue = false: window.GM_getValue = PF_GM_getValue;
function PF_GM_getValue(name, defaultValue) {
name = String(name);
if (GM_POLYFILL_storage.hasOwnProperty(name)) {
return GM_POLYFILL_storage[name];
} else {
return defaultValue;
}
}
}
// GM_deleteValue
function GM_deleteValue_polyfill() {
typeof (GM_deleteValue) === 'function' ? GM_POLYFILLED.GM_deleteValue = false: window.GM_deleteValue = PF_GM_deleteValue;
function PF_GM_deleteValue(name) {
name = String(name);
if (GM_POLYFILL_storage.hasOwnProperty(name)) {
delete GM_POLYFILL_storage[name];
GM_POLYFILL_saveStorage();
}
}
}
// GM_listValues
function GM_listValues_polyfill() {
typeof (GM_listValues) === 'function' ? GM_POLYFILLED.GM_listValues = false: window.GM_listValues = PF_GM_listValues;
function PF_GM_listValues() {
return Object.keys(GM_POLYFILL_storage);
}
}
// unsafeWindow
function unsafeWindow_polyfill() {
typeof (unsafeWindow) === 'object' ? GM_POLYFILLED.unsafeWindow = false: window.unsafeWindow = window;
}
// GM_xmlhttpRequest
// not supported properties of details: synchronous binary nocache revalidate context fetch
// not supported properties of response(onload arguments[0]): finalUrl
// ---!IMPORTANT!--- DOES NOT SUPPORT CROSS-ORIGIN REQUESTS!!!!! ---!IMPORTANT!---
function GM_xmlhttpRequest_polyfill() {
typeof (GM_xmlhttpRequest) === 'function' ? GM_POLYFILLED.GM_xmlhttpRequest = false: window.GM_xmlhttpRequest = PF_GM_xmlhttpRequest;
// details.synchronous is not supported as Tempermonkey
function PF_GM_xmlhttpRequest(details) {
const xhr = new XMLHttpRequest();
// open request
const openArgs = [details.method, details.url, true];
if (details.user && details.password) {
openArgs.push(details.user);
openArgs.push(details.password);
}
xhr.open.apply(xhr, openArgs);
// set headers
if (details.headers) {
for (const key of Object.keys(details.headers)) {
xhr.setRequestHeader(key, details.headers[key]);
}
}
details.cookie ? xhr.setRequestHeader('cookie', details.cookie) : function () {};
details.anonymous ? xhr.setRequestHeader('cookie', '') : function () {};
// properties
xhr.timeout = details.timeout;
xhr.responseType = details.responseType;
details.overrideMimeType ? xhr.overrideMimeType(details.overrideMimeType) : function () {};
// events
xhr.onabort = details.onabort;
xhr.onerror = details.onerror;
xhr.onloadstart = details.onloadstart;
xhr.onprogress = details.onprogress;
xhr.onreadystatechange = details.onreadystatechange;
xhr.ontimeout = details.ontimeout;
xhr.onload = function (e) {
const response = {
readyState: xhr.readyState,
status: xhr.status,
statusText: xhr.statusText,
responseHeaders: xhr.getAllResponseHeaders(),
response: xhr.response
};
(details.responseType === '' || details.responseType === 'text') ? (response.responseText = xhr.responseText) : function () {};
(details.responseType === '' || details.responseType === 'document') ? (response.responseXML = xhr.responseXML) : function () {};
details.onload(response);
}
// send request
details.data ? xhr.send(details.data) : xhr.send();
return {
abort: xhr.abort
};
}
}
// NOTE: options(arg2) is NOT SUPPORTED! if provided, then will just be skipped.
function GM_openInTab_polyfill() {
typeof (GM_openInTab) === 'function' ? GM_POLYFILLED.GM_openInTab = false: window.GM_openInTab = PF_GM_openInTab;
function PF_GM_openInTab(url) {
window.open(url);
}
}
// NOTE: needs to be called in an event handler function, and info(arg2) is NOT SUPPORTED!
function GM_setClipboard_polyfill() {
typeof (GM_setClipboard) === 'function' ? GM_POLYFILLED.GM_setClipboard = false: window.GM_setClipboard = PF_GM_setClipboard;
function PF_GM_setClipboard(text) {
// Create a new textarea for copying
const newInput = document.createElement('textarea');
document.body.appendChild(newInput);
newInput.value = text;
newInput.select();
document.execCommand('copy');
document.body.removeChild(newInput);
}
}
return GM_POLYFILLED;
}
// Polyfill GM_info
function polyfill_GM_info(version='移动端适配版') {
// Polyfill GM_info for this script
if(typeof(GM_info) !== 'object') {
window.GM_info = {
script: {
name: '轻小说文库+',
version: version,
author: 'PY-DNG'
}
}
}
}
// Polyfill alert
function polyfillAlert() {
if (typeof(GM_POLYFILLED) !== 'object') {return false;}
if (GM_POLYFILLED.GM_setValue) {
new ElegantAlertBox(TEXT_ALT_POLYFILL);
}
}
// Polyfill String.prototype.replaceAll
// replaceValue does NOT support regexp match groups($1, $2, etc.)
function polyfill_replaceAll() {
String.prototype.replaceAll = String.prototype.replaceAll ? String.prototype.replaceAll : PF_replaceAll;
function PF_replaceAll(searchValue, replaceValue) {
const str = String(this);
if (searchValue instanceof RegExp) {
const global = RegExp(searchValue, 'g');
if (/\$/.test(replaceValue)) {console.error('Error: Polyfilled String.protopype.replaceAll does support regexp groups');};
return str.replace(global, replaceValue);
} else {
return str.split(searchValue).join(replaceValue);
}
}
}
// Append a style text to document() with a