MSPFA extras

Adds custom quality of life features to MSPFA.

当前为 2020-08-08 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name MSPFA extras
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.5.2
  5. // @description Adds custom quality of life features to MSPFA.
  6. // @author seymour schlong
  7. // @icon https://raw.githubusercontent.com/GrantGryczan/MSPFA/master/www/images/ico.svg
  8. // @icon64 https://raw.githubusercontent.com/GrantGryczan/MSPFA/master/www/images/ico.svg
  9. // @match https://mspfa.com/
  10. // @match https://mspfa.com/*/
  11. // @match https://mspfa.com/*/?*
  12. // @match https://mspfa.com/?s=*
  13. // @match https://mspfa.com/my/*
  14. // @grant none
  15. // ==/UserScript==
  16.  
  17.  
  18. (function() {
  19. 'use strict';
  20.  
  21. const currentVersion = "1.5.2";
  22. console.log(`MSPFA extras script v${currentVersion} by seymour schlong`);
  23.  
  24. /**
  25. * https://github.com/GrantGryczan/MSPFA/projects/1?fullscreen=true
  26. * Github to-do completion list
  27. *
  28. * https://github.com/GrantGryczan/MSPFA/issues/26 - Dropdown menu - February 23rd, 2020
  29. * https://github.com/GrantGryczan/MSPFA/issues/18 - MSPFA themes - February 23rd, 2020
  30. * https://github.com/GrantGryczan/MSPFA/issues/32 - Adventure creation dates - February 23rd, 2020
  31. * https://github.com/GrantGryczan/MSPFA/issues/32 - User creation dates - February 23rd, 2020
  32. * https://github.com/GrantGryczan/MSPFA/issues/40 - Turn certain buttons into links - July 21st, 2020
  33. * https://github.com/GrantGryczan/MSPFA/issues/41 - Word and character count - July 21st, 2020
  34. * https://github.com/GrantGryczan/MSPFA/issues/57 - Default spoiler values - August 7th, 2020
  35. * https://github.com/GrantGryczan/MSPFA/issues/62 - Buttonless spoilers - August 7th, 2020
  36. *
  37. * Note: At this point, I think I've done pretty much all that I /can/ do from the Github list. However, that doesn't mean I'll stop working on this.
  38. * So long as people have ideas for features to add, I can continue adding things.
  39. *
  40. * Extension to-do... maybe...
  41. *
  42. * 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.
  43. */
  44.  
  45. // A general function that allows for waiting until a certain element appears on the page.
  46. const pageLoad = (fn, length) => {
  47. const interval = setInterval(() => {
  48. if (fn()) clearInterval(interval);
  49. }, length ? length*1000 : 500);
  50. };
  51.  
  52. // Saves the options data for the script.
  53. const saveData = (data) => {
  54. localStorage.mspfaextra = JSON.stringify(data);
  55. };
  56.  
  57. // Encases an element within a link
  58. const addLink = (elm, url) => {
  59. const link = document.createElement('a');
  60. link.href = url;
  61. elm.parentNode.insertBefore(link, elm);
  62. link.appendChild(elm);
  63. };
  64.  
  65. // Returns true if version 2 is newer
  66. const compareVer = (ver1, ver2) => {
  67. ver1 = ver1.split(/\./); // current version
  68. ver2 = ver2.split(/\./); // new version
  69. ver1.push(0);
  70. ver2.push(0);
  71. if (parseInt(ver2[0]) > parseInt(ver1[0])) { // 1.x.x
  72. return true;
  73. } else if (parseInt(ver2[1]) > parseInt(ver1[1])) { // x.1.x
  74. return true;
  75. } else if (parseInt(ver2[2]) > parseInt(ver1[2])) { // x.x.1
  76. return true;
  77. }
  78. return false;
  79. }
  80.  
  81. // Easy br element
  82. const newBr = () => {
  83. return document.createElement('br');
  84. }
  85.  
  86. const settings = {};
  87. const defaultSettings = {
  88. autospoiler: false,
  89. style: 0,
  90. styleURL: "",
  91. night: false,
  92. auto502: true,
  93. textFix: false,
  94. pixelFix: false,
  95. intro: false,
  96. autoUpdate: true,
  97. version: currentVersion,
  98. spoilerValues: {}
  99. }
  100.  
  101. // Load any previous settings from localStorage
  102. if (localStorage.mspfaextra) {
  103. Object.assign(settings, JSON.parse(localStorage.mspfaextra));
  104. }
  105.  
  106. // If any settings are undefined, re-set to their default state. (For older users when new things get stored)
  107. const defaultSettingsKeys = Object.keys(defaultSettings);
  108. for (let i = 0; i < defaultSettingsKeys.length; i++) {
  109. if (typeof settings[defaultSettingsKeys[i]] === "undefined") {
  110. settings[defaultSettingsKeys[i]] = defaultSettings[defaultSettingsKeys[i]];
  111. }
  112. }
  113. saveData(settings);
  114.  
  115. // Update saved version to the version used in the script to prevent unnecessary notifications
  116. if (compareVer(settings.version, currentVersion)) {
  117. settings.version = currentVersion;
  118. saveData(settings);
  119. }
  120.  
  121. window.MSPFAe = {
  122. getSettings: () => {
  123. return settings;
  124. },
  125. getSettingsString: () => {
  126. console.log(JSON.stringify(settings));
  127. },
  128. changeSettings: (newSettings) => {
  129. console.log('Settings updated');
  130. console.log(settings);
  131. Object.assign(settings, newSettings);
  132. saveData(settings);
  133. }
  134. }
  135.  
  136. const styleOptions = ["Standard", "Low Contrast", "Light", "Dark", "Felt", "Trickster", "Custom"];
  137. const styleUrls = ['', '/css/theme1.css', '/css/theme2.css', '/css/?s=36237', '/css/theme4.css', '/css/theme5.css'];
  138.  
  139. // Dropdown menu
  140. const myLink = document.querySelector('nav a[href="/my/"]');
  141. if (myLink) {
  142. const dropDiv = document.createElement('div');
  143. dropDiv.className = 'dropdown';
  144. Object.assign(dropDiv.style, {
  145. position: 'relative',
  146. display: 'inline-block',
  147. backgroundColor: 'inherit'
  148. });
  149.  
  150. const dropContent = document.createElement('div');
  151. dropContent.className = 'dropdown-content';
  152. Object.assign(dropContent.style, {
  153. display: 'none',
  154. backgroundColor: 'inherit',
  155. position: 'absolute',
  156. textAlign: 'left',
  157. minWidth: '100px',
  158. marginLeft: '-5px',
  159. padding: '2px',
  160. zIndex: '1',
  161. borderRadius: '0 0 5px 5px'
  162. });
  163.  
  164. dropDiv.addEventListener('mouseenter', evt => {
  165. dropContent.style.display = 'block';
  166. dropContent.style.color = getComputedStyle(myLink).color;
  167. });
  168. dropDiv.addEventListener('mouseleave', evt => {
  169. dropContent.style.display = 'none';
  170. });
  171.  
  172. myLink.parentNode.insertBefore(dropDiv, myLink);
  173. dropDiv.appendChild(myLink);
  174. dropDiv.appendChild(dropContent);
  175.  
  176. const dLinks = [];
  177. dLinks[0] = [ 'Messages', 'My Adventures', 'Settings' ];
  178. dLinks[1] = [ '/my/messages/', '/my/stories/', '/my/settings/' ];
  179.  
  180. for (let i = 0; i < dLinks[0].length; i++) {
  181. const newLink = document.createElement('a');
  182. newLink.textContent = dLinks[0][i];
  183. newLink.href = dLinks[1][i];
  184. dropContent.appendChild(newLink);
  185. }
  186.  
  187. // Append "My Profile" to the dropdown list if you're signed in
  188. pageLoad(() => {
  189. if (window.MSPFA) {
  190. if (window.MSPFA.me.n) {
  191. const newLink = document.createElement('a');
  192. newLink.textContent = "My Profile";
  193. newLink.href = `/user/?u=${window.MSPFA.me.i}`;
  194. dropContent.appendChild(newLink);
  195. return true;
  196. }
  197. }
  198. });
  199. }
  200.  
  201. // Error reloading
  202. window.addEventListener("load", () => {
  203. // Reload the page if 502 CloudFlare error page appears
  204. if (settings.auto502 && document.querySelector('.cf-error-overview')) {
  205. window.location.reload();
  206. }
  207.  
  208. // Wait five seconds, then refresh the page
  209. if (document.body.textContent === "Your client is sending data to MSPFA too quickly. Wait a moment before continuing.") {
  210. setTimeout(() => {
  211. window.location.reload();
  212. }, 5000);
  213. }
  214. });
  215.  
  216. // Message that shows when you first get the script
  217. const showIntroDialog = () => {
  218. 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]');
  219. window.MSPFA.dialog("MSPFA extras message", msg, ["Okay"]);
  220. }
  221.  
  222. // Check for updates by comparing currentVersion to text data from an adventure that has update text and info
  223. const checkForUpdates = (evt) => {
  224. window.MSPFA.request(0, {
  225. do: "story",
  226. s: "36596"
  227. }, story => {
  228. if (typeof story !== "undefined") {
  229. const ver = settings.version.split(/\./);
  230. const newVer = story.p[1].c.split(/\./);
  231. // compare versions
  232. if (compareVer(settings.version, story.p[1].c) || (evt && evt.type === 'click')) {
  233. const msg = window.MSPFA.parseBBCode(story.p[1].b);
  234. settings.version = story.p[1].c;
  235. saveData(settings);
  236. window.MSPFA.dialog(`MSPFA extras update! (${story.p[1].c})`, msg, ["Opt-out", "Dismiss", "Update"], (output, form) => {
  237. if (output === "Update") {
  238. window.open('https://greasyfork.org/en/scripts/396798-mspfa-extras', '_blank').focus();
  239. } else if (output === "Opt-out") {
  240. settings.autoUpdate = false;
  241. saveData(settings);
  242. }
  243. });
  244. }
  245. }
  246. });
  247. };
  248.  
  249.  
  250. // Check for updates and show intro dialog if needed
  251. pageLoad(() => {
  252. if (window.MSPFA) {
  253. if (settings.autoUpdate) {
  254. checkForUpdates();
  255. }
  256.  
  257. if (!settings.intro) {
  258. showIntroDialog();
  259. settings.intro = true;
  260. saveData(settings);
  261. }
  262. return true;
  263. }
  264. });
  265.  
  266. const linkColour = document.createElement('a');
  267. linkColour.href = "/";
  268. linkColour.id = "linkColour";
  269. document.body.appendChild(linkColour);
  270. const details = document.querySelector('#details');
  271. // Add 'link' at the bottom to show the intro dialog again
  272. const introLink = document.createElement('a');
  273. introLink.textContent = 'View Script Message';
  274. introLink.style = 'cursor: pointer; text-decoration: underline;';
  275. introLink.style.color = getComputedStyle(linkColour).color;
  276. introLink.className = 'intro-link';
  277. introLink.addEventListener('click', showIntroDialog);
  278. details.appendChild(introLink);
  279.  
  280. // vbar!!!!
  281. const vbar = document.createElement('span');
  282. vbar.className = 'vbar';
  283. vbar.style = 'padding: 0 5px';
  284. vbar.textContent = '|';
  285. details.appendChild(vbar);
  286.  
  287. // Add 'link' at the bottom to show the update dialog again
  288. const updateLink = document.createElement('a');
  289. updateLink.textContent = 'View Update';
  290. updateLink.style = 'cursor: pointer; text-decoration: underline;';
  291. updateLink.style.color = getComputedStyle(linkColour).color;
  292. updateLink.className = 'intro-link';
  293. updateLink.addEventListener('click', checkForUpdates);
  294. details.appendChild(updateLink);
  295.  
  296. // Theme stuff
  297. const theme = document.createElement('link');
  298. Object.assign(theme, { id: 'theme', type: 'text/css', rel: 'stylesheet' });
  299. const updateTheme = (src) => {
  300. theme.href = src;
  301. setTimeout(() => {
  302. introLink.style.color = getComputedStyle(linkColour).color;
  303. updateLink.style.color = getComputedStyle(linkColour).color;
  304. }, 1500);
  305. }
  306. if (!document.querySelector('#theme') && !/^\/css\/|^\/js\//.test(location.pathname)) {
  307. document.querySelector('head').appendChild(theme);
  308. if (settings.night) {
  309. updateTheme('/css/?s=36237');
  310. } else {
  311. updateTheme(settings.style == styleOptions.length - 1 ? settings.styleURL : styleUrls[settings.style]);
  312. }
  313. }
  314.  
  315. // Dropdown menu and pixelated scaling
  316. const dropStyle = document.createElement('style');
  317. const pixelFixText = 'img, .mspfalogo, .major, .arrow, #flashytitle, .heart, .fav, .notify, .edit, .rss, input, #loading { image-rendering: pixelated !important; }';
  318. const dropStyleText = `#notification { z-index: 2; } .dropdown-content a { color: inherit; padding: 2px; text-decoration: underline; display: block;}`;
  319. if (!document.querySelector('#dropdown-style')) {
  320. dropStyle.id = 'dropdown-style';
  321. dropStyle.textContent = dropStyleText + (settings.pixelFix ? ' '+pixelFixText : '');
  322. //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;}';
  323.  
  324. document.querySelector('head').appendChild(dropStyle);
  325. }
  326.  
  327. // Remove the current theme if the adventure has CSS (to prevent conflicts);
  328. pageLoad(() => {
  329. if (window.MSPFA) {
  330. if (window.MSPFA.story && window.MSPFA.story.y && window.MSPFA.story.y.length > 0) {
  331. updateTheme('');
  332. }
  333. return true;
  334. }
  335. });
  336.  
  337. // Enabling night mode.
  338. pageLoad(() => {
  339. if (document.querySelector('footer .mspfalogo')) {
  340. document.querySelector('footer .mspfalogo').addEventListener('dblclick', evt => {
  341. if (evt.button === 0) {
  342. settings.night = !settings.night;
  343. saveData(settings);
  344.  
  345. if (settings.night) {
  346. updateTheme('/css/?s=36237');
  347. } else {
  348. updateTheme(settings.style == styleOptions.length - 1 ? settings.styleURL : styleUrls[settings.style]);
  349. }
  350.  
  351. dropStyle.textContent = dropStyleText + '';
  352. dropStyle.textContent = dropStyleText + '*{transition:1s}';
  353. setTimeout(() => {
  354. dropStyle.textContent = dropStyleText;
  355. }, 1000);
  356.  
  357. console.log(`Night mode turned ${settings.night ? 'on' : 'off'}.`);
  358. }
  359. });
  360. return true;
  361. }
  362. });
  363.  
  364. if (location.pathname === "/" || location.pathname === "/preview/") {
  365. // Automatic spoiler opening
  366. if (settings.autospoiler) {
  367. window.MSPFA.slide.push((p) => {
  368. document.querySelectorAll('#slide .spoiler:not(.open) > div:first-child > input').forEach(sb => sb.click());
  369. });
  370. }
  371.  
  372. if (location.search) {
  373. // Show creation date
  374. pageLoad(() => {
  375. if (document.querySelector('#infobox tr td:nth-child(2)')) {
  376. 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(' ')));
  377. return true;
  378. }
  379. });
  380.  
  381. // Attempt to fix text errors
  382. if (settings.textFix) {
  383. pageLoad(() => {
  384. if (window.MSPFA.story && window.MSPFA.story.p) {
  385. // russian/bulgarian is not possible =(
  386. const currentPage = parseInt(/^\?s(?:.*?)&p=([\d]*)$/.exec(location.search)[1]);
  387. const library = [
  388. ["&acirc;��", "'"],
  389. ["&Atilde;�", "Ñ"],
  390. ["&Atilde;&plusmn;", "ñ"],
  391. ["&Atilde;&sup3;", "ó"],
  392. ["&Atilde;&iexcl;", "á"],
  393. ["&Atilde;&shy;", "í"],
  394. ["&Atilde;&ordm;", "ú"],
  395. ["&Atilde;&copy;", "é"],
  396. ["&Acirc;&iexcl;", "¡"],
  397. ["&Acirc;&iquest;", "¿"],
  398. ["N&Acirc;&ordm;", "#"]
  399. ];
  400. // https://mspfa.com/?s=5280&p=51 -- unknown error
  401.  
  402. const replaceTerms = (p) => {
  403. library.forEach(term => {
  404. if (window.MSPFA.story.p[p]) {
  405. window.MSPFA.story.p[p].c = window.MSPFA.story.p[p].c.replace(new RegExp(term[0], 'g'), term[1]);
  406. window.MSPFA.story.p[p].b = window.MSPFA.story.p[p].b.replace(new RegExp(term[0], 'g'), term[1]);
  407. }
  408. });
  409. };
  410.  
  411. replaceTerms(currentPage-1);
  412.  
  413. window.MSPFA.slide.push(p => {
  414. replaceTerms(p);
  415. replaceTerms(p-2);
  416. });
  417. window.MSPFA.page(currentPage);
  418. return true;
  419. }
  420. });
  421. }
  422.  
  423. // Turn buttons into links
  424. pageLoad(() => {
  425. const infoButton = document.querySelector('.edit.major');
  426. if (infoButton) {
  427. pageLoad(() => {
  428. if (window.MSPFA.me.i) {
  429. addLink(infoButton, `/my/stories/info/${location.search.split('&p=')[0]}`);
  430. return;
  431. }
  432. });
  433. addLink(document.querySelector('.rss.major'), `/rss/${location.search.split('&p=')[0]}`);
  434. return true;
  435. }
  436. });
  437.  
  438. // Add "Reply" button to comment gear
  439. document.body.addEventListener('click', evt => {
  440. if (evt.toElement.classList.contains('gear')) {
  441. const userID = evt.path[2].classList[2].replace('u', '');
  442. const reportButton = document.querySelector('#dialog button[data-value="Report"]');
  443. const replyButton = document.createElement('button');
  444. replyButton.classList.add('major');
  445. replyButton.type = 'submit';
  446. replyButton.setAttribute('data-value', 'Reply');
  447. replyButton.textContent = 'Reply';
  448. replyButton.style = 'margin-right: 9px';
  449. reportButton.parentNode.insertBefore(replyButton, reportButton);
  450.  
  451. replyButton.addEventListener('click', evt => {
  452. document.querySelector('#dialog button[data-value="Cancel"]').click();
  453. const commentBox = document.querySelector('#commentbox textarea');
  454. commentBox.value = `[user]${userID}[/user], ${commentBox.value}`;
  455. commentBox.focus();
  456. // Weird bug where if you have JS console open it opens debugger?
  457. });
  458. } else return;
  459. });/**/
  460. }
  461. }
  462. else if (location.pathname === "/my/settings/") { // Custom settings
  463. const saveBtn = document.querySelector('#savesettings');
  464.  
  465. const table = document.querySelector("#editsettings tbody");
  466. let saveTr = table.querySelectorAll("tr");
  467. saveTr = saveTr[saveTr.length - 1];
  468.  
  469. const headerTr = document.createElement('tr');
  470. const header = document.createElement('th');
  471. Object.assign(header, { id: 'extraSettings', textContent: 'Extra Settings' });
  472. headerTr.appendChild(header);
  473.  
  474. const moreTr = document.createElement('tr');
  475. const more = document.createElement('td');
  476. 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.";
  477. moreTr.appendChild(more);
  478.  
  479. const settingsTr = document.createElement('tr');
  480. const localMsg = document.createElement('span');
  481. const settingsTd = document.createElement('td');
  482. 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!";
  483. const plusTable = document.createElement('table');
  484. const plusTbody = document.createElement('tbody');
  485. plusTable.appendChild(plusTbody);
  486. settingsTd.appendChild(localMsg);
  487. settingsTd.appendChild(newBr());
  488. settingsTd.appendChild(newBr());
  489. settingsTd.appendChild(plusTable);
  490. settingsTr.appendChild(settingsTd);
  491.  
  492. plusTable.style = "text-align: center;";
  493.  
  494. // Create checkbox (soooo much better)
  495. const createCheckbox = (text, checked) => {
  496. const optionTr = plusTbody.insertRow(plusTbody.childNodes.length);
  497. const optionTextTd = optionTr.insertCell(0);
  498. const optionInputTd = optionTr.insertCell(1);
  499. const optionInput = document.createElement('input');
  500. optionInputTd.appendChild(optionInput);
  501.  
  502. optionTextTd.textContent = text;
  503. optionInput.type = "checkbox";
  504. optionInput.checked = checked;
  505.  
  506. return optionInput;
  507. }
  508.  
  509. const spoilerInput = createCheckbox("Automatically open spoilers:", settings.autospoiler);
  510. const errorInput = createCheckbox("Automatically reload Cloudflare 502 error pages:", settings.auto502);
  511. const updateInput = createCheckbox("Automatically check for updates:", settings.autoUpdate);
  512. const pixelFixInput = createCheckbox("Change pixel scaling to nearest neighbour:", settings.pixelFix);
  513. const textFixInput = createCheckbox("Attempt to fix text errors (experimental)*:", settings.textFix);
  514.  
  515. const cssTr = plusTbody.insertRow(plusTbody.childNodes.length);
  516. const cssTextTd = cssTr.insertCell(0);
  517. const cssSelectTd = cssTr.insertCell(1);
  518. const cssSelect = document.createElement('select');
  519. cssSelectTd.appendChild(cssSelect);
  520.  
  521. cssTextTd.textContent = "Change style:";
  522.  
  523. const customTr = plusTbody.insertRow(plusTbody.childNodes.length);
  524. const customTextTd = customTr.insertCell(0);
  525. const customCssTd = customTr.insertCell(1);
  526. const customCssInput = document.createElement('input');
  527. customCssTd.appendChild(customCssInput);
  528.  
  529. customTextTd.textContent = "Custom CSS URL:";
  530. customCssInput.style.width = "99px";
  531. customCssInput.value = settings.styleURL;
  532.  
  533. styleOptions.forEach(o => cssSelect.appendChild(new Option(o, o)));
  534.  
  535. // Enable the save button
  536.  
  537. saveTr.parentNode.insertBefore(headerTr, saveTr);
  538. saveTr.parentNode.insertBefore(settingsTr, saveTr);
  539. saveTr.parentNode.insertBefore(moreTr, saveTr);
  540. cssSelect.selectedIndex = settings.style;
  541.  
  542. // Add event listeners
  543. plusTbody.querySelectorAll('input, select').forEach(elm => {
  544. elm.addEventListener("change", () => {
  545. saveBtn.disabled = false;
  546. });
  547. });
  548.  
  549. saveBtn.addEventListener('mouseup', () => {
  550. settings.autospoiler = spoilerInput.checked;
  551. settings.style = cssSelect.selectedIndex;
  552. settings.styleURL = customCssInput.value;
  553. settings.auto502 = errorInput.checked;
  554. settings.textFix = textFixInput.checked;
  555. settings.pixelFix = pixelFixInput.checked;
  556. settings.autoUpdate = updateInput.checked;
  557. settings.night = false;
  558. console.log(settings);
  559. saveData(settings);
  560.  
  561. updateTheme(settings.style == styleOptions.length - 1 ? settings.styleURL : styleUrls[settings.style]);
  562.  
  563. dropStyle.textContent = dropStyleText + (settings.pixelFix ? ' '+pixelFixText : '');
  564.  
  565. dropStyle.textContent = dropStyleText + (settings.pixelFix ? ' '+pixelFixText : '') + ' *{transition:1s}';
  566. setTimeout(() => {
  567. dropStyle.textContent = dropStyleText + (settings.pixelFix ? ' '+pixelFixText : '');
  568. }, 1000);
  569. });
  570. }
  571. else if (location.pathname === "/my/messages/") { // New buttons
  572. const btnStyle = "margin: 10px 5px;";
  573.  
  574. // Select all read messages button.
  575. const selRead = document.createElement('input');
  576. selRead.style = btnStyle;
  577. selRead.value = "Select Read";
  578. selRead.id = "selectread";
  579. selRead.classList.add("major");
  580. selRead.type = "button";
  581.  
  582. // On click, select all messages with the style attribute indicating it as read.
  583. selRead.addEventListener('mouseup', () => {
  584. document.querySelectorAll('td[style="border-left: 8px solid rgb(221, 221, 221);"] > input').forEach((m) => m.click());
  585. });
  586.  
  587. // Select duplicate message (multiple update notifications).
  588. const selDupe = document.createElement('input');
  589. selDupe.style = btnStyle;
  590. selDupe.value = "Select Same";
  591. selDupe.id = "selectdupe";
  592. selDupe.classList.add("major");
  593. selDupe.type = "button";
  594.  
  595. selDupe.addEventListener('mouseup', evt => {
  596. const temp = document.querySelectorAll('#messages > tr');
  597. const msgs = [];
  598. for (let i = temp.length - 1; i >= 0; i--) {
  599. msgs.push(temp[i]);
  600. }
  601. const titles = [];
  602. msgs.forEach((msg) => {
  603. let title = msg.querySelector('a.major').textContent;
  604. if (/^New update: /.test(title)) { // Select only adventure updates
  605. if (titles.indexOf(title) === -1) {
  606. if (msg.querySelector('td').style.cssText !== "border-left: 8px solid rgb(221, 221, 221);") {
  607. titles.push(title);
  608. }
  609. } else {
  610. msg.querySelector('input').click();
  611. }
  612. }
  613. });
  614. });
  615.  
  616. // Add buttons to the page.
  617. const del = document.querySelector('#deletemsgs');
  618. del.parentNode.appendChild(newBr());
  619. del.parentNode.appendChild(selRead);
  620. del.parentNode.appendChild(selDupe);
  621. }
  622. else if (location.pathname === "/my/messages/new/" && location.search) { // Auto-fill user when linked from a user page
  623. const recipientInput = document.querySelector('#addrecipient');
  624. recipientInput.value = location.search.replace('?u=', '');
  625. pageLoad(() => {
  626. const recipientButton = document.querySelector('#addrecipientbtn');
  627. if (recipientButton) {
  628. recipientButton.click();
  629. if (recipientInput.value === "") { // If the button press doesn't work
  630. return true;
  631. }
  632. }
  633. });
  634. }
  635. else if (location.pathname === "/my/stories/") {
  636. // Add links to buttons
  637. pageLoad(() => {
  638. const adventures = document.querySelectorAll('#stories tr');
  639. if (adventures.length > 0) {
  640. adventures.forEach(story => {
  641. const buttons = story.querySelectorAll('input.major');
  642. const id = story.querySelector('a').href.replace('https://mspfa.com/', '').replace('&p=1', '');
  643. if (id) {
  644. addLink(buttons[0], `/my/stories/info/${id}`);
  645. addLink(buttons[1], `/my/stories/pages/${id}`);
  646. }
  647. });
  648. return true;
  649. }
  650. });
  651.  
  652. // Add user guides
  653. const guides = ["A Guide To Uploading Your Comic To MSPFA", "MSPFA Etiquette", "Fanventure Guide for Dummies", "CSS Guide", "HTML and CSS Things", ];
  654. const links = ["https://docs.google.com/document/d/17QI6Cv_BMbr8l06RrRzysoRjASJ-ruWioEtVZfzvBzU/edit?usp=sharing", "/?s=27631", "/?s=29299", "/?s=21099", "/?s=23711"];
  655. const authors = ["Farfrom Tile", "Radical Dude 42", "nzar", "MadCreativity", "seymour schlong"];
  656.  
  657. const parentTd = document.querySelector('.container > tbody > tr:last-child > td');
  658. const unofficial = parentTd.querySelector('span');
  659. unofficial.textContent = "Unofficial Guides";
  660. const guideTable = document.createElement('table');
  661. const guideTbody = document.createElement('tbody');
  662. guideTable.style.width = "100%";
  663. guideTable.style.textAlign = "center";
  664.  
  665. guideTable.appendChild(guideTbody);
  666. parentTd.appendChild(guideTable);
  667.  
  668. for (let i = 0; i < guides.length; i++) {
  669. const guideTr = guideTbody.insertRow(i);
  670. const guideTd = guideTr.insertCell(0);
  671. const guideLink = document.createElement('a');
  672. guideLink.href = links[i];
  673. guideLink.textContent = guides[i];
  674. guideLink.className = "major";
  675. guideTd.appendChild(guideLink);
  676. guideTd.appendChild(newBr());
  677. guideTd.appendChild(document.createTextNode('by '+authors[i]));
  678. guideTd.appendChild(newBr());
  679. guideTd.appendChild(newBr());
  680. }
  681. }
  682. else if (location.pathname === "/my/stories/info/" && location.search) {
  683. // Button links
  684. addLink(document.querySelector('#userfavs'), `/readers/${location.search}`);
  685. addLink(document.querySelector('#editpages'), `/my/stories/pages/${location.search}`);
  686. }
  687. else if (location.pathname === "/my/stories/pages/" && location.search) {
  688. const adventureID = /\?s=(\d{5})/.exec(location.search)[1];
  689.  
  690. // Button links
  691. addLink(document.querySelector('#editinfo'), `/my/stories/info/?s=${adventureID}`);
  692.  
  693. // Default spoiler values
  694. const replaceButton = document.querySelector('#replaceall');
  695. const spoilerButton = document.createElement('input');
  696. spoilerButton.classList.add('major');
  697. spoilerButton.value = 'Default Spoiler Values';
  698. spoilerButton.type = 'button';
  699. replaceButton.parentNode.insertBefore(spoilerButton, replaceButton);
  700. replaceButton.parentNode.insertBefore(newBr(), replaceButton);
  701. replaceButton.parentNode.insertBefore(newBr(), replaceButton);
  702.  
  703. const spoilerSpan = document.createElement('span');
  704. const spoilerOpen = document.createElement('input');
  705. const spoilerClose = document.createElement('input');
  706. spoilerSpan.appendChild(document.createTextNode('Open button text:'));
  707. spoilerSpan.appendChild(newBr());
  708. spoilerSpan.appendChild(spoilerOpen);
  709. spoilerSpan.appendChild(newBr());
  710. spoilerSpan.appendChild(newBr());
  711. spoilerSpan.appendChild(document.createTextNode('Close button text:'));
  712. spoilerSpan.appendChild(newBr());
  713. spoilerSpan.appendChild(spoilerClose);
  714.  
  715. if (!settings.spoilerValues[adventureID]) {
  716. settings.spoilerValues[adventureID] = {
  717. open: 'Show',
  718. close: 'Hide'
  719. }
  720. }
  721.  
  722. spoilerOpen.value = settings.spoilerValues[adventureID].open;
  723. spoilerClose.value = settings.spoilerValues[adventureID].close;
  724.  
  725. spoilerButton.addEventListener('click', evt => {
  726. window.MSPFA.dialog('Default Spoiler Values', spoilerSpan, ['Save', 'Cancel'], (output, form) => {
  727. if (output === 'Save') {
  728. settings.spoilerValues[adventureID].open = spoilerOpen.value === '' ? 'Show' : spoilerOpen.value;
  729. settings.spoilerValues[adventureID].close = spoilerClose.value === '' ? 'Hide' : spoilerClose.value;
  730. if (settings.spoilerValues[adventureID].open === 'Show' && settings.spoilerValues[adventureID].close === 'Hide') {
  731. delete settings.spoilerValues[adventureID];
  732. }
  733. saveData(settings);
  734. }
  735. });
  736. });
  737.  
  738. document.querySelector('input[title="Spoiler"]').addEventListener('click', evt => {
  739. document.querySelector('#dialog input[name="open"]').value = settings.spoilerValues[adventureID].open;
  740. document.querySelector('#dialog input[name="close"]').value = settings.spoilerValues[adventureID].close;
  741. });
  742.  
  743. // Buttonless spoilers
  744. const flashButton = document.querySelector('input[title="Flash');
  745. const newSpoilerButton = document.createElement('input');
  746. newSpoilerButton.setAttribute('data-tag', 'Buttonless Spoiler');
  747. newSpoilerButton.title = 'Buttonless Spoiler';
  748. newSpoilerButton.type = 'button';
  749. newSpoilerButton.style = 'background-image: url("https://pipe.miroware.io/5b52ba1d94357d5d623f74aa/HTML%20and%20CSS%20Things/icons.png"); background-position: -66px -88px;';
  750.  
  751. newSpoilerButton.addEventListener('click', evt => {
  752. const bbe = document.querySelector('#bbtoolbar').parentNode.querySelector('textarea');
  753. if (bbe) {
  754. bbe.focus();
  755. const start = bbe.selectionStart;
  756. const end = bbe.selectionEnd;
  757. bbe.value = bbe.value.slice(0, start) + '<div class="spoiler"><div>' + bbe.value.slice(start, end) + '</div></div>' + bbe.value.slice(end);
  758. bbe.selectionStart = start + 26;
  759. bbe.selectionEnd = end + 26;
  760. }
  761. });
  762.  
  763. flashButton.parentNode.insertBefore(newSpoilerButton, flashButton);
  764.  
  765. // Open preview in new tab with middle mouse
  766. document.body.addEventListener('mouseup', evt => {
  767. if (evt.toElement.value === "Preview" && evt.button === 1) {
  768. evt.toElement.click();
  769. return false;
  770. }
  771. });
  772. }
  773. else if (location.pathname === "/user/") {
  774. const id = location.search.slice(3);
  775. const statAdd = [];
  776. // Button links
  777. pageLoad(() => {
  778. const msgButton = document.querySelector('#sendmsg');
  779. if (msgButton) {
  780. addLink(msgButton, `/my/messages/new/${location.search}`); // note: doesn't input the desired user's id
  781. addLink(document.querySelector('#favstories'), `/favs/${location.search}`);
  782. return true;
  783. }
  784. });
  785.  
  786. // Add extra user stats
  787. pageLoad(() => {
  788. if (window.MSPFA) {
  789. const stats = document.querySelector('#userinfo table');
  790.  
  791. const joinTr = stats.insertRow(1);
  792. const joinTextTd = joinTr.insertCell(0);
  793. joinTextTd.appendChild(document.createTextNode("Account created:"));
  794. const joinDate = joinTr.insertCell(1);
  795. const joinTime = document.createElement('b');
  796. joinTime.textContent = "Loading...";
  797. joinDate.appendChild(joinTime);
  798.  
  799. const advCountTr = stats.insertRow(2);
  800. const advTextTd = advCountTr.insertCell(0);
  801. advTextTd.appendChild(document.createTextNode("Adventures created:"));
  802. const advCount = advCountTr.insertCell(1);
  803. const advCountText = document.createElement('b');
  804. advCountText.textContent = "Loading...";
  805. advCount.appendChild(advCountText);
  806.  
  807. if (statAdd.indexOf('date') === -1) {
  808. window.MSPFA.request(0, {
  809. do: "user",
  810. u: id
  811. }, user => {
  812. if (typeof user !== "undefined") {
  813. statAdd.push('date');
  814. joinTime.textContent = new Date(user.d).toString().split(' ').splice(1, 4).join(' ');
  815. }
  816. });
  817. }
  818.  
  819. if (statAdd.indexOf('made') === -1) {
  820. window.MSPFA.request(0, {
  821. do: "editor",
  822. u: id
  823. }, s => {
  824. if (typeof s !== "undefined") {
  825. statAdd.push('made');
  826. advCountText.textContent = s.length;
  827. }
  828. });
  829. }
  830.  
  831. if (document.querySelector('#favstories').style.display !== 'none' && statAdd.indexOf('fav') === -1) {
  832. statAdd.push('fav');
  833. const favCountTr = stats.insertRow(3);
  834. const favTextTd = favCountTr.insertCell(0);
  835. favTextTd.appendChild(document.createTextNode("Adventures favorited:"));
  836. const favCount = favCountTr.insertCell(1);
  837. const favCountText = document.createElement('b');
  838. favCountText.textContent = "Loading...";
  839. window.MSPFA.request(0, {
  840. do: "favs",
  841. u: id
  842. }, s => {
  843. if (typeof s !== "undefined") {
  844. favCountText.textContent = s.length;
  845. }
  846. });
  847. favCount.appendChild(favCountText);
  848. }
  849.  
  850. return true;
  851. }
  852. });
  853. }
  854. else if (location.pathname === "/favs/" && location.search) {
  855. // Button links
  856. pageLoad(() => {
  857. const stories = document.querySelectorAll('#stories tr');
  858. let favCount = 0;
  859.  
  860. if (stories.length > 0) {
  861. stories.forEach(story => {
  862. favCount++;
  863. const id = story.querySelector('a').href.replace('https://mspfa.com/', '');
  864. pageLoad(() => {
  865. if (window.MSPFA.me.i) {
  866. addLink(story.querySelector('.edit.major'), `/my/stories/info/${id}`);
  867. return;
  868. }
  869. });
  870. addLink(story.querySelector('.rss.major'), `/rss/${id}`);
  871. });
  872.  
  873. // Fav count
  874. const username = document.querySelector('#username');
  875. username.parentNode.appendChild(newBr());
  876. username.parentNode.appendChild(newBr());
  877. username.parentNode.appendChild(document.createTextNode(`Favorited adventures: ${favCount}`));
  878.  
  879. return true;
  880. }
  881. });
  882. }
  883. else if (location.pathname === "/search/" && location.search) {
  884. // Character and word statistics
  885. const statTable = document.createElement('table');
  886. const statTbody = document.createElement('tbody');
  887. const statTr = statTbody.insertRow(0);
  888. const charCount = statTr.insertCell(0);
  889. const wordCount = statTr.insertCell(0);
  890. const statParentTr = document.querySelector('#pages').parentNode.parentNode.insertRow(2);
  891. const statParentTd = statParentTr.insertCell(0);
  892.  
  893. const statHeaderTr = statTbody.insertRow(0);
  894. const statHeader = document.createElement('th');
  895. statHeader.colSpan = '2';
  896.  
  897. statHeaderTr.appendChild(statHeader);
  898. statHeader.textContent = 'Statistics may not be entirely accurate.';
  899.  
  900. statTable.style.width = "100%";
  901.  
  902. charCount.textContent = "Character count: loading...";
  903. wordCount.textContent = "Word count: loading...";
  904.  
  905. statTable.appendChild(statTbody);
  906. statParentTd.appendChild(statTable);
  907.  
  908. pageLoad(() => {
  909. if (document.querySelector('#pages br')) {
  910. const bbc = window.MSPFA.BBC.slice();
  911. bbc.splice(0, 3);
  912.  
  913. window.MSPFA.request(0, {
  914. do: "story",
  915. s: location.search.replace('?s=', '')
  916. }, story => {
  917. if (typeof story !== "undefined") {
  918. const pageContent = [];
  919. story.p.forEach(p => {
  920. pageContent.push(p.c);
  921. pageContent.push(p.b);
  922. });
  923.  
  924. const storyText = pageContent.join(' ')
  925. .replace(/\n/g, ' ')
  926. .replace(bbc[0][0], '$1')
  927. .replace(bbc[1][0], '$1')
  928. .replace(bbc[2][0], '$1')
  929. .replace(bbc[3][0], '$1')
  930. .replace(bbc[4][0], '$2')
  931. .replace(bbc[5][0], '$3')
  932. .replace(bbc[6][0], '$3')
  933. .replace(bbc[7][0], '$3')
  934. .replace(bbc[8][0], '$3')
  935. .replace(bbc[9][0], '$3')
  936. .replace(bbc[10][0], '$2')
  937. .replace(bbc[11][0], '$1')
  938. .replace(bbc[12][0], '$3')
  939. .replace(bbc[13][0], '$3')
  940. .replace(bbc[14][0], '')
  941. .replace(bbc[16][0], '$1')
  942. .replace(bbc[17][0], '$2 $4 $5')
  943. .replace(bbc[18][0], '$2 $4 $5')
  944. .replace(bbc[19][0], '')
  945. .replace(bbc[20][0], '')
  946. .replace(/<(.*?)>/g, '');
  947.  
  948. wordCount.textContent = `Word count: ${storyText.split(/ +/g).length}`;
  949. charCount.textContent = `Character count: ${storyText.replace(/ +/g, '').length}`;
  950. }
  951. });
  952. return true;
  953. }
  954. });
  955. }
  956. })();