您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds custom quality of life features to MSPFA.
当前为
// ==UserScript== // @name MSPFA extras // @namespace http://tampermonkey.net/ // @version 1.6.1.1 // @description Adds custom quality of life features to MSPFA. // @author seymour schlong // @icon https://raw.githubusercontent.com/GrantGryczan/MSPFA/master/www/images/ico.svg // @icon64 https://raw.githubusercontent.com/GrantGryczan/MSPFA/master/www/images/ico.svg // @match https://mspfa.com/ // @match https://mspfa.com/*/ // @match https://mspfa.com/*/?* // @match https://mspfa.com/?s=* // @match https://mspfa.com/my/* // @match https://mspfa.com/random/ // @grant none // ==/UserScript== (function() { 'use strict'; const currentVersion = "1.6.1.1"; console.log(`MSPFA extras script v${currentVersion} by seymour schlong`); /** * https://github.com/GrantGryczan/MSPFA/projects/1?fullscreen=true * Github to-do completion list * * https://github.com/GrantGryczan/MSPFA/issues/26 - Dropdown menu - February 23rd, 2020 * https://github.com/GrantGryczan/MSPFA/issues/18 - MSPFA themes - February 23rd, 2020 * https://github.com/GrantGryczan/MSPFA/issues/32 - Adventure creation dates - February 23rd, 2020 * https://github.com/GrantGryczan/MSPFA/issues/32 - User creation dates - February 23rd, 2020 * https://github.com/GrantGryczan/MSPFA/issues/40 - Turn certain buttons into links - July 21st, 2020 * https://github.com/GrantGryczan/MSPFA/issues/41 - Word and character count - July 21st, 2020 * https://github.com/GrantGryczan/MSPFA/issues/57 - Default spoiler values - August 7th, 2020 * https://github.com/GrantGryczan/MSPFA/issues/62 - Buttonless spoilers - August 7th, 2020 * https://github.com/GrantGryczan/MSPFA/issues/52 - Hash URLs - August 8th, 2020 * - Page drafts - August 8th, 2020 * - Edit pages button - August 8th, 2020 * * Extension to-do... maybe... * * If trying to save a page and any other save button is not disabled, ask the user if they would rather Save All instead, or prompt to disable update notifications. */ // A general function that allows for waiting until a certain element appears on the page. const pageLoad = (fn, length) => { const interval = setInterval(() => { if (fn()) clearInterval(interval); }, length ? length*1000 : 500); }; // Saves the options data for the script. const saveData = (data) => { localStorage.mspfaextra = JSON.stringify(data); }; // Encases an element within a link const addLink = (elm, url) => { const link = document.createElement('a'); link.href = url; elm.parentNode.insertBefore(link, elm); link.appendChild(elm); }; // Returns true if version 2 is newer const compareVer = (ver1, ver2) => { ver1 = ver1.split(/\./); // current version ver2 = ver2.split(/\./); // new version ver1.push(0); ver1.push(0); ver2.push(0); ver2.push(0); if (parseInt(ver2[0]) > parseInt(ver1[0])) { // 1.x.x return true; } else if (parseInt(ver2[1]) > parseInt(ver1[1])) { // x.1.x return true; } else if (parseInt(ver2[2]) > parseInt(ver1[2])) { // x.x.1 return true; } else if (parseInt(ver2[3]) > parseInt(ver1[3])) { // x.x.1 return true; } return false; } // Easy br element const newBr = () => { return document.createElement('br'); } let settings = {}; const defaultSettings = { autospoiler: false, style: 0, styleURL: "", night: false, auto502: true, textFix: false, pixelFix: false, intro: false, autoUpdate: false, version: currentVersion, spoilerValues: {}, drafts: {} } // Load any previous settings from localStorage if (localStorage.mspfaextra) { Object.assign(settings, JSON.parse(localStorage.mspfaextra)); } // If any settings are undefined, re-set to their default state. (For older users when new things get stored) const checkSettings = () => { const defaultSettingsKeys = Object.keys(defaultSettings); for (let i = 0; i < defaultSettingsKeys.length; i++) { if (typeof settings[defaultSettingsKeys[i]] === "undefined") { settings[defaultSettingsKeys[i]] = defaultSettings[defaultSettingsKeys[i]]; } } saveData(settings); } checkSettings(); // Update saved version to the version used in the script to prevent unnecessary notifications if (compareVer(settings.version, currentVersion)) { settings.version = currentVersion; saveData(settings); } // Functions to get/change data from the console window.MSPFAe = { getSettings: () => { return settings; }, getSettingsString: (formatted) => { if (formatted) { console.log(JSON.stringify(settings, null, 4)); } else { console.log(JSON.stringify(settings)); } }, changeSettings: (newSettings) => { console.log('Settings updated'); console.log(settings); Object.assign(settings, newSettings); saveData(settings); }, changeSettingsString: (fullString) => { try { JSON.parse(fullString); } catch (err) { console.error(err); return; } settings = JSON.parse(fullString); checkSettings(); console.log(settings); } } // Delete any unchanged spoiler values or empty drafts if (location.pathname !== "/my/stories/pages/") { // Go through spoiler values and remove any that aren't unique Object.keys(settings.spoilerValues).forEach(adventure => { if (settings.spoilerValues[adventure].open === "Show" && settings.spoilerValues[adventure].close === "Hide") { delete settings.spoilerValues[adventure]; } else if (settings.spoilerValues[adventure].open === '' && settings.spoilerValues[adventure].close === '') { delete settings.spoilerValues[adventure]; } }); // Go through and remove adventures with empty drafts Object.keys(settings.drafts).forEach(adventure => { if (Object.keys(settings.drafts[adventure]).length === 0) { delete settings.drafts[adventure]; } }); } const styleOptions = ["Standard", "Low Contrast", "Light", "Dark", "Felt", "Trickster", "Custom"]; const styleUrls = ['', '/css/theme1.css', '/css/theme2.css', '/css/?s=36237', '/css/theme4.css', '/css/theme5.css']; // Dropdown menu const myLink = document.querySelector('nav a[href="/my/"]'); if (myLink) { const dropDiv = document.createElement('div'); dropDiv.className = 'dropdown'; Object.assign(dropDiv.style, { position: 'relative', display: 'inline-block', backgroundColor: 'inherit' }); const 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'; }); myLink.parentNode.insertBefore(dropDiv, myLink); dropDiv.appendChild(myLink); dropDiv.appendChild(dropContent); const dLinks = []; dLinks[0] = [ 'Messages', 'My Adventures', 'Settings' ]; dLinks[1] = [ '/my/messages/', '/my/stories/', '/my/settings/' ]; for (let i = 0; i < dLinks[0].length; i++) { const newLink = document.createElement('a'); newLink.textContent = dLinks[0][i]; newLink.href = dLinks[1][i]; dropContent.appendChild(newLink); } // Append "My Profile" to the dropdown list if you're signed in, or "Login" if you're not pageLoad(() => { if (window.MSPFA) { if (window.MSPFA.me.n) { const newLink = document.createElement('a'); newLink.textContent = "My Profile"; newLink.href = `/user/?u=${window.MSPFA.me.i}`; dropContent.appendChild(newLink); return true; } } }); } const hashSearch = location.href.replace(location.origin + location.pathname, '').replace(location.search, ''); if (hashSearch !== '') { pageLoad(() => { const idElement = document.querySelector(hashSearch); if (idElement) { document.querySelector(hashSearch).scrollIntoView(); return true; } }, 1); } // Error reloading window.addEventListener("load", () => { // Reload the page if 502 CloudFlare error page appears if (settings.auto502 && document.querySelector('.cf-error-overview')) { window.location.reload(); } // Wait five seconds, then refresh the page if (document.body.textContent === "Your client is sending data to MSPFA too quickly. Wait a moment before continuing.") { setTimeout(() => { window.location.reload(); }, 5000); } }); // Message that shows when you first get the script const showIntroDialog = () => { const msg = window.MSPFA.parseBBCode('Hi! Thanks for installing this script!\n\nBe sure to check the [url=https://greasyfork.org/en/scripts/396798-mspfa-extras#additional-info]GreasyFork[/url] page to see a full list of features, and don\'t forget to check out your [url=https://mspfa.com/my/settings/#extraSettings]settings[/url] page to tweak things to how you want.\n\nIf you have any suggestions, or you find a bug, please be sure to let me know on Discord at [url=discord://discordapp.com/users/277928549866799125]@seymour schlong#3669[/url].\n\n[size=12]This dialog will only appear once. To view it again, click "View Script Message" at the bottom of the site.[/size]'); window.MSPFA.dialog("MSPFA extras message", msg, ["Okay"]); } // Check for updates by comparing currentVersion to text data from an adventure that has update text and info const checkForUpdates = (evt) => { window.MSPFA.request(0, { do: "story", s: "36596" }, story => { if (typeof story !== "undefined") { const ver = settings.version.split(/\./); const newVer = story.p[1].c.split(/\./); // compare versions if (compareVer(settings.version, story.p[1].c) || (evt && evt.type === 'click')) { const msg = window.MSPFA.parseBBCode(story.p[1].b); settings.version = story.p[1].c; saveData(settings); window.MSPFA.dialog(`MSPFA extras update! (${story.p[1].c})`, msg, ["Opt-out", "Dismiss", "Update"], (output, form) => { if (output === "Update") { window.open('https://greasyfork.org/en/scripts/396798-mspfa-extras', '_blank').focus(); } else if (output === "Opt-out") { settings.autoUpdate = false; saveData(settings); } }); } } }); }; // Check for updates and show intro dialog if needed pageLoad(() => { if (window.MSPFA) { if (settings.autoUpdate) { checkForUpdates(); } if (!settings.intro) { showIntroDialog(); settings.intro = true; saveData(settings); } return true; } }); const linkColour = document.createElement('a'); linkColour.href = "/"; linkColour.id = "linkColour"; document.body.appendChild(linkColour); const details = document.querySelector('#details'); // Add 'link' at the bottom to show the intro dialog again const introLink = document.createElement('a'); introLink.textContent = 'View Script Message'; introLink.style = 'cursor: pointer; text-decoration: underline;'; introLink.style.color = getComputedStyle(linkColour).color; introLink.className = 'intro-link'; introLink.addEventListener('click', showIntroDialog); details.appendChild(introLink); // vbar!!!! const vbar = document.createElement('span'); Object.assign(vbar, {className: 'vbar', style: 'padding: 0 5px', textContent: '|'}); details.appendChild(vbar); // Add 'link' at the bottom to show the update dialog again const updateLink = document.createElement('a'); updateLink.textContent = 'View Update'; updateLink.style = 'cursor: pointer; text-decoration: underline;'; updateLink.style.color = getComputedStyle(linkColour).color; updateLink.className = 'intro-link'; updateLink.addEventListener('click', checkForUpdates); details.appendChild(updateLink); // vbar 2!!!! const vbar2 = document.createElement('span'); Object.assign(vbar2, {className: 'vbar', style: 'padding: 0 5px', textContent: '|'}); details.appendChild(vbar2); // if you really enjoy the script and has some extra moneys 🥺 const donateLink = document.createElement('a'); donateLink.textContent = 'Donate'; donateLink.href = 'https://ko-fi.com/ironbean'; donateLink.target = "blank"; details.appendChild(donateLink); // Theme stuff const theme = document.createElement('link'); Object.assign(theme, { id: 'theme', type: 'text/css', rel: 'stylesheet' }); const updateTheme = (src) => { theme.href = src; setTimeout(() => { introLink.style.color = getComputedStyle(linkColour).color; updateLink.style.color = getComputedStyle(linkColour).color; }, 1500); } if (!document.querySelector('#theme') && !/^\/css\/|^\/js\//.test(location.pathname)) { document.querySelector('head').appendChild(theme); if (settings.night) { updateTheme('/css/?s=36237'); } else { updateTheme(settings.style == styleOptions.length - 1 ? settings.styleURL : styleUrls[settings.style]); } } // Dropdown menu and pixelated scaling const dropStyle = document.createElement('style'); const pixelFixText = 'img, .mspfalogo, .major, .arrow, #flashytitle, .heart, .fav, .notify, .edit, .rss, input, #loading { image-rendering: pixelated !important; }'; const dropStyleText = `#notification { z-index: 2; } .dropdown-content a { color: inherit; padding: 2px; text-decoration: underline; display: block;}`; if (!document.querySelector('#dropdown-style')) { dropStyle.id = 'dropdown-style'; dropStyle.textContent = dropStyleText + (settings.pixelFix ? ' '+pixelFixText : ''); //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;}'; document.querySelector('head').appendChild(dropStyle); } // Remove the current theme if the adventure has CSS (to prevent conflicts); pageLoad(() => { if (window.MSPFA) { if (window.MSPFA.story && window.MSPFA.story.y && (window.MSPFA.story.y.toLowerCase().includes('import') || window.MSPFA.story.y.includes('{'))) { updateTheme(''); return true; } } }); // Enabling night mode. document.querySelector('footer .mspfalogo').addEventListener('click', evt => { settings.night = !settings.night; saveData(settings); // Transition to make it feel nicer on the eyes dropStyle.textContent = dropStyleText + (settings.pixelFix ? ' '+pixelFixText : '') + ''; dropStyle.textContent = dropStyleText + (settings.pixelFix ? ' '+pixelFixText : '') + ' *{transition:1s}'; if (settings.night) { updateTheme('/css/?s=36237'); } else { updateTheme(settings.style == styleOptions.length - 1 ? settings.styleURL : styleUrls[settings.style]); } setTimeout(() => { dropStyle.textContent = dropStyleText + (settings.pixelFix ? ' '+pixelFixText : ''); }, 1000); console.log(`Night mode turned ${settings.night ? 'on' : 'off'}.`); }); if (location.pathname === "/" || location.pathname === "/preview/") { if (location.search) { // Automatic spoiler opening if (settings.autospoiler) { window.MSPFA.slide.push((p) => { document.querySelectorAll('#slide .spoiler:not(.open) > div:first-child > input').forEach(sb => sb.click()); }); } // Show creation date 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; } }); // Hash scrolling and opening infobox or commmentbox if (['#infobox', '#commentbox', '#latestpages'].indexOf(hashSearch) !== -1) { pageLoad(() => { if (document.querySelector(hashSearch)) { if (hashSearch === '#infobox') { document.querySelector('input[data-open="Show Adventure Info"]').click(); } else if (hashSearch === '#commentbox') { document.querySelector('input[data-open="Show Comments"]').click(); } else if (hashSearch === '#latestpages') { document.querySelector('input[data-open="Show Adventure Info"]').click(); document.querySelector('input[data-open="Show Latest Pages"]').click(); } return true; } }); } // Attempt to fix text errors if (settings.textFix) { pageLoad(() => { if (window.MSPFA.story && window.MSPFA.story.p) { // russian/bulgarian is not possible =( const currentPage = parseInt(/^\?s(?:.*?)&p=([\d]*)$/.exec(location.search)[1]); const 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; } }); } // Turn buttons into links const pageButton = document.createElement('button'); const pageLink = document.createElement('a'); const searchContent = location.search.split('&p='); pageLink.href = `/my/stories/pages/${searchContent[0]}#p${searchContent[1].split('#')[0]}`; pageButton.className = 'pages edit major'; pageButton.type = 'button'; pageButton.title = 'Edit Pages'; pageLink.style.marginRight = '9.5px'; pageButton.style.backgroundImage = 'url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAL1JREFUOI1jYKAQMCJzugoL/+NSePPhQ4a569YxoouzoAuU9vUx3AoLY1BbtYqBgYGB4d21a8jS/9ENwTBg/ezZDNpQGh2oy8szJAcFoRiCYUBgairDrd27Gdz9/Bh+vH2LInfn+nUMQ3G6YOemTRiKsQEMA9z9/Bie7N7NYG9tjaGYKBfs3LSJQZuBgeHg0aPkucDe2prhDSUuOHj06Ih3wdw5cxgYZGQYGObMId0FNx8+JKhBQlGRKIOJBgBpTFVYzRG//QAAAABJRU5ErkJggg==")'; pageLink.appendChild(pageButton); pageLoad(() => { const infoButton = document.querySelector('.edit.major'); if (infoButton) { pageLoad(() => { if (window.MSPFA.me.i) { infoButton.title = "Edit Info"; infoButton.parentNode.insertBefore(pageLink, infoButton); addLink(infoButton, `/my/stories/info/${searchContent[0]}`); pageButton.style.display = document.querySelector('.edit.major:not(.pages)').style.display; return true; } }); addLink(document.querySelector('.rss.major'), `/rss/${searchContent[0]}`); return true; } }); window.addEventListener('load', evt => { const favButton = document.querySelector('.fav'); window.MSPFA.slide.push(p => { const newSearch = location.search.split('&p='); pageLink.href = `/my/stories/pages/${newSearch[0]}#p${newSearch[1].split('#')[0]}`; }); }); // Add "Reply" button to comment gear document.body.addEventListener('click', evt => { if (evt.toElement.classList.contains('gear')) { const userID = evt.path[2].classList[2].replace('u', ''); const reportButton = document.querySelector('#dialog button[data-value="Report"]'); const replyButton = document.createElement('button'); replyButton.classList.add('major'); replyButton.type = 'submit'; replyButton.setAttribute('data-value', 'Reply'); replyButton.textContent = 'Reply'; replyButton.style = 'margin-right: 9.5px'; reportButton.parentNode.insertBefore(replyButton, reportButton); replyButton.addEventListener('click', evt => { document.querySelector('#dialog button[data-value="Cancel"]').click(); const commentBox = document.querySelector('#commentbox textarea'); commentBox.value = `[user]${userID}[/user], ${commentBox.value}`; commentBox.focus(); // Weird bug where if you have JS console open it opens debugger? }); } else return; });/**/ } } else if (location.pathname === "/my/settings/") { // Custom settings const saveBtn = document.querySelector('#savesettings'); const table = document.querySelector("#editsettings tbody"); let saveTr = table.querySelectorAll("tr"); saveTr = saveTr[saveTr.length - 1]; const headerTr = document.createElement('tr'); const header = document.createElement('th'); Object.assign(header, { id: 'extraSettings', textContent: 'Extra Settings' }); headerTr.appendChild(header); const moreTr = document.createElement('tr'); const more = document.createElement('td'); more.textContent = "* This only applies to a select few older adventures that have had their text corrupted. Some punctuation is fixed, as well as regular characters with accents. Currently only some spanish/french is fixable. Russian/Bulgarian is not possible."; moreTr.appendChild(more); const settingsTr = document.createElement('tr'); const localMsg = document.createElement('span'); const 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!"; const plusTable = document.createElement('table'); const plusTbody = document.createElement('tbody'); plusTable.appendChild(plusTbody); settingsTd.appendChild(localMsg); settingsTd.appendChild(newBr()); settingsTd.appendChild(newBr()); settingsTd.appendChild(plusTable); settingsTr.appendChild(settingsTd); plusTable.style = "text-align: center;"; // Create checkbox (soooo much better) const createCheckbox = (text, checked) => { const optionTr = plusTbody.insertRow(plusTbody.childNodes.length); const optionTextTd = optionTr.insertCell(0); const optionInputTd = optionTr.insertCell(1); const optionInput = document.createElement('input'); optionInputTd.appendChild(optionInput); optionTextTd.textContent = text; optionInput.type = "checkbox"; optionInput.checked = checked; return optionInput; } const spoilerInput = createCheckbox("Automatically open spoilers:", settings.autospoiler); const errorInput = createCheckbox("Automatically reload Cloudflare 502 error pages:", settings.auto502); const updateInput = createCheckbox("Automatically check for updates:", settings.autoUpdate); const pixelFixInput = createCheckbox("Change pixel scaling to nearest neighbour:", settings.pixelFix); const textFixInput = createCheckbox("Attempt to fix text errors (experimental)*:", settings.textFix); const cssTr = plusTbody.insertRow(plusTbody.childNodes.length); const cssTextTd = cssTr.insertCell(0); const cssSelectTd = cssTr.insertCell(1); const cssSelect = document.createElement('select'); cssSelectTd.appendChild(cssSelect); cssTextTd.textContent = "Change style:"; const customTr = plusTbody.insertRow(plusTbody.childNodes.length); const customTextTd = customTr.insertCell(0); const customCssTd = customTr.insertCell(1); const customCssInput = document.createElement('input'); customCssTd.appendChild(customCssInput); customTextTd.textContent = "Custom CSS URL:"; customCssInput.style.width = "99px"; customCssInput.value = settings.styleURL; styleOptions.forEach(o => cssSelect.appendChild(new Option(o, o))); saveTr.parentNode.insertBefore(headerTr, saveTr); saveTr.parentNode.insertBefore(settingsTr, saveTr); saveTr.parentNode.insertBefore(moreTr, saveTr); cssSelect.selectedIndex = settings.style; const buttonSpan = document.createElement('span'); const draftButton = document.createElement('input'); const spoilerButton = document.createElement('input'); draftButton.style = 'margin: 0 9.5px;'; draftButton.value = 'Clear Drafts'; draftButton.className = 'major'; draftButton.type = 'button'; spoilerButton.value = 'Clear Spoiler Values'; spoilerButton.className = 'major'; spoilerButton.type = 'button'; buttonSpan.appendChild(draftButton); buttonSpan.appendChild(spoilerButton); settingsTd.appendChild(buttonSpan); draftButton.addEventListener('click', () => { window.MSPFA.dialog('Delete all Drafts?', window.MSPFA.parseBBCode(`Doing this will delete all drafts saved for [b]${Object.keys(settings.drafts).length}[/b] adventure(s).\nAre you sure? This action is irreversible.`), ["Yes", "No"], (output, form) => { if (output === "Yes") { setTimeout(() => { window.MSPFA.dialog('Delete all Drafts?', document.createTextNode('Are you really sure?'), ["No", "Yes"], (output, form) => { if (output === "Yes") { settings.drafts = {}; saveData(settings); } }); }, 1); } }); }); spoilerButton.addEventListener('click', () => { window.MSPFA.dialog('Delete all Spoiler Values?', window.MSPFA.parseBBCode(`Doing this will delete all spoiler values saved for [b]${Object.keys(settings.spoilerValues).length}[/b] adventure(s).\nAre you sure? This action is irreversible.`), ["Yes", "No"], (output, form) => { if (output === "Yes") { settings.spoilerValues = {}; saveData(settings); } }); }); // Add event listeners plusTbody.querySelectorAll('input, select').forEach(elm => { elm.addEventListener("change", () => { saveBtn.disabled = false; }); }); saveBtn.addEventListener('mouseup', () => { settings.autospoiler = spoilerInput.checked; settings.style = cssSelect.selectedIndex; settings.styleURL = customCssInput.value; settings.auto502 = errorInput.checked; settings.textFix = textFixInput.checked; settings.pixelFix = pixelFixInput.checked; settings.autoUpdate = updateInput.checked; settings.night = false; console.log(settings); saveData(settings); updateTheme(settings.style == styleOptions.length - 1 ? settings.styleURL : styleUrls[settings.style]); dropStyle.textContent = dropStyleText + (settings.pixelFix ? ' '+pixelFixText : ''); dropStyle.textContent = dropStyleText + (settings.pixelFix ? ' '+pixelFixText : '') + ' *{transition:1s}'; setTimeout(() => { dropStyle.textContent = dropStyleText + (settings.pixelFix ? ' '+pixelFixText : ''); }, 1000); }); } else if (location.pathname === "/my/messages/") { // New buttons const 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 => { const temp = document.querySelectorAll('#messages > tr'); const msgs = []; for (let i = temp.length - 1; i >= 0; i--) { msgs.push(temp[i]); } const 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. const del = document.querySelector('#deletemsgs'); del.parentNode.appendChild(newBr()); del.parentNode.appendChild(selRead); del.parentNode.appendChild(selDupe); } else if (location.pathname === "/my/messages/new/" && location.search) { // Auto-fill user when linked from a user page const recipientInput = document.querySelector('#addrecipient'); recipientInput.value = location.search.replace('?u=', ''); pageLoad(() => { const recipientButton = document.querySelector('#addrecipientbtn'); if (recipientButton) { recipientButton.click(); if (recipientInput.value === "") { // If the button press doesn't work return true; } } }); } else if (location.pathname === "/my/stories/") { // Add links to buttons pageLoad(() => { const adventures = document.querySelectorAll('#stories tr'); if (adventures.length > 0) { adventures.forEach(story => { const buttons = story.querySelectorAll('input.major'); const id = story.querySelector('a').href.replace('https://mspfa.com/', '').replace('&p=1', ''); if (id) { addLink(buttons[0], `/my/stories/info/${id}`); addLink(buttons[1], `/my/stories/pages/${id}`); } }); return true; } }); // Add user guides const guides = ["A Guide To Uploading Your Comic To MSPFA", "MSPFA Etiquette", "Fanventure Guide for Dummies", "CSS Guide", "HTML and CSS Things", ]; const links = ["https://docs.google.com/document/d/17QI6Cv_BMbr8l06RrRzysoRjASJ-ruWioEtVZfzvBzU/edit?usp=sharing", "/?s=27631", "/?s=29299", "/?s=21099", "/?s=23711"]; const authors = ["Farfrom Tile", "Radical Dude 42", "nzar", "MadCreativity", "seymour schlong"]; const parentTd = document.querySelector('.container > tbody > tr:last-child > td'); const unofficial = parentTd.querySelector('span'); unofficial.textContent = "Unofficial Guides"; const guideTable = document.createElement('table'); const 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++) { const guideTr = guideTbody.insertRow(i); const guideTd = guideTr.insertCell(0); const guideLink = document.createElement('a'); guideLink.href = links[i]; guideLink.textContent = guides[i]; guideLink.className = "major"; guideTd.appendChild(guideLink); guideTd.appendChild(newBr()); guideTd.appendChild(document.createTextNode('by '+authors[i])); guideTd.appendChild(newBr()); guideTd.appendChild(newBr()); } } else if (location.pathname === "/my/stories/info/" && location.search) { // Button links 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) { const adventureID = /\?s=(\d{5})/.exec(location.search)[1]; if (!settings.drafts[adventureID]) { settings.drafts[adventureID] = {} } // Button links addLink(document.querySelector('#editinfo'), `/my/stories/info/?s=${adventureID}`); // Default spoiler values const replaceButton = document.querySelector('#replaceall'); const spoilerButton = document.createElement('input'); spoilerButton.classList.add('major'); spoilerButton.value = 'Default Spoiler Values'; spoilerButton.type = 'button'; replaceButton.parentNode.insertBefore(spoilerButton, replaceButton); replaceButton.parentNode.insertBefore(newBr(), replaceButton); replaceButton.parentNode.insertBefore(newBr(), replaceButton); const spoilerSpan = document.createElement('span'); const spoilerOpen = document.createElement('input'); const spoilerClose = document.createElement('input'); spoilerSpan.appendChild(document.createTextNode('Open button text:')); spoilerSpan.appendChild(newBr()); spoilerSpan.appendChild(spoilerOpen); spoilerSpan.appendChild(newBr()); spoilerSpan.appendChild(newBr()); spoilerSpan.appendChild(document.createTextNode('Close button text:')); spoilerSpan.appendChild(newBr()); spoilerSpan.appendChild(spoilerClose); if (!settings.spoilerValues[adventureID]) { settings.spoilerValues[adventureID] = { open: 'Show', close: 'Hide' } } spoilerOpen.value = settings.spoilerValues[adventureID].open; spoilerClose.value = settings.spoilerValues[adventureID].close; spoilerButton.addEventListener('click', evt => { window.MSPFA.dialog('Default Spoiler Values', spoilerSpan, ['Save', 'Cancel'], (output, form) => { if (output === 'Save') { settings.spoilerValues[adventureID].open = spoilerOpen.value === '' ? 'Show' : spoilerOpen.value; settings.spoilerValues[adventureID].close = spoilerClose.value === '' ? 'Hide' : spoilerClose.value; if (settings.spoilerValues[adventureID].open === 'Show' && settings.spoilerValues[adventureID].close === 'Hide') { delete settings.spoilerValues[adventureID]; } saveData(settings); } }); }); document.querySelector('input[title="Spoiler"]').addEventListener('click', evt => { document.querySelector('#dialog input[name="open"]').value = settings.spoilerValues[adventureID].open; document.querySelector('#dialog input[name="close"]').value = settings.spoilerValues[adventureID].close; document.querySelector('#dialog input[name="open"]').placeholder = settings.spoilerValues[adventureID].open; document.querySelector('#dialog input[name="close"]').placeholder = settings.spoilerValues[adventureID].close; }); // Buttonless spoilers const flashButton = document.querySelector('input[title="Flash'); const newSpoilerButton = document.createElement('input'); newSpoilerButton.setAttribute('data-tag', 'Buttonless Spoiler'); newSpoilerButton.title = 'Buttonless Spoiler'; newSpoilerButton.type = 'button'; newSpoilerButton.style = 'background-position: -66px -88px; background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAAAXNSR0IArs4c6QAAARNJREFUOI3V1LFqhEAQBuDfPaxcLkFwsbhgJ2STWlhQG4twvT7IWecZ8iB3L5In2BfIbaVNglaipMkQPAiBMAnkr2Zg52OZYoD/lg0AKKVMkiQmiqK7russ9cMwvPm+v03T9MHzvKtlWUD1OI5nrXVzOUO9AICyLA9cPzXGfFp1XR+54JWllDJcMFkCAIQQr1wwWQIAiqJouWCyBADM8/zCBa+sMAy3XDBZAgCqqnrkgsmiVVxzwSsrjuNbLpgsAQB5nt9zwWSJj77hgskSAOCcO3GpnNbX+Y0jJL57+NNsAEBKuTPGtFrrxlp7yrLsYIxp+74/B0Fws9/vn6SUu2maPKqdc891XR8vZwDAWvsHe+bOOy65ZXyuZ2jeAAAAAElFTkSuQmCC");'; newSpoilerButton.addEventListener('click', evt => { const bbe = document.querySelector('#bbtoolbar').parentNode.querySelector('textarea'); if (bbe) { bbe.focus(); const start = bbe.selectionStart; const end = bbe.selectionEnd; bbe.value = bbe.value.slice(0, start) + '<div class="spoiler"><div>' + bbe.value.slice(start, end) + '</div></div>' + bbe.value.slice(end); bbe.selectionStart = start + 26; bbe.selectionEnd = end + 26; } }); flashButton.parentNode.insertBefore(newSpoilerButton, flashButton); // Open preview in new tab with middle mouse document.body.addEventListener('mouseup', evt => { if (evt.toElement.value === "Preview" && evt.button === 1) { evt.toElement.click(); // TODO: Find a way to prevent the middle mouse scroll after clicking there. evt.preventDefault(); return false; } }); // -- Drafts -- // Accessing draft text const accessDraftsButton = document.createElement('input'); accessDraftsButton.classList.add('major'); accessDraftsButton.value = 'Saved Drafts'; accessDraftsButton.type = 'button'; replaceButton.parentNode.insertBefore(accessDraftsButton, replaceButton); accessDraftsButton.parentNode.insertBefore(newBr(), replaceButton); accessDraftsButton.parentNode.insertBefore(newBr(), replaceButton); accessDraftsButton.addEventListener('click', () => { const draftDialog = window.MSPFA.parseBBCode('Use the textbox below to copy out the data and save to a file somewhere else.\nYou can also paste in data to replace the current drafts to ones stored there.'); const draftInputTextarea = document.createElement('textarea'); draftInputTextarea.placeholder = 'Paste your draft data here'; draftInputTextarea.style = 'width: 100%; box-sizing: border-box; resize: vertical;'; draftInputTextarea.rows = 8; draftDialog.appendChild(newBr()); draftDialog.appendChild(newBr()); draftDialog.appendChild(draftInputTextarea); setTimeout(() => { draftInputTextarea.focus(); draftInputTextarea.selectionStart = 0; draftInputTextarea.selectionEnd = 0; draftInputTextarea.scrollTop = 0; }, 1); draftInputTextarea.value = JSON.stringify(settings.drafts[adventureID], null, 4); window.MSPFA.dialog('Saved Drafts', draftDialog, ['Load Draft', 'Cancel'], (output, form) => { if (output === "Load Draft") { if (draftInputTextarea.value === '') { setTimeout(() => { window.MSPFA.dialog('Saved Drafts', window.MSPFA.parseBBCode('Are you sure you want to delete this adventure\'s draft data?\nMake sure you have it saved somewhere!'), ["Delete", "Cancel"], (output, form) => { if (output === "Delete") { settings.drafts[adventureID] = {}; saveData(settings); } }); }, 1); } else if (draftInputTextarea.value !== JSON.stringify(settings.drafts[adventureID], null, 4)) { setTimeout(() => { window.MSPFA.dialog('Saved Drafts', window.MSPFA.parseBBCode('Are you sure you want to load this draft data?\nAll previous draft data for this adventure will be lost!'), ["Load", "Cancel"], (output, form) => { if (output === "Load") { let newData = {}; try { // Just in case the data given is invalid. newData = JSON.parse(draftInputTextarea.value); } catch (err) { console.error(err); setTimeout(() => { window.MSPFA.dialog('Error', window.MSPFA.parseBBCode('The entered data is invalid.'), ["Okay"]); }, 1); return; } settings.drafts[adventureID] = newData; saveData(settings); } }); }, 1); } } }); }); // Draft stuff const msg = document.createElement('span'); msg.appendChild(document.createTextNode('Command:')); msg.appendChild(document.createElement('br')); const commandInput = document.createElement('input'); commandInput.style = 'width: 100%; box-sizing: border-box;'; commandInput.readOnly = true; commandInput.value = 'yes'; msg.appendChild(commandInput); msg.appendChild(document.createElement('br')); msg.appendChild(document.createElement('br')); msg.appendChild(document.createTextNode('Body:')); const bodyInput = document.createElement('textarea'); bodyInput.style = 'width: 100%; box-sizing: border-box; resize: vertical;'; bodyInput.readOnly = true; bodyInput.rows = 8; bodyInput.textContent = ''; msg.appendChild(bodyInput); const showDraftDialog = (pageNum) => { const pageElement = document.querySelector(`#p${pageNum}`); let shownMessage = msg; let optionButtons = []; const commandElement = pageElement.querySelector('input[name="cmd"]'); const pageContentElement = pageElement.querySelector('textarea[name="body"]'); if (typeof settings.drafts[adventureID][pageNum] === "undefined") { shownMessage = document.createTextNode('There is no draft saved for this page.'); optionButtons = ["Save New", "Close"]; } else { commandInput.value = settings.drafts[adventureID][pageNum].command; bodyInput.textContent = settings.drafts[adventureID][pageNum].pageContent; optionButtons = ["Save New", "Load", "Delete", "Close"]; } window.MSPFA.dialog(`Page ${pageNum} Draft`, shownMessage, optionButtons, (output, form) => { if (output === "Save New") { if (typeof settings.drafts[adventureID][pageNum] === "undefined") { settings.drafts[adventureID][pageNum] = { command: commandElement.value, pageContent: pageContentElement.value } saveData(settings); } else { setTimeout(() => { window.MSPFA.dialog('Overwrite current draft?', document.createTextNode('Doing this will overwrite your current draft with what is currently written in the page box. Are you sure?'), ["Yes", "No"], (output, form) => { if (output === "Yes") { settings.drafts[adventureID][pageNum] = { command: commandElement.value, pageContent: pageContentElement.value } saveData(settings); } }); }, 1); } } else if (output === "Load") { if (pageContentElement.value === '' && (commandElement.value === '' || commandElement.value === document.querySelector('#defaultcmd').value)) { commandElement.value = settings.drafts[adventureID][pageNum].command; pageContentElement.value = settings.drafts[adventureID][pageNum].pageContent; pageElement.querySelector('input[value="Save"]').disabled = false; } else { setTimeout(() => { window.MSPFA.dialog('Overwrite current page?', document.createTextNode('Doing this will overwrite the page\'s content with what is currently written in the draft. Are you sure?'), ["Yes", "No"], (output, form) => { if (output === "Yes") { commandElement.value = settings.drafts[adventureID][pageNum].command; pageContentElement.value = settings.drafts[adventureID][pageNum].pageContent; pageElement.querySelector('input[value="Save"]').disabled = false; } }); }, 1); } } else if (output === "Delete") { setTimeout(() => { window.MSPFA.dialog('Delete this draft?', document.createTextNode('This action is unreversable! Are you sure?'), ["Yes", "No"], (output, form) => { if (output === "Yes") { delete settings.drafts[adventureID][pageNum]; saveData(settings); } }); }, 1); } }); } const createDraftButton = (form) => { const draftButton = document.createElement('input'); draftButton.className = 'major draft'; draftButton.type = 'button'; draftButton.value = 'Draft'; draftButton.style = 'margin-right: 9.5px;'; draftButton.addEventListener('click', () => { showDraftDialog(form.id.replace('p', '')); }); return draftButton; } pageLoad(() => { let allPages = document.querySelectorAll('#storypages form:not(#newpage)'); if (allPages.length !== 0) { allPages.forEach(form => { const prevButton = form.querySelector('input[name="preview"]'); prevButton.parentNode.insertBefore(createDraftButton(form), prevButton); }); document.querySelector('input[value="Add"]').addEventListener('click', () => { allPages = document.querySelectorAll('#storypages form:not(#newpage)'); const form = document.querySelector(`#p${allPages.length}`); const prevButton = form.querySelector('input[name="preview"]'); prevButton.parentNode.insertBefore(createDraftButton(form), prevButton); }); return true; } }); /* // Removed because apparently MSPFA already does this fine! if (hashSearch) { pageLoad(() => { const element = document.querySelector(hashSearch); if (element) { if (element.style.display === "none") { element.style = ''; } return true; } }); }/**/ } else if (location.pathname === "/user/") { const id = location.search.slice(3); const statAdd = []; // Button links pageLoad(() => { const msgButton = document.querySelector('#sendmsg'); if (msgButton) { addLink(msgButton, `/my/messages/new/${location.search}`); addLink(document.querySelector('#favstories'), `/favs/${location.search}`); return true; } }); // Add extra user stats pageLoad(() => { if (window.MSPFA) { const stats = document.querySelector('#userinfo table'); const joinTr = stats.insertRow(1); const joinTextTd = joinTr.insertCell(0); joinTextTd.appendChild(document.createTextNode("Account created:")); const joinDate = joinTr.insertCell(1); const joinTime = document.createElement('b'); joinTime.textContent = "Loading..."; joinDate.appendChild(joinTime); const advCountTr = stats.insertRow(2); const advTextTd = advCountTr.insertCell(0); advTextTd.appendChild(document.createTextNode("Adventures created:")); const advCount = advCountTr.insertCell(1); const advCountText = document.createElement('b'); advCountText.textContent = "Loading..."; advCount.appendChild(advCountText); if (statAdd.indexOf('date') === -1) { window.MSPFA.request(0, { do: "user", u: id }, user => { if (typeof user !== "undefined") { statAdd.push('date'); joinTime.textContent = new Date(user.d).toString().split(' ').splice(1, 4).join(' '); } }); } if (statAdd.indexOf('made') === -1) { window.MSPFA.request(0, { do: "editor", u: id }, s => { if (typeof s !== "undefined") { statAdd.push('made'); advCountText.textContent = s.length; } }); } if (document.querySelector('#favstories').style.display !== 'none' && statAdd.indexOf('fav') === -1) { statAdd.push('fav'); const favCountTr = stats.insertRow(3); const favTextTd = favCountTr.insertCell(0); favTextTd.appendChild(document.createTextNode("Adventures favorited:")); const favCount = favCountTr.insertCell(1); const favCountText = document.createElement('b'); favCountText.textContent = "Loading..."; window.MSPFA.request(0, { do: "favs", u: id }, s => { if (typeof s !== "undefined") { favCountText.textContent = s.length; } }); favCount.appendChild(favCountText); } return true; } }); } else if (location.pathname === "/favs/" && location.search) { // Button links pageLoad(() => { const stories = document.querySelectorAll('#stories tr'); let favCount = 0; if (stories.length > 0) { stories.forEach(story => { favCount++; const 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 true; } }); addLink(story.querySelector('.rss.major'), `/rss/${id}`); }); // Fav count const username = document.querySelector('#username'); username.parentNode.appendChild(newBr()); username.parentNode.appendChild(newBr()); username.parentNode.appendChild(document.createTextNode(`Favorited adventures: ${favCount}`)); return true; } }); } else if (location.pathname === "/search/" && location.search) { // Character and word statistics const statTable = document.createElement('table'); const statTbody = document.createElement('tbody'); const statTr = statTbody.insertRow(0); const charCount = statTr.insertCell(0); const wordCount = statTr.insertCell(0); const statParentTr = document.querySelector('#pages').parentNode.parentNode.insertRow(2); const statParentTd = statParentTr.insertCell(0); const statHeaderTr = statTbody.insertRow(0); const 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')) { const 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") { const pageContent = []; story.p.forEach(p => { pageContent.push(p.c); pageContent.push(p.b); }); const 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], '') .replace(/<(.*?)>/g, ''); wordCount.textContent = `Word count: ${storyText.split(/ +/g).length}`; charCount.textContent = `Character count: ${storyText.replace(/ +/g, '').length}`; } }); return true; } }); } })();