// ==UserScript==
// @name MSPFA extras
// @namespace http://tampermonkey.net/
// @version 1.3
// @description Adds custom features to MSPFA.
// @author seymour schlong
// @match https://mspfa.com/
// @match https://mspfa.com/*/
// @match https://mspfa.com/*/?*
// @match https://mspfa.com/?s=*
// @match https://mspfa.com/my/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
/**
* https://github.com/GrantGryczan/MSPFA/projects/1?fullscreen=true
* Github to-do completion list
*
* https://github.com/GrantGryczan/MSPFA/issues/26 - Dropdown menu
* https://github.com/GrantGryczan/MSPFA/issues/18 - MSPFA themes
* https://github.com/GrantGryczan/MSPFA/issues/32 - Adventure creation dates
* https://github.com/GrantGryczan/MSPFA/issues/32 - User creation dates
* https://github.com/GrantGryczan/MSPFA/issues/40 - Turn certain buttons into links
* https://github.com/GrantGryczan/MSPFA/issues/41 - Word and character count
*
* Extension to-do... maybe...
* https://github.com/GrantGryczan/MSPFA/issues/57 - Default spoiler values
* https://github.com/GrantGryczan/MSPFA/issues/62 - Buttonless spoilers
*/
// A general function that allows for waiting until a certain element appears on the page.
const pageLoad = (fn) => {
let interval = setInterval(() => {
if (fn()) clearInterval(interval);
}, 500);
};
// Saves the options data for the script.
const saveData = (data) => {
localStorage.mspfaextra = JSON.stringify(data);
console.log("Saved cookies under mspfaextra.");
};
// Encases an element within a link
const addLink = (elm, url) => {
let link = document.createElement('a');
link.href = url;
elm.parentNode.insertBefore(link, elm);
link.appendChild(elm);
};
let settings = {};
if (localStorage.mspfaextra) {
settings = JSON.parse(localStorage.mspfaextra);
} else {
settings.autospoiler = false;
settings.style = 0;
settings.styleURL = "";
settings.night = false;
settings.auto502 = true;
saveData(settings);
}
if (typeof settings.autospoiler === "undefined") {
settings.autospoiler = false;
}
if (typeof settings.style === "undefined") {
settings.style = 0;
}
if (typeof settings.styleURL === "undefined") {
settings.styleURL = "";
}
if (typeof settings.night === "undefined") {
settings.night = false;
}
if (typeof settings.auto502 === "undefined") {
settings.auto502 = true;
}
if (typeof settings.textFix === "undefined") {
settings.textFix = false;
}
//console.log(settings);
let styleOptions = ["Standard", "Low Contrast", "Light", "Dark", "Felt", "Trickster", "Custom"];
let myLink = document.querySelector('nav a[href="/my/"]');
let dropDiv = document.createElement('div');
dropDiv.className = 'dropdown';
Object.assign(dropDiv.style, {
position: 'relative',
display: 'inline-block',
backgroundColor: 'inherit'
});
let dropContent = document.createElement('div');
dropContent.className = 'dropdown-content';
Object.assign(dropContent.style, {
display: 'none',
backgroundColor: 'inherit',
position: 'absolute',
textAlign: 'left',
minWidth: '100px',
marginLeft: '-5px',
padding: '2px',
zIndex: '1',
borderRadius: '0 0 5px 5px'
});
dropDiv.addEventListener('mouseenter', evt => {
dropContent.style.display = 'block';
dropContent.style.color = getComputedStyle(myLink).color;
});
dropDiv.addEventListener('mouseleave', evt => {
dropContent.style.display = 'none';
});
if (myLink) {
myLink.parentNode.insertBefore(dropDiv, myLink);
dropDiv.appendChild(myLink);
dropDiv.appendChild(dropContent);
let dLinks = [];
dLinks[0] = [ 'Messages', 'My Adventures', 'Settings' ];
dLinks[1] = [ '/my/messages/', '/my/stories/', '/my/settings/' ];
for (let i = 0; i < dLinks[0].length; i++) {
let newLink = document.createElement('a');
newLink.textContent = dLinks[0][i];
newLink.href = dLinks[1][i];
dropContent.appendChild(newLink);
}
}
window.addEventListener("load", () => {
// Reload the page if 502 CloudFlare error page appears
if (settings.auto502 && document.querySelector('.cf-error-overview')) {
window.location.reload();
}
// Append "My Profile" to the dropdown list if you're signed in
pageLoad(() => {
if (window.MSPFA) {
if (window.MSPFA.me.n) {
let newLink = document.createElement('a');
newLink.textContent = "My Profile";
newLink.href = `/user/?u=${window.MSPFA.me.i}`;
dropContent.appendChild(newLink);
return true;
}
return true;
}
});
});
let dropStyleText = '#notification { z-index: 2; } .dropdown-content a { color: inherit; padding: 2px; text-decoration: underline; display: block;}';
let dropStyle = document.createElement('style');
dropStyle.id = 'dropdown-style';
dropStyle.textContent = dropStyleText;
//dropdownStyle.textContent = '#notification { z-index: 2;}.dropdown:hover .dropdown-content { display: block;}.dropdown { position: relative; display: inline-block; background-color: inherit;}.dropdown-content { display: none; position: absolute; text-align: left; background-color: inherit; min-width: 100px; margin-left: -5px; padding: 2px; z-index: 1; border-radius: 0 0 5px 5px;}.dropdown-content a { color: #fffa36; padding: 2px 2px; text-decoration: underline; display: block;}';
let theme = document.createElement('link');
Object.assign(theme, { id: 'theme', type: 'text/css', rel: 'stylesheet' });
if (!document.querySelector('#theme') && !/^\/css\/|^\/js\//.test(location.pathname)) {
document.querySelector('head').appendChild(theme);
}
if (!document.querySelector('#dropdown-style')) {
document.querySelector('head').appendChild(dropStyle);
}
const updateTheme = (src) => {
theme.href = src;
}
if (settings.night || settings.style === 3) {
updateTheme('/css/?s=36237');
} else {
updateTheme(settings.style < styleOptions.length - 1 ? '/css/theme' + settings.style + '.css' : settings.styleURL);
}
pageLoad(() => {
if (window.MSPFA) {
if (window.MSPFA.story && window.MSPFA.story.y && window.MSPFA.story.y.length > 0) {
updateTheme("");
}
return true;
}
});
pageLoad(() => {
if (document.querySelector('footer .mspfalogo')) {
document.querySelector('footer .mspfalogo').addEventListener('dblclick', evt => {
if (evt.button === 0) {
settings.night = !settings.night;
saveData(settings);
if (settings.night || settings.style === 3) {
updateTheme('/css/?s=36237');
} else {
updateTheme(settings.style < styleOptions.length - 1 ? '/css/theme' + settings.style + '.css' : settings.styleURL);
}
dropStyle.textContent = dropStyleText + '*{transition:1s}';
setTimeout(() => {
dropStyle.textContent = dropStyleText;
}, 1000);
console.log(`Night mode turned ${settings.night ? 'on' : 'off'}.`);
}
});
return true;
}
});
if (location.pathname === "/" || location.pathname === "/preview/") {
if (settings.autospoiler) {
window.MSPFA.slide.push((p) => {
document.querySelectorAll('#slide .spoiler:not(.open) > div:first-child > input').forEach(sb => sb.click());
});
}
if (location.search) {
pageLoad(() => {
if (document.querySelector('#infobox tr td:nth-child(2)')) {
document.querySelector('#infobox tr td:nth-child(2)').appendChild(document.createTextNode('Creation date: ' + new Date(window.MSPFA.story.d).toString().split(' ').splice(1, 3).join(' ')));
return true;
}
});
if (settings.textFix) {
pageLoad(() => {
if (window.MSPFA.story && window.MSPFA.story.p) {
// russian/bulgarian is not possible =(
let currentPage = parseInt(/^\?s(?:.*?)&p=([\d]*)$/.exec(location.search)[1]);
let library = [
["�", "'"],
["Ã�", "Ñ"],
["ñ", "ñ"],
["ó", "ó"],
["á", "á"],
["í", "í"],
["ú", "ú"],
["é", "é"],
["¡", "¡"],
["¿", "¿"],
["Nº", "#"]
];
// https://mspfa.com/?s=5280&p=51 -- unknown error
const replaceTerms = (p) => {
library.forEach(term => {
if (window.MSPFA.story.p[p]) {
window.MSPFA.story.p[p].c = window.MSPFA.story.p[p].c.replace(new RegExp(term[0], 'g'), term[1]);
window.MSPFA.story.p[p].b = window.MSPFA.story.p[p].b.replace(new RegExp(term[0], 'g'), term[1]);
}
});
};
replaceTerms(currentPage-1);
window.MSPFA.slide.push(p => {
replaceTerms(p);
replaceTerms(p-2);
});
window.MSPFA.page(currentPage);
return true;
}
});
}
pageLoad(() => {
let infoButton = document.querySelector('.edit.major');
if (infoButton) {
pageLoad(() => {
if (window.MSPFA.me.i) {
addLink(infoButton, `/my/stories/info/${location.search.split('&p=')[0]}`);
return;
}
});
addLink(document.querySelector('.rss.major'), `/rss/${location.search.split('&p=')[0]}`);
return true;
}
});
/*
pageLoad(() => {
let infoButton = document.querySelector('.edit.major');
if (infoButton) {
let editPages = document.createElement('button');
Object.assign(editPages, { className: 'editpages major edit', title: 'Edit pages'});
//infoButton.parentNode.insertBefore(editPages, infoButton);
return true;
}
});/**/
}
}
else if (location.pathname === "/my/settings/") { // Custom settings
let saveBtn = document.querySelector('#savesettings');
let table = document.querySelector("#editsettings tbody");
let saveTr = table.querySelectorAll("tr");
saveTr = saveTr[saveTr.length - 1];
let headerTr = document.createElement('tr');
let header = document.createElement('th');
header.textContent = "Extra Settings";
headerTr.appendChild(header);
let moreTr = document.createElement('tr');
let more = document.createElement('td');
more.textContent = "* This only applies to a select few older adventures that have had their text corrupted. Currently only some spanish/french is fixable. Russian/Bulgarian is not possible.";
moreTr.appendChild(more);
let settingsTr = document.createElement('tr');
let localMsg = document.createElement('span');
let settingsTd = document.createElement('td');
localMsg.innerHTML = "Because this is an extension, any data saved is only <b>locally</b> on this device.<br>Don't forget to <b>save</b> when you've finished making changes!";
let plusTable = document.createElement('table');
let plusTbody = document.createElement('tbody');
plusTable.appendChild(plusTbody);
settingsTd.appendChild(localMsg);
settingsTd.appendChild(document.createElement('br'));
settingsTd.appendChild(document.createElement('br'));
settingsTd.appendChild(plusTable);
settingsTr.appendChild(settingsTd);
let spoilerTr = plusTbody.insertRow(0);
let spoilerTextTd = spoilerTr.insertCell(0);
let spoilerInputTd = spoilerTr.insertCell(1);
let spoilerInput = document.createElement('input');
spoilerInputTd.appendChild(spoilerInput);
let errorTr = plusTbody.insertRow(1);
let errorTextTd = errorTr.insertCell(0);
let errorInputTd = errorTr.insertCell(1);
let errorInput = document.createElement('input');
errorInputTd.appendChild(errorInput);
let textFixTr = plusTbody.insertRow(2);
let textFixTextTd = textFixTr.insertCell(0);
let textFixInputTd = textFixTr.insertCell(1);
let textFixInput = document.createElement('input');
textFixInputTd.appendChild(textFixInput);
let cssTr = plusTbody.insertRow(3);
let cssTextTd = cssTr.insertCell(0);
let cssSelectTd = cssTr.insertCell(1);
let cssSelect = document.createElement('select');
cssSelectTd.appendChild(cssSelect);
let customTr = plusTbody.insertRow(4);
let customTextTd = customTr.insertCell(0);
let customCssTd = customTr.insertCell(1);
let customCssInput = document.createElement('input');
customCssTd.appendChild(customCssInput);
plusTable.style = "text-align: center;";
spoilerTextTd.textContent = "Automatically open spoilers:";
spoilerInput.name = "p1";
spoilerInput.type = "checkbox";
spoilerInput.checked = settings.autospoiler;
errorTextTd.textContent = "Automatically reload Cloudflare 502 error pages:";
errorInput.name = "p2";
errorInput.type = "checkbox";
errorInput.checked = settings.auto502;
textFixTextTd.textContent = "Attempt to fix text errors (experimental)*:";
textFixInput.name = "p2";
textFixInput.type = "checkbox";
textFixInput.checked = settings.textFix;
cssTextTd.textContent = "Change style:";
customTextTd.textContent = "Custom CSS URL:";
customCssInput.style.width = "99px";
customCssInput.value = settings.styleURL;
styleOptions.forEach(o => cssSelect.appendChild(new Option(o, o)));
// Enable the save button
spoilerInput.addEventListener("change", () => {
saveBtn.disabled = false;
});
errorInput.addEventListener("change", () => {
saveBtn.disabled = false;
});
textFixInput.addEventListener("change", () => {
saveBtn.disabled = false;
});
cssSelect.addEventListener("change", () => {
saveBtn.disabled = false;
});
customCssInput.addEventListener("keydown", () => {
saveBtn.disabled = false;
});
saveTr.parentNode.insertBefore(headerTr, saveTr);
saveTr.parentNode.insertBefore(settingsTr, saveTr);
saveTr.parentNode.insertBefore(moreTr, saveTr);
cssSelect.selectedIndex = settings.style;
saveBtn.addEventListener('mouseup', () => {
settings.autospoiler = spoilerInput.checked;
settings.style = cssSelect.selectedIndex;
settings.styleURL = customCssInput.value;
settings.auto502 = errorInput.checked;
settings.textFix = textFixInput.checked;
theme.href = settings.style < styleOptions.length - 1 ? '/css/theme' + settings.style + '.css' : settings.styleURL;
settings.night = false;
console.log(settings);
saveData(settings);
dropStyle.textContent = dropStyleText + '*{transition:1s}';
setTimeout(() => {
dropStyle.textContent = dropStyleText;
}, 1000);
});
}
else if (location.pathname === "/my/messages/") { // New buttons
let btnStyle = "margin: 10px 5px;";
// Select all read messages button.
const selRead = document.createElement('input');
selRead.style = btnStyle;
selRead.value = "Select Read";
selRead.id = "selectread";
selRead.classList.add("major");
selRead.type = "button";
// On click, select all messages with the style attribute indicating it as read.
selRead.addEventListener('mouseup', () => {
document.querySelectorAll('td[style="border-left: 8px solid rgb(221, 221, 221);"] > input').forEach((m) => m.click());
});
// Select duplicate message (multiple update notifications).
const selDupe = document.createElement('input');
selDupe.style = btnStyle;
selDupe.value = "Select Same";
selDupe.id = "selectdupe";
selDupe.classList.add("major");
selDupe.type = "button";
selDupe.addEventListener('mouseup', evt => {
let temp = document.querySelectorAll('#messages > tr');
let msgs = [];
for (let i = temp.length - 1; i >= 0; i--) {
msgs.push(temp[i]);
}
let titles = [];
msgs.forEach((msg) => {
let title = msg.querySelector('a.major').textContent;
if (/^New update: /.test(title)) { // Select only adventure updates
if (titles.indexOf(title) === -1) {
if (msg.querySelector('td').style.cssText !== "border-left: 8px solid rgb(221, 221, 221);") {
titles.push(title);
}
} else {
msg.querySelector('input').click();
}
}
});
});
// Add buttons to the page.
let del = document.querySelector('#deletemsgs');
del.parentNode.appendChild(document.createElement('br'));
del.parentNode.appendChild(selRead);
del.parentNode.appendChild(selDupe);
}
else if (location.pathname === "/my/stories/") {
pageLoad(() => {
let adventures = document.querySelectorAll('#stories tr');
if (adventures.length > 0) {
adventures.forEach(story => {
let buttons = story.querySelectorAll('input.major');
let id = story.querySelector('a').href.replace('https://mspfa.com/', '').replace('&p=1', '');
addLink(buttons[0], `/my/stories/info/${id}`);
addLink(buttons[1], `/my/stories/pages/${id}`);
});
return true;
}
});
let guides = ["MSPFA Etiquette", "Fanventure Guide for Dummies", "CSS Guide", "HTML and CSS Things"];
let ids = ["27631", "29299", "21099", "23711"];
let authors = ["Radical Dude 42", "nzar", "MadCreativity", "seymour schlong"];
let parentTd = document.querySelector('.container > tbody > tr:last-child > td');
let unofficial = parentTd.querySelector('span');
unofficial.textContent = "Unofficial Guides";
let guideTable = document.createElement('table');
let guideTbody = document.createElement('tbody');
guideTable.style.width = "100%";
guideTable.style.textAlign = "center";
guideTable.appendChild(guideTbody);
parentTd.appendChild(guideTable);
for (let i = 0; i < guides.length; i++) {
let guideTr = guideTbody.insertRow(i);
let guideTd = guideTr.insertCell(0);
let guideLink = document.createElement('a');
guideLink.href = '/?s='+ids[i];
guideLink.textContent = guides[i];
guideLink.className = "major";
guideTd.appendChild(guideLink);
guideTd.appendChild(document.createElement('br'));
guideTd.appendChild(document.createTextNode('by '+authors[i]));
guideTd.appendChild(document.createElement('br'));
guideTd.appendChild(document.createElement('br'));
}
}
else if (location.pathname === "/my/stories/info/" && location.search) {
addLink(document.querySelector('#userfavs'), `/readers/${location.search}`);
addLink(document.querySelector('#editpages'), `/my/stories/pages/${location.search}`);
}
else if (location.pathname === "/my/stories/pages/" && location.search) {
addLink(document.querySelector('#editinfo'), `/my/stories/info/${location.search}`);
}
else if (location.pathname === "/user/") {
pageLoad(() => {
let msgButton = document.querySelector('#sendmsg');
if (msgButton) {
addLink(msgButton, '/my/messages/new/'); // note: doesn't input the desired user's id
addLink(document.querySelector('#favstories'), `/favs/${location.search}`);
}
});
pageLoad(() => {
if (window.MSPFA) {
window.MSPFA.request(0, {
do: "user",
u: location.search.slice(3)
}, user => {
if (typeof user !== "undefined") {
let stats = document.querySelector('#userinfo table');
let joinTr = stats.insertRow(1);
let joinTextTd = joinTr.insertCell(0);
joinTextTd.appendChild(document.createTextNode("Account created:"));
let d = new Date(user.d).toString().split(' ').splice(1, 4).join(' ');
let joinDate = joinTr.insertCell(1);
let joinTime = document.createElement('b');
joinTime.appendChild(document.createTextNode(d));
joinDate.appendChild(joinTime);
}
}, status => {
console.log(status);
}, true);
return true;
}
});
}
else if (location.pathname === "/favs/" && location.search) {
pageLoad(() => {
let stories = document.querySelectorAll('#stories tr');
if (stories.length > 0) {
stories.forEach(story => {
let id = story.querySelector('a').href.replace('https://mspfa.com/', '');
pageLoad(() => {
if (window.MSPFA.me.i) {
addLink(story.querySelector('.edit.major'), `/my/stories/info/${id}`);
return;
}
});
addLink(story.querySelector('.rss.major'), `/rss/${id}`);
});
return true;
}
});
}
else if (location.pathname === "/search/" && location.search) {
let pages = document.querySelector('#pages');
let statTable = document.createElement('table');
let statTbody = document.createElement('tbody');
let statTr = statTbody.insertRow(0);
let charCount = statTr.insertCell(0);
let wordCount = statTr.insertCell(0);
let statParentTr = pages.parentNode.parentNode.insertRow(2);
let statParentTd = statParentTr.insertCell(0);
let statHeaderTr = statTbody.insertRow(0);
let statHeader = document.createElement('th');
statHeader.colSpan = '2';
statHeaderTr.appendChild(statHeader);
statHeader.textContent = 'Statistics may not be entirely accurate.';
statTable.style.width = "100%";
charCount.textContent = "Character count: loading...";
wordCount.textContent = "Word count: loading...";
statTable.appendChild(statTbody);
statParentTd.appendChild(statTable);
pageLoad(() => {
if (document.querySelector('#pages br')) {
let bbc = window.MSPFA.BBC.slice();
bbc.splice(0, 3);
window.MSPFA.request(0, {
do: "story",
s: location.search.replace('?s=', '')
}, story => {
if (typeof story !== "undefined") {
let pageContent = [];
story.p.forEach(p => {
pageContent.push(p.c);
pageContent.push(p.b);
});
let storyText = pageContent.join(' ')
.replace(/\n/g, ' ')
.replace(bbc[0][0], '$1')
.replace(bbc[1][0], '$1')
.replace(bbc[2][0], '$1')
.replace(bbc[3][0], '$1')
.replace(bbc[4][0], '$2')
.replace(bbc[5][0], '$3')
.replace(bbc[6][0], '$3')
.replace(bbc[7][0], '$3')
.replace(bbc[8][0], '$3')
.replace(bbc[9][0], '$3')
.replace(bbc[10][0], '$2')
.replace(bbc[11][0], '$1')
.replace(bbc[12][0], '$3')
.replace(bbc[13][0], '$3')
.replace(bbc[14][0], '')
.replace(bbc[16][0], '$1')
.replace(bbc[17][0], '$2 $4 $5')
.replace(bbc[18][0], '$2 $4 $5')
.replace(bbc[19][0], '')
.replace(bbc[20][0], '');
console.log(storyText);
wordCount.textContent = `Word count: ${storyText.split(/ +/g).length}`;
charCount.textContent = `Character count: ${storyText.replace(/ +/g, '').length}`;
}
}, status => {
console.log(status);
}, true);
return true;
}
});
}
})();