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':
// wenku8 has forbidden [img] tag for secure reason (preventing CSRF)
//content += '[img]S[/img]'.replace('S', node.src);
content += ' 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);
} 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;
}
}
}
// Append floor to #content
function appendfloor(floor) {
// Append
const table = floor.table;
const elmafter = main.querySelector('table.grid+table[border]');
main.insertBefore(table, elmafter);
// Enhances
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 = 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) {
destroyEvent(e);
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();
// Beautifier page
beautifier();
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.contentmain = document.querySelector('#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);
}
}
// 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);
}
}
// 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.reviewshow.backgroundImage)
.replaceAll('{S}', config.textScale)
.replaceAll('{H}', usedHeight), 'beautifier'
);
// Get rest height without #contentmain
function getRestHeight() {
let usedHeight = 0;
['adv1', 'adtop', 'headlink', 'footlink', 'adbottom'].forEach((id) => {
const node = document.querySelector('#'+id);
if (node instanceof Element && node.id !== 'contentmain') {
const cs = getComputedStyle(node);
['height', 'marginTop', 'marginBottom', 'paddingTop', 'paddingBottom', 'borderTop', 'borderBottom'].forEach((style) => {
usedHeight += Number(cs[style].match(/([\.\d]+)px/)[1]);
});
};
});
usedHeight = usedHeight.toString() + 'px';
return usedHeight;
}
}
}
// 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);
}
}
// Detail page add-on
function pageDetail() {
// Get elements
const content = document.querySelector('#content');
const tbody = content.querySelector('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_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_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'}]
]
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;
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)) {
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) {
settip(radio, imager.tip);
settip(text, imager.tip);
//settip(span, imager.tip);
}
elements.imager.appendChild(span);
}
elements.imager.querySelector('#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 ckbtfnvl = elements.ckbtfnvl = document.createElement('input');
const ckbtfrvw = elements.ckbtfrvw = document.createElement('input');
ckbtfnvl.type = ckbtfrvw.type = 'checkbox';
ckbtfnvl.page = 'novel';
ckbtfrvw.page = 'reviewshow';
ckbtfnvl.checked = CONFIG.BeautifierCfg.getConfig().novel.beautiful;
ckbtfrvw.checked = CONFIG.BeautifierCfg.getConfig().reviewshow.beautiful;
ckbtfnvl.addEventListener('change', beautifulChange);
ckbtfrvw.addEventListener('change', beautifulChange);
btfnvlctnr.appendChild(ckbtfnvl);
btfrvwctnr.appendChild(ckbtfrvw);
// 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);
// 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);
// 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') {
new ElegantAlertBox(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.reviewshow.backgroundImage = url;
config.reviewshow.bgiName = name;
CONFIG.BeautifierCfg.saveConfig(config);
elements.bgioprt.innerHTML = name;
URL.revokeObjectURL(objurl);
// Upload if need
if (config.upload) {
new ElegantAlertBox(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) => {
new ElegantAlertBox(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.reviewshow.backgroundImage = json.url;
CONFIG.BeautifierCfg.saveConfig(config);
elements.bgioprt.innerHTML = name;
new ElegantAlertBox(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.reviewshow.bgiName ? config.reviewshow.bgiName : 'image.jpeg';
if (config.upload) {
// Upload
const url = config.reviewshow.backgroundImage;
if (!/^https?:\/\//.test(url)) {
new ElegantAlertBox(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) => {
new ElegantAlertBox(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.reviewshow.backgroundImage);
CONFIG.BeautifierCfg.saveConfig(config);
},
onload: (json) => {
const config = CONFIG.BeautifierCfg.getConfig();
config.reviewshow.backgroundImage = json.url;
config.reviewshow.bgiName = elements.bgioprt.innerHTML = json.name;
CONFIG.BeautifierCfg.saveConfig(config);
new ElegantAlertBox(TEXT_ALT_IMAGE_UPLOAD_SUCCESS.replace('{NAME}', json.name).replace('{URL}', json.url));
}
});
}
} else {
// Download
const url = config.reviewshow.backgroundImage;
if (/^https?:\/\//.test(url)) {
new ElegantAlertBox(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.reviewshow.backgroundImage);
CONFIG.BeautifierCfg.saveConfig(config);
return false;
};
// Save to config
const config = CONFIG.BeautifierCfg.getConfig();
config.reviewshow.backgroundImage = dataurl;
CONFIG.BeautifierCfg.saveConfig(config);
new ElegantAlertBox(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);
new ElegantAlertBox('图床已切换到{NAME}'.replace('{NAME}', imager.name));
imager.warning && new ElegantAlertBox(imager.warning);
}
}
function scaleChange(e) {
e.stopPropagation();
const config = CONFIG.BeautifierCfg.getConfig();
config.textScale = e.target.value;
CONFIG.BeautifierCfg.saveConfig(config);
new ElegantAlertBox(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);
new ElegantAlertBox(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);
new ElegantAlertBox(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') {
new ElegantAlertBox(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);
new ElegantAlertBox(TEXT_ALT_DETAIL_IMPORTED);
} catch(err) {
fileError(err);
}
}
function fileError(e) {
DoLog(LogLevel.Error, ['pageDetail.insertSettings.GUI.configfileGot:', e]);
new ElegantAlertBox(TEXT_ALT_DETAIL_CONFIG_IMPORT_ERROR_READ);
}
}
}
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;
}
}
// 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 = document.querySelector('#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 = 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);
});
settip(serverEles[i], 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, '');
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 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);
};
}
})
}
})
}
}
// API page and its sub pages add-on
function pageAPI(API) {
addStyle(CSS_PAGE_API);
//logAPI();
switch(API) {
case 'modules/article/addbookcase.php':
pageAddbookcase();
break;
default:
logAPI();
}
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('确实要将本书移出书架么?')
}
});*/
}
// Add a bottom-styled botton into bottom line, to the first place
function addBottomButton(details) {
const aClose = document.querySelector('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 = document.querySelectorAll('.block');
const close = document.querySelectorAll('a[href="javascript:window.close()"]');
return block.length === 1 && close.length === 1;
}
// 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);
}
});
}
// 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 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 = '轻小说文库+_配置文件_v{V}_{T}.wkp'.replace('{V}', GM_info.script.version).replace('{T}', getTime());
downloadText(text, name);
new ElegantAlertBox(TEXT_ALT_CONFIG_EXPORTED.replace('{N}', name));
}
function importConfig(json) {
// Redirect
redirectGMStorage();
// 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);
}
// 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;
}
// 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();
}
// Create a list gui like reviewshow.php##FontSizeTable
// list = {display: '', id: '', parentElement: <*>, insertBefore: <*>, list: [{value: '', onclick: Function, tip: ''/Function}, ...], visible: bool, onshow: Function(bool shown), onhide: Function(bool hidden)}
// structure: {div:
, ul:
, list: [{li: - , button: }, ...], visible: list.visible, show: Function, hide: Function, append: Function({...}), remove: Function(index), clear: Function, onshow: list.onshow, onhide: list.onhide}
// Use 'new' keyword
function PlusList(list) {
const PL = this;
// Make list
const div = PL.div = document.createElement('div');
const ul = PL.ul = document.createElement('ul');
div.classList.add(CLASSNAME_LIST);
div.appendChild(ul);
list.display && (div.style.display = list.display);
list.id && (div.id = list.id);
list.parentElement && list.parentElement.insertBefore(div, list.insertBefore ? list.insertBefore : null);
let maxlength = 0;
PL.list = [];
for (const item of list.list) {
item.value && (maxlength = Math.max(maxlength, String(item.value).length));
appendItem(item);
}
ul.style.width = String(maxlength+2) + 'em';
// Attach properties
let onshow = list.onshow ? list.onshow : function() {};
let onhide = list.onhide ? list.onhide : function() {};
let visible = list.visible;
PL.create = createItem;
PL.append = appendItem;
PL.insert = insertItem;
PL.remove = removeItem;
PL.clear = removeAll;
PL.show = showList;
PL.hide = hideList;
Object.defineProperty(PL, 'onshow', {
get: function() {return onshow;},
set: function(func) {
onshow = func ? func : function() {};
},
configurable: false,
enumerable: true
});
Object.defineProperty(PL, 'onhide', {
get: function() {return onhide;},
set: function(func) {
onhide = func ? func : function() {};
},
configurable: false,
enumerable: true
});
Object.defineProperty(PL, 'visible', {
get: function() {return visible;},
set: function(bool) {
if (typeof(bool) !== 'boolean') {return false;};
visible = bool;
bool ? showList() : hideList();
},
configurable: false,
enumerable: true
});
Object.defineProperty(PL, 'maxheight', {
get: function() {return maxheight;},
set: function(num) {
if (typeof(num) !== 'number') {return false;};
maxheight = num;
},
configurable: false,
enumerable: true
});
// Apply configurations
div.style.display = list.visible === true ? '' : 'none';
// Functions
function appendItem(item) {
const listitem = createItem(item);
ul.appendChild(listitem.li);
PL.list.push(listitem);
return listitem;
}
function insertItem(item, index, insertByNode=false) {
const listitem = createItem(item);
const children = insertByNode ? ul.childNodes : ul.children;
const elmafter = children[index];
ul.insertBefore(item.li, elmafter);
inserttoarr(PL.list, listitem, index);
}
function createItem(item) {
const listitem = {
remove: () => {removeItem(listitem);},
li: document.createElement('li'),
button: document.createElement('input')
};
const li = listitem.li;
const btn = listitem.button;
btn.type = 'button';
btn.classList.add(CLASSNAME_LIST_BUTTON);
li.classList.add(CLASSNAME_LIST_ITEM);
item.value && (btn.value = item.value);
item.onclick && btn.addEventListener('click', item.onclick);
item.tip && settip(li, item.tip);
item.tip && settip(btn, item.tip);
li.appendChild(btn);
return listitem;
}
function removeItem(itemorindex) {
// Get index
let index;
if (typeof(itemorindex) === 'number') {
index = itemorindex;
} else if (typeof(itemorindex) === 'object') {
index = PL.list.indexOf(itemorindex);
} else {
return false;
}
if (index < 0 || index >= PL.list.length) {
return false;
}
// Remove
const li = PL.list[index];
ul.removeChild(li.li);
delfromarr(PL.list, index);
return li;
}
function removeAll() {
const length = PL.list.length;
for (let i = 0; i < length; i++) {
removeItem(0);
}
}
function showList() {
if (visible) {return false;};
onshow(false);
div.style.display = '';
onshow(true);
visible = true;
}
function hideList() {
if (!visible) {return false;};
onhide(false);
div.style.display = 'none';
hidetip();
onhide(true);
visible = false;
}
// Support functions
// Del an item from an array by provided index, returns the deleted item. MODIFIES the original array directly!!
function delfromarr(arr, delIndex) {
if (delIndex < 0 || delIndex > arr.length-1) {
return false;
}
const deleted = arr[delIndex];
for (let i = delIndex; i < arr.length-1; i++) {
arr[i] = arr[i+1];
}
arr.pop();
return deleted;
}
// Insert an item to an array by its provided index, returns the item itself. MODIFIES the original array directly!!
function inserttoarr(arr, item, index) {
if (index < 0 || index > arr.length-1) {
return false;
}
for (let i = arr.length; i > index; i--) {
arr[i] = arr[i-1];
}
arr[index] = item;
return item;
}
}
// 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 && settip(a, link.tiptitle);
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 && settip(a, link.tiptitle);
a.innerHTML = link.innerHTML;
a.id = link.id ? link.id : '';
li.appendChild(a);
ul.appendChild(li);
}
blockEle.appendChild(ul);
}
}
// 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);
};
};
// Upload image to KIENG images
// details: {file: File, onload: Function, onerror: Function, type: 'sm.ms/jd/sg/tt/...'}
function uploadImage(details) {
const file = details.file;
const onload = details.onload ? details.onload : function() {};
const onerror = details.onerror ? details.onerror : uploadError;
const type = details.type ? details.type : CONFIG.UserGlobalCfg.getConfig().imager;
if (!DATA_IMAGERS.hasOwnProperty(type) || !DATA_IMAGERS[type].available) {
onerror();
return false;
}
const imager = DATA_IMAGERS[type];
const upload = imager.upload;
const request = upload.request;
const response = upload.response;
// Construct request url
let url = request.url;
if (request.urlargs) {
const args = request.urlargs;
const makearg = (key, value) => ('{K}={V}'.replace('{K}', key).replace('{V}', value));
const replacers = {
'$filename$': () => (encodeURIComponent(file.name)),
'$random$': () => (Math.random().toString()),
'$time$': () => ((new Date()).getTime().toString())
}
for (let [key, value] of Object.entries(args)) {
url += url.includes('?') ? '&' : '?';
for (const [str, replacer] of Object.entries(replacers)) {
while (value.includes(str)) {
value = value.replace(str, replacer());
}
}
url += makearg(key, value);
}
}
// Construst request body
let data;
if (request.name) {
data = new FormData();
data.append(request.name.file, file);
for (const [key, value] of Object.entries(request.name)) {
if (key === 'file') {continue;};
data.append(key, value);
}
} else {
data = file;
}
GM_xmlhttpRequest({
method: 'POST',
url: url,
timeout: 15 * 1000,
data: data,
responseType: request.responseType ? request.responseType : 'json',
onerror: onerror,
ontimeout: onerror,
onabort: onerror,
onload: (e) => {
const json = e.response;
const success = e.status === 200 && response.checksuccess(json);
if (success) {
const url = response.geturl(json);
const name = response.getname ? (response.getname(json) ? response.getname(json) : TEXT_ALT_IMAGE_RESPONSE_NONAME) : TEXT_ALT_IMAGE_RESPONSE_NONAME
onload({
url: url,
name: name,
});
} else {
onerror(json);
return;
}
}
})
/* Common xhr version. Cannot bypass CORS.
const re = new XMLHttpRequest();
re.open('POST', request.url, true);
re.timeout = 15 * 1000;
re.onerror = re.ontimeout = re.onabort = uploadError;
re.responseType = request.responseType ? request.responseType : 'json';
re.onload = (e) => {
const json = re.response;
const success = response.checksuccess(json)
if (success) {
onload({
url: response.geturl(json),
name: response.getname ? response.getname(json) : TEXT_ALT_IMAGE_RESPONSE_NONAME,
});
} else {
uploadError(json);
return;
}
}
re.send(data);*/
function uploadError(json) {
new ElegantAlertBox(TEXT_ALT_IMAGE_UPLOAD_ERROR);
DoLog(LogLevel.Error, [TEXT_ALT_IMAGE_UPLOAD_ERROR, json]);
}
}
// 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);
}
}
// Just stopPropagation and preventDefault
function destroyEvent(e) {
if (!e) {return false;};
if (!e instanceof Event) {return false;};
e.stopPropagation();
e.preventDefault();
}
// 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);
}
}
// Aim to separate big data from config, to boost up the speed of config reading.
// FAILED. NEVER USE THESE CODES. NEVER DO THESE THINGS AGAIN. FUCK MYSELF ME STUPID.
function GMBigData() {
function hookget() {
const oGet = GM_getValue;
GM_getValue = function(name, defaultValue) {
const value = oGet(name, defaultValue);
// Only hooks object
if (typeof(value) !== 'object') {
return value;
}
const data = dealAll(value);
function dealAll(data) {
const meta = {};
for (const key of Object.keys(data)) {
if (isDatakey(value) && keyExists(value)) {
Object.defineProperty(data, key, {
get: function() {
return localStorage.getItem(data[key]);
},
configurable: true,
enumerable: true
});
meta[key] = value;
}
if (typeof(value) === 'object') {dealAll(value);};
}
Object.defineProperty(data, 'GM_BIGDATA_META_INFO', {
value: meta,
writable: false,
configurable: false,
enumerable: false
});
return data;
}
}
}
function hookset() {
const oSet = GM_setValue;
GM_setValue = function(name, value) {
// Only hooks object
if (typeof(value) !== 'object') {
oSet(name, value);
}
// Deal value data
const data = dealAll(value);
// Save to GM_storage
oSet(name, data);
// Search & deal bigdata in data object, returns dealed data; not modifying original data object directly
// NOT working on non-objective data types
function dealAll(data) {
const result = {};
const meta = data['GM_BIGDATA_META_INFO'];
for (const [key, value] of Object.entries(data)) {
if (typeof(value) !== 'object') {
if (meta.hasOwnProperty(key)) {
const datakey = meta[key];
localStorage.setItem(datakey, value);
} else {
data = dealOne(data);
}
return data;
} else {
result[key] = dealAll(value);
}
}
return result;
}
// Deal one data, returns the datakey of dealed data
function dealOne(data) {
// Only data bigger than 10kb will be dealed as bigdata
if (getDataSize(data) <= 1024*10) {return data;};
// If data is already a datakey, then no repeating the work
if (isDatakey(data)) {return data;};
// Generate a key for this bigdata
const datakey = generateKey();
// Save data to localStorage[datakey]
localStorage.setItem(datakey, data);
// Return datakey
return datakey;
}
}
}
// Datakey generator
function generateKey(length=16) {
let datakey = 'GM_BIGDATA:' + randstr(length);
while (keyExists(datakey)) {
datakey = 'GM_BIGDATA:' + randstr(length);
}
}
// Check whether a datakey already exists
function keyExists(datakey) {
for (let i = 0; i < localStorage.length; i++) {
if (localStorage.key(i) === datakey) {return true;};
}
return false;
}
// Check whether the value is a datakey
function isDatakey(value) {
return typeof(value) !== 'string' ? false : /^BIGDATA:[a-zA-Z0-9]+$/.test(value);
}
}
// 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;
parseDocument(htmlblob, callback, args);
}
})
}
function parseDocument(htmlblob, callback, args=[]) {
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');
}
// Get a base64-formatted url of an image
// When image load error occurs, callback will be called without any argument
function getImageUrl(src, fitx, fity, callback, args=[]) {
const image = new Image();
image.setAttribute("crossOrigin",'anonymous');
image.onload = convert;
image.onerror = image.onabort = callback;
image.src = src;
function convert() {
const cvs = document.createElement('canvas');
const ctx = cvs.getContext('2d');
let width, height;
if (fitx && fity) {
width = window.innerWidth;
height = window.innerHeight;
} else if (fitx) {
width = window.innerWidth;
height = (width / image.width) * image.height;
} else if (fity) {
height = window.innerHeight;
width = (height / image.height) * image.width;
} else {
width = image.width;
height = image.height;
}
cvs.width = width;
cvs.height = height;
ctx.drawImage(image, 0, 0, width, height);
try {
callback.apply(null, [cvs.toDataURL()].concat(args));
} catch (e) {
DoLog(LogLevel.Error, ['Error at getImageUrl.convert()', e]);
callback();
}
}
}
// Convert a 'data:image/jpeg;base64,57af8b....' to a Blob object
function b64toBlob(dataURI) {
const mime = dataURI.match(/data:(.+?);/)[1];
const byteString = atob(dataURI.split(',')[1]);
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
return new Blob([ab], {type: mime});
}
//将base64转换为文件
function dataURLtoFile(dataurl, filename) {
var arr = dataurl.split(','),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new File([u8arr], filename, {
type: mime
});
}
// 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, retried=0) {
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);
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() {
retried++;
if (retried <= maxRetry) {
loadJSPlus(url, callback, oDoc, maxRetry, retried);
} 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: {url=location.href, name, dealFunc=((a)=>{return a;}), defaultValue=null} or 'name'
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;
}
// Get the size of data
function getDataSize(data) {
return (new Blob([data])).size;
}
// 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;
}
}
// Returns a random string
function randstr(length=16, cases=true, aviod=[]) {
const all = 'abcdefghijklmnopqrstuvwxyz0123456789' + (cases ? 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' : '');
while (true) {
let str = '';
for (let i = 0; i < length; i++) {
str += all.charAt(randint(0, all.length-1));
}
if (!aviod.includes(str)) {return str;};
}
}
function randint(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// 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);
}
}
}
// Bypass xbrowser's useless GM_functions
function bypassXB() {
if (typeof(mbrowser) === 'object') {
window.unsafeWindow = window.GM_setClipboard = window.GM_openInTab = window.GM_xmlhttpRequest = window.GM_getValue = window.GM_setValue = window.GM_listValues = window.GM_deleteValue = undefined;
}
}
// 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';
let GM_POLYFILL_storage;
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,
once: false
}
// Ignore GM_PolyFill_Once
window.GM_POLYFILLED && window.GM_POLYFILLED.once && (window.unsafeWindow = window.GM_setClipboard = window.GM_openInTab = window.GM_xmlhttpRequest = window.GM_getValue = window.GM_setValue = window.GM_listValues = window.GM_deleteValue = undefined);
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) {
GM_POLYFILL_storage = GM_POLYFILL_getStorage();
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) {
GM_POLYFILL_storage = GM_POLYFILL_getStorage();
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) {
GM_POLYFILL_storage = GM_POLYFILL_getStorage();
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() {
GM_POLYFILL_storage = GM_POLYFILL_getStorage();
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