MSPFA extras

Adds custom quality of life features to MSPFA.

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

  1. // ==UserScript==
  2. // @name MSPFA extras
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.6.6.2
  5. // @description Adds custom quality of life features to MSPFA.
  6. // @author seymour schlong
  7. // @icon https://pipe.miroware.io/5b52ba1d94357d5d623f74aa/mspfa/ico.png
  8. // @icon64 https://pipe.miroware.io/5b52ba1d94357d5d623f74aa/mspfa/ico.png
  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. // @match https://mspfa.com/random/
  15. // @exclude https://mspfa.com/js/*
  16. // @exclude https://mspfa.com/css/*
  17. // @grant none
  18. // ==/UserScript==
  19.  
  20.  
  21. (function() {
  22. 'use strict';
  23.  
  24. const currentVersion = "1.6.6.2";
  25. console.log(`MSPFA extras script v${currentVersion} by seymour schlong`);
  26.  
  27. const debug = false;
  28.  
  29. /**
  30. * https://github.com/GrantGryczan/MSPFA/projects/1?fullscreen=true
  31. * Github to-do completion list (and other stuff too)
  32. *
  33. * https://github.com/GrantGryczan/MSPFA/issues/26 - Dropdown menu - February 23rd, 2020
  34. * https://github.com/GrantGryczan/MSPFA/issues/18 - MSPFA themes - February 23rd, 2020
  35. * https://github.com/GrantGryczan/MSPFA/issues/32 - Adventure creation dates - February 23rd, 2020
  36. * https://github.com/GrantGryczan/MSPFA/issues/32 - User creation dates - February 23rd, 2020
  37. * https://github.com/GrantGryczan/MSPFA/issues/40 - Turn certain buttons into links - July 21st, 2020
  38. * https://github.com/GrantGryczan/MSPFA/issues/41 - Word and character count - July 21st, 2020
  39. * https://github.com/GrantGryczan/MSPFA/issues/57 - Default spoiler values - August 7th, 2020
  40. * https://github.com/GrantGryczan/MSPFA/issues/62 - Buttonless spoilers - August 7th, 2020
  41. * https://github.com/GrantGryczan/MSPFA/issues/52 - Hash URLs - August 8th, 2020
  42. * - Page drafts - August 8th, 2020
  43. * - Edit pages button - August 8th, 2020
  44. * - Image preloading - August 20th, 2020
  45. * https://github.com/GrantGryczan/MSPFA/issues/19 - Manage game saves - August 22nd, 2020
  46. *
  47. * Extension to-do... maybe...
  48. *
  49. * 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.
  50. */
  51.  
  52. // A general function that allows for waiting until a certain element appears on the page.
  53. const pageLoad = (fn, length) => {
  54. const interval = setInterval(() => {
  55. if (fn()) clearInterval(interval);
  56. }, length ? length*1000 : 500);
  57. };
  58.  
  59. // Saves the options data for the script.
  60. const saveData = (data) => {
  61. localStorage.mspfaextra = JSON.stringify(data);
  62. if (debug) {
  63. console.log('Settings:');
  64. console.log(data);
  65. }
  66. };
  67.  
  68. // Saves the data for drafts
  69. const saveDrafts = (data) => {
  70. localStorage.mspfadrafts = JSON.stringify(data);
  71. if (debug) {
  72. console.log('Drafts:');
  73. console.log(data);
  74. }
  75. };
  76.  
  77. // Encases an element within a link
  78. const addLink = (elm, url) => {
  79. const link = document.createElement('a');
  80. link.href = url;
  81. elm.parentNode.insertBefore(link, elm);
  82. link.appendChild(elm);
  83. };
  84.  
  85. // Returns true if version 2 is newer
  86. const compareVer = (ver1, ver2) => {
  87. ver1 = ver1.split(/\./); // current version
  88. ver2 = ver2.split(/\./); // new version
  89. ver1.push(0);
  90. ver1.push(0);
  91. ver2.push(0);
  92. ver2.push(0);
  93. if (parseInt(ver2[0]) > parseInt(ver1[0])) { // 1.x.x.x
  94. return true;
  95. } else if (parseInt(ver2[1]) > parseInt(ver1[1])) { // x.1.x.x
  96. return true;
  97. } else if (parseInt(ver2[2]) > parseInt(ver1[2])) { // x.x.1.x
  98. return true;
  99. } else if (parseInt(ver2[3]) > parseInt(ver1[3]) && parseInt(ver2[2]) === parseInt(ver1[2])) { // x.x.x.1
  100. return true;
  101. }
  102. return false;
  103. }
  104.  
  105. // Easy br element
  106. const newBr = () => {
  107. return document.createElement('br');
  108. }
  109.  
  110. let settings = {};
  111. let drafts = {};
  112.  
  113. const defaultSettings = {
  114. autospoiler: false,
  115. style: 0,
  116. styleURL: "",
  117. night: false,
  118. auto502: true,
  119. textFix: false,
  120. pixelFix: false,
  121. intro: false,
  122. autoUpdate: true,
  123. commandScroll: false,
  124. preload: true,
  125. version: currentVersion,
  126. spoilerValues: {}
  127. }
  128.  
  129. let pageLoaded = false;
  130.  
  131. if (localStorage.mspfadrafts) {
  132. Object.assign(drafts, JSON.parse(localStorage.mspfadrafts));
  133. }
  134.  
  135. // Load any previous settings from localStorage
  136. if (localStorage.mspfaextra) {
  137. Object.assign(settings, JSON.parse(localStorage.mspfaextra));
  138.  
  139. // Get draft data from settings
  140. if (typeof settings.drafts === "object") {
  141. if (Object.keys(settings.drafts).length > 0 && Object.keys(drafts).length === 0) {
  142. drafts = settings.drafts;
  143. }
  144. }
  145. saveDrafts(drafts);
  146. }
  147.  
  148. // If any settings are undefined, re-set to their default state. (For older users when new things get stored)
  149. const checkSettings = () => {
  150. const defaultSettingsKeys = Object.keys(defaultSettings);
  151. for (let i = 0; i < defaultSettingsKeys.length; i++) {
  152. if (typeof settings[defaultSettingsKeys[i]] === "undefined") {
  153. settings[defaultSettingsKeys[i]] = defaultSettings[defaultSettingsKeys[i]];
  154. }
  155. }
  156. saveData(settings);
  157. }
  158.  
  159. checkSettings();
  160.  
  161. // Update saved version to the version used in the script to prevent unnecessary notifications
  162. if (compareVer(settings.version, currentVersion)) {
  163. settings.version = currentVersion;
  164. saveData(settings);
  165. }
  166.  
  167. // Scrolls you to where you need to be
  168. const hashSearch = location.href.replace(location.origin + location.pathname, '').replace(location.search, '');
  169. if (hashSearch !== '') {
  170. pageLoad(() => {
  171. const idElement = document.querySelector(hashSearch);
  172. if (idElement) {
  173. const selected = document.querySelector(hashSearch);
  174. selected.scrollIntoView();
  175. selected.style.boxShadow = '1px 1px 5px red, -1px -1px 5px red, -1px 1px 5px red, -1px 1px 5px red';
  176. selected.style.transition = '0.5s';
  177. pageLoad(() => {
  178. if (pageLoaded) {
  179. selected.style.boxShadow = '';
  180. }
  181. });
  182.  
  183. return true;
  184. }
  185. }, 1);
  186. }
  187.  
  188. // Ripped shamelessly right from mspfa lol (URL search parameters -- story ID, page num, etc.)
  189. let rawParams;
  190. if (location.href.indexOf("#") != -1) {
  191. rawParams = location.href.slice(0, location.href.indexOf("#"));
  192. } else {
  193. rawParams = location.href;
  194. }
  195. if (rawParams.indexOf("?") != -1) {
  196. rawParams = rawParams.slice(rawParams.indexOf("?") + 1).split("&");
  197. } else {
  198. rawParams = [];
  199. }
  200. const params = {};
  201. for (let i = 0; i < rawParams.length; i++) {
  202. try {
  203. const p = rawParams[i].split("=");
  204. params[p[0]] = decodeURIComponent(p[1]);
  205. } catch (err) {}
  206. }
  207.  
  208. if (debug) {
  209. console.log('URL parameters:');
  210. console.log(params);
  211. }
  212.  
  213. // Functions to get/change data from the console
  214. window.MSPFAe = {
  215. getSettings: () => {
  216. return settings;
  217. },
  218. getSettingsString: (formatted) => {
  219. if (formatted) {
  220. console.log(JSON.stringify(settings, null, 4));
  221. } else {
  222. console.log(JSON.stringify(settings));
  223. }
  224. },
  225. getDrafts: () => {
  226. return drafts;
  227. },
  228. getDraftsString: (formatted) => {
  229. if (formatted) {
  230. console.log(JSON.stringify(drafts, null, 4));
  231. } else {
  232. console.log(JSON.stringify(drafts));
  233. }
  234. },
  235. changeSettings: (newSettings) => {
  236. console.log('Settings updated');
  237. console.log(settings);
  238. Object.assign(settings, newSettings);
  239. saveData(settings);
  240. },
  241. changeSettingsString: (fullString) => {
  242. try {
  243. JSON.parse(fullString);
  244. } catch (err) {
  245. console.error(err);
  246. return;
  247. }
  248. settings = JSON.parse(fullString);
  249. checkSettings();
  250. console.log(settings);
  251. },
  252. getParams: params
  253. }
  254.  
  255. // Delete any unchanged spoiler values or empty drafts
  256. if (location.pathname !== "/my/stories/pages/") {
  257. // Go through spoiler values and remove any that aren't unique
  258. Object.keys(settings.spoilerValues).forEach(adventure => {
  259. if (settings.spoilerValues[adventure].open === "Show" && settings.spoilerValues[adventure].close === "Hide") {
  260. delete settings.spoilerValues[adventure];
  261. } else if (settings.spoilerValues[adventure].open === '' && settings.spoilerValues[adventure].close === '') {
  262. delete settings.spoilerValues[adventure];
  263. }
  264. });
  265. // Go through and remove adventures with empty drafts
  266. Object.keys(drafts).forEach(adventure => {
  267. if (Object.keys(drafts[adventure]).length === 0) {
  268. delete drafts[adventure];
  269. }
  270. });
  271. }
  272.  
  273. const styleOptions = ["Standard", "Low Contrast", "Light", "Dark", "Felt", "Trickster", "Custom"];
  274. const styleUrls = ['', '/css/theme1.css', '/css/theme2.css', '/css/?s=36237', '/css/theme4.css', '/css/theme5.css'];
  275.  
  276. // Dropdown menu
  277. const myLink = document.querySelector('nav a[href="/my/"]');
  278. if (myLink) {
  279. const dropDiv = document.createElement('div');
  280. dropDiv.className = 'dropdown';
  281. Object.assign(dropDiv.style, {
  282. position: 'relative',
  283. display: 'inline-block',
  284. backgroundColor: 'inherit'
  285. });
  286.  
  287. const dropContent = document.createElement('div');
  288. dropContent.className = 'dropdown-content';
  289. Object.assign(dropContent.style, {
  290. display: 'none',
  291. backgroundColor: 'inherit',
  292. position: 'absolute',
  293. textAlign: 'left',
  294. minWidth: '100px',
  295. marginLeft: '-5px',
  296. padding: '2px',
  297. zIndex: '1',
  298. borderRadius: '0 0 5px 5px'
  299. });
  300.  
  301. dropDiv.addEventListener('mouseenter', evt => {
  302. dropContent.style.display = 'block';
  303. dropContent.style.color = getComputedStyle(myLink).color;
  304. });
  305. dropDiv.addEventListener('mouseleave', evt => {
  306. dropContent.style.display = 'none';
  307. });
  308.  
  309. myLink.parentNode.insertBefore(dropDiv, myLink);
  310. dropDiv.appendChild(myLink);
  311. dropDiv.appendChild(dropContent);
  312.  
  313. const dLinks = [];
  314. dLinks[0] = [ 'Messages', 'My Adventures', 'Settings' ];
  315. dLinks[1] = [ '/my/messages/', '/my/stories/', '/my/settings/' ];
  316.  
  317. for (let i = 0; i < dLinks[0].length; i++) {
  318. const newLink = document.createElement('a');
  319. newLink.textContent = dLinks[0][i];
  320. newLink.href = dLinks[1][i];
  321. newLink.style = 'visibility: visible; word-spacing: normal; letter-spacing: normal; font-size: 10px;';
  322. dropContent.appendChild(newLink);
  323. }
  324.  
  325. // Append "My Profile" to the dropdown list if you're signed in
  326. pageLoad(() => {
  327. if (window.MSPFA) {
  328. if (window.MSPFA.me.n) {
  329. const newLink = document.createElement('a');
  330. newLink.textContent = "My Profile";
  331. newLink.href = `/user/?u=${window.MSPFA.me.i}`;
  332. newLink.style = 'visibility: visible; word-spacing: normal; letter-spacing: normal; font-size: 10px;';
  333. dropContent.appendChild(newLink);
  334. return true;
  335. }
  336. }
  337. });
  338. }
  339.  
  340. // Error reloading
  341. window.addEventListener("load", () => {
  342. pageLoaded = true;
  343.  
  344. // Reload the page if 502 CloudFlare error page appears
  345. if (settings.auto502 && document.querySelector('.cf-error-overview')) {
  346. window.location.reload();
  347. }
  348.  
  349. // Wait five seconds, then refresh the page
  350. if (document.body.textContent === "Your client is sending data to MSPFA too quickly. Wait a moment before continuing.") {
  351. setTimeout(() => {
  352. window.location.reload();
  353. }, 5000);
  354. }
  355. });
  356.  
  357. // Message that shows when you first get the script
  358. const showIntroDialog = () => {
  359. 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]');
  360. window.MSPFA.dialog("MSPFA extras message", msg, ["Okay"]);
  361. }
  362.  
  363. // Check for updates by comparing currentVersion to text data from a text file update text and info
  364. const checkForUpdates = (evt) => {
  365. const rand = Math.floor(Math.random() * 9999999999999); // Sometimes it doesn't fetch the right version of the text file.
  366. fetch(`https://pipe.miroware.io/5b52ba1d94357d5d623f74aa/mspfa/update.txt?rand=${rand}`, { cache:'no-store' }).then((req) => {
  367. if (req.ok) {
  368. return req.text().then(r => {
  369. const version = /^{(.*?)}/.exec(r)[1];
  370. const content = r.replace(/^{(.*?)}\r\n/, '');
  371.  
  372. if (compareVer(settings.version, version) || (evt && evt.type === 'click')) {
  373. const msg = window.MSPFA.parseBBCode(content);
  374. settings.version = version;
  375. saveData(settings);
  376. window.MSPFA.dialog(`MSPFA extras update! (${version})`, msg, ["Opt-out", "Dismiss", "Update"], (output, form) => {
  377. if (output === "Update") {
  378. window.open('https://greasyfork.org/en/scripts/396798-mspfa-extras', '_blank').focus();
  379. } else if (output === "Opt-out") {
  380. settings.autoUpdate = false;
  381. saveData(settings);
  382. }
  383. });
  384. } else {
  385. console.log('No new update found.');
  386. }
  387. });
  388. }
  389. });
  390. };
  391.  
  392. // Check for updates and show intro dialog if needed
  393. pageLoad(() => {
  394. if (window.MSPFA) {
  395. if (settings.autoUpdate) {
  396. console.log('Checking for updates...');
  397. checkForUpdates();
  398. }
  399.  
  400. if (!settings.intro) {
  401. showIntroDialog();
  402. settings.intro = true;
  403. saveData(settings);
  404. }
  405. return true;
  406. }
  407. });
  408.  
  409. const details = document.querySelector('#details');
  410.  
  411. // Add 'link' at the bottom to show the intro dialog again
  412. const introLink = document.createElement('a');
  413. introLink.textContent = 'View Script Message';
  414. introLink.href = 'javascript:void(0);';
  415. introLink.addEventListener('click', showIntroDialog);
  416. details.appendChild(introLink);
  417.  
  418. // vbar!!!!
  419. const vbar = document.createElement('span');
  420. Object.assign(vbar, {className: 'vbar', style: 'padding: 0 5px', textContent: '|'});
  421. details.appendChild(vbar);
  422.  
  423. // Add 'link' at the bottom to show the update dialog again
  424. const updateLink = document.createElement('a');
  425. updateLink.textContent = 'View Update';
  426. updateLink.href = 'javascript:void(0);';
  427. updateLink.addEventListener('click', checkForUpdates);
  428. details.appendChild(updateLink);
  429.  
  430. // vbar 2!!!!
  431. const vbar2 = document.createElement('span');
  432. Object.assign(vbar2, {className: 'vbar', style: 'padding: 0 5px', textContent: '|'});
  433. details.appendChild(vbar2);
  434.  
  435. // if you really enjoy the script and has some extra moneys 🥺
  436. const donateLink = document.createElement('a');
  437. donateLink.textContent = 'Donate';
  438. donateLink.href = 'https://ko-fi.com/ironbean';
  439. donateLink.target = "blank";
  440. details.appendChild(donateLink);
  441.  
  442. // Theme stuff
  443. const theme = document.createElement('link');
  444. Object.assign(theme, { id: 'theme', type: 'text/css', rel: 'stylesheet' });
  445. const updateTheme = (src) => {
  446. theme.href = src;
  447. }
  448. if (!document.querySelector('#theme')) {
  449. document.querySelector('head').appendChild(theme);
  450. if (settings.night) {
  451. updateTheme('/css/?s=36237');
  452. } else {
  453. updateTheme(settings.style == styleOptions.length - 1 ? settings.styleURL : styleUrls[settings.style]);
  454. }
  455. }
  456.  
  457. // Dropdown menu and pixelated scaling
  458. const dropStyle = document.createElement('style');
  459. const pixelFixText = 'img, .mspfalogo, .major, .arrow, #flashytitle, .heart, .fav, .notify, .edit, .rss, input, #loading { image-rendering: pixelated !important; }';
  460. const dropStyleText = `#notification { z-index: 2; } .dropdown-content a { color: inherit; padding: 2px; text-decoration: underline; display: block;}`;
  461. if (!document.querySelector('#dropdown-style')) {
  462. dropStyle.id = 'dropdown-style';
  463. dropStyle.textContent = dropStyleText + (settings.pixelFix ? ' '+pixelFixText : '');
  464. document.querySelector('head').appendChild(dropStyle);
  465. }
  466.  
  467. // Enabling night mode.
  468. document.querySelector('footer .mspfalogo').addEventListener('click', evt => {
  469. settings.night = !settings.night;
  470. saveData(settings);
  471.  
  472. // Transition to make it feel nicer on the eyes
  473. dropStyle.textContent = dropStyleText + (settings.pixelFix ? ' '+pixelFixText : '') + '';
  474. dropStyle.textContent = dropStyleText + (settings.pixelFix ? ' '+pixelFixText : '') + ' *{transition:1s}';
  475.  
  476. if (settings.night) {
  477. updateTheme('/css/?s=36237');
  478. } else {
  479. updateTheme(settings.style == styleOptions.length - 1 ? settings.styleURL : styleUrls[settings.style]);
  480. }
  481.  
  482. setTimeout(() => {
  483. dropStyle.textContent = dropStyleText + (settings.pixelFix ? ' '+pixelFixText : '');
  484. }, 1000);
  485.  
  486. console.log(`Night mode turned ${settings.night ? 'on' : 'off'}.`);
  487. });
  488.  
  489. if (location.pathname === "/" || location.pathname === "/preview/") {
  490. if (location.search) {
  491. // Remove the current theme if the adventure has CSS (to prevent conflicts);
  492. if (settings.style > 0) {
  493. pageLoad(() => {
  494. if (window.MSPFA) {
  495. if (window.MSPFA.story && window.MSPFA.story.y && (window.MSPFA.story.y.toLowerCase().includes('import') || window.MSPFA.story.y.includes('{'))) {
  496. if (!settings.night) updateTheme('');
  497. return true;
  498. }
  499. }
  500. if (pageLoaded) return true;
  501. });
  502. }
  503.  
  504. // Preload the next page
  505. if (settings.preload) {
  506. const preloadImages = document.createElement('div');
  507. preloadImages.id = 'preload';
  508. preloadImages.style.display = 'none';
  509. document.querySelector('#container').appendChild(preloadImages);
  510.  
  511. window.MSPFA.slide.push(p => {
  512. preloadImages.innerHTML = '';
  513. if (window.MSPFA.story.p[p-2]) {
  514. window.MSPFA.parseBBCode(window.MSPFA.story.p[p-2].b).querySelectorAll('img').forEach(image => {
  515. preloadImages.appendChild(image);
  516. });
  517. }
  518. if (window.MSPFA.story.p[p]) {
  519. window.MSPFA.parseBBCode(window.MSPFA.story.p[p].b).querySelectorAll('img').forEach(image => {
  520. preloadImages.appendChild(image);
  521. });
  522. }
  523. });
  524. }
  525.  
  526. // Scroll up to the nav bar when changing page so you don't have to scroll down as much =)
  527. if (settings.commandScroll) {
  528. const heightTop = document.querySelector('nav').getBoundingClientRect().top - document.body.getBoundingClientRect().top;
  529. let temp = -2; // To prevent moving the page down when loading it for the first time
  530. if (settings.textFix) temp--; // Text Fix adds a page load, so it needs an extra value to not activate
  531. window.MSPFA.slide.push((p) => {
  532. if (temp < 0) {
  533. temp++;
  534. } else {
  535. window.scroll(0, heightTop);
  536. }
  537. });
  538. }
  539.  
  540. // Automatic spoiler opening
  541. if (settings.autospoiler) {
  542. window.MSPFA.slide.push((p) => {
  543. document.querySelectorAll('#slide .spoiler:not(.open) > div:first-child > input').forEach(sb => sb.click());
  544. });
  545. }
  546.  
  547. // Show creation date
  548. pageLoad(() => {
  549. if (document.querySelector('#infobox tr td:nth-child(2)')) {
  550. 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(' ')));
  551. return true;
  552. }
  553. });
  554.  
  555. // Hash scrolling and opening infobox or commmentbox
  556. if (['#infobox', '#commentbox', '#newcomment', '#latestpages'].indexOf(hashSearch) !== -1) {
  557. pageLoad(() => {
  558. if (document.querySelector(hashSearch)) {
  559. if (hashSearch === '#infobox') {
  560. document.querySelector('input[data-open="Show Adventure Info"]').click();
  561. } else if (hashSearch === '#commentbox' || hashSearch === '#newcomment') {
  562. document.querySelector('input[data-open="Show Comments"]').click();
  563. } else if (hashSearch === '#latestpages') {
  564. document.querySelector('input[data-open="Show Adventure Info"]').click();
  565. document.querySelector('input[data-open="Show Latest Pages"]').click();
  566. }
  567. return true;
  568. }
  569. });
  570. }
  571.  
  572. // Attempt to fix text errors
  573. if (settings.textFix) {
  574. pageLoad(() => {
  575. if (window.MSPFA.story && window.MSPFA.story.p) {
  576. // russian/bulgarian is not possible =(
  577. const currentPage = parseInt(/^\?s(?:.*?)&p=([\d]*)$/.exec(location.search)[1]);
  578. const library = [
  579. ["&acirc;��", "'"],
  580. ["&Atilde;�", "Ñ"],
  581. ["&Atilde;&plusmn;", "ñ"],
  582. ["&Atilde;&sup3;", "ó"],
  583. ["&Atilde;&iexcl;", "á"],
  584. ["&Auml;�", "ą"],
  585. ["&Atilde;&shy;", "í"],
  586. ["&Atilde;&ordm;", "ú"],
  587. ["&Atilde;&copy;", "é"],
  588. ["&Aring;�", "ł"],
  589. ["&Aring;&frac14;", "ż"],
  590. ["&Acirc;&iexcl;", "¡"],
  591. ["&Acirc;&iquest;", "¿"],
  592. ["N&Acirc;&ordm;", "#"]
  593. ];
  594. // https://mspfa.com/?s=5280&p=51 -- unknown error
  595.  
  596. const replaceTerms = (p) => {
  597. library.forEach(term => {
  598. if (window.MSPFA.story.p[p]) {
  599. window.MSPFA.story.p[p].c = window.MSPFA.story.p[p].c.replace(new RegExp(term[0], 'g'), term[1]);
  600. window.MSPFA.story.p[p].b = window.MSPFA.story.p[p].b.replace(new RegExp(term[0], 'g'), term[1]);
  601. }
  602. });
  603. };
  604.  
  605. replaceTerms(currentPage-1);
  606.  
  607. window.MSPFA.slide.push(p => {
  608. replaceTerms(p);
  609. replaceTerms(p-2);
  610. });
  611. return true;
  612. }
  613. });
  614. }
  615.  
  616. // Turn buttons into links
  617. const pageButton = document.createElement('button');
  618. const pageLink = document.createElement('a');
  619. pageLink.href = `/my/stories/pages/?s=${params.s}#p${params.p}`;
  620. pageButton.className = 'pages edit major';
  621. pageButton.type = 'button';
  622. pageButton.title = 'Edit Pages';
  623. pageLink.style.marginRight = '9.5px';
  624. pageButton.style.backgroundImage = 'url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAL1JREFUOI1jYKAQMCJzugoL/+NSePPhQ4a569YxoouzoAuU9vUx3AoLY1BbtYqBgYGB4d21a8jS/9ENwTBg/ezZDNpQGh2oy8szJAcFoRiCYUBgairDrd27Gdz9/Bh+vH2LInfn+nUMQ3G6YOemTRiKsQEMA9z9/Bie7N7NYG9tjaGYKBfs3LSJQZuBgeHg0aPkucDe2prhDSUuOHj06Ih3wdw5cxgYZGQYGObMId0FNx8+JKhBQlGRKIOJBgBpTFVYzRG//QAAAABJRU5ErkJggg==")';
  625. pageLink.appendChild(pageButton);
  626.  
  627. // Edit pages button & button link
  628. pageLoad(() => {
  629. const infoButton = document.querySelector('.edit.major');
  630. if (infoButton) {
  631. pageLoad(() => {
  632. if (window.MSPFA.me.i) {
  633. infoButton.title = "Edit Info";
  634. infoButton.parentNode.insertBefore(pageLink, infoButton);
  635. addLink(infoButton, `/my/stories/info/?s=${params.s}`);
  636. pageButton.style.display = document.querySelector('.edit.major:not(.pages)').style.display;
  637.  
  638. // Change change page link when switching pages
  639. window.MSPFA.slide.push(p => {
  640. const newSearch = location.search.split('&p=');
  641. pageLink.href = `/my/stories/pages/?s=${params.s}#p${newSearch[1].split('#')[0]}`;
  642. });
  643. return true;
  644. }
  645. if (pageLoaded) return true;
  646. });
  647. addLink(document.querySelector('.rss.major'), `/rss/?s=${params.s}`);
  648. return true;
  649. }
  650. });
  651.  
  652. // Add "Reply" button to comment gear
  653. document.body.addEventListener('click', evt => {
  654. if (evt.toElement.classList.contains('gear')) {
  655. const userID = evt.path[2].classList[2].replace('u', '');
  656. const reportButton = document.querySelector('#dialog button[data-value="Report"]');
  657. const replyButton = document.createElement('button');
  658. replyButton.classList.add('major');
  659. replyButton.type = 'submit';
  660. replyButton.setAttribute('data-value', 'Reply');
  661. replyButton.textContent = 'Reply';
  662. replyButton.style = 'margin-right: 9.5px';
  663. reportButton.parentNode.insertBefore(replyButton, reportButton);
  664.  
  665. replyButton.addEventListener('click', evt => {
  666. document.querySelector('#dialog button[data-value="Cancel"]').click();
  667. const commentBox = document.querySelector('#commentbox textarea');
  668. commentBox.value = `[user]${userID}[/user], ${commentBox.value}`;
  669. commentBox.focus();
  670. });
  671. } else return;
  672. });/**/
  673. }
  674. }
  675. else if (location.pathname === "/my/") {
  676. const parent = document.querySelector('#editstories').parentNode;
  677. const viewSaves = document.createElement('a');
  678. Object.assign(viewSaves, { id: 'viewsaves', className: 'major', textContent: 'View Adventure Saves' });
  679.  
  680. parent.appendChild(viewSaves);
  681. parent.appendChild(newBr());
  682. parent.appendChild(newBr());
  683.  
  684. pageLoad(() => {
  685. if (window.MSPFA && window.MSPFA.me && window.MSPFA.me.i) {
  686. viewSaves.href = `/?s=36596&p=6`;
  687. return true;
  688. }
  689. });
  690. }
  691. else if (location.pathname === "/my/settings/") { // Custom settings
  692. const saveBtn = document.querySelector('#savesettings');
  693.  
  694. const table = document.querySelector("#editsettings tbody");
  695. let saveTr = table.querySelectorAll("tr");
  696. saveTr = saveTr[saveTr.length - 1];
  697.  
  698. const headerTr = document.createElement('tr');
  699. const header = document.createElement('th');
  700. Object.assign(header, { id: 'extraSettings', textContent: 'Extra Settings' });
  701. headerTr.appendChild(header);
  702.  
  703. const moreTr = document.createElement('tr');
  704. const more = document.createElement('td');
  705. 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.";
  706. moreTr.appendChild(more);
  707.  
  708. const settingsTr = document.createElement('tr');
  709. const localMsg = document.createElement('span');
  710. const settingsTd = document.createElement('td');
  711. 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!";
  712. const plusTable = document.createElement('table');
  713. const plusTbody = document.createElement('tbody');
  714. plusTable.appendChild(plusTbody);
  715. settingsTd.appendChild(localMsg);
  716. settingsTd.appendChild(newBr());
  717. settingsTd.appendChild(newBr());
  718. settingsTd.appendChild(plusTable);
  719. settingsTr.appendChild(settingsTd);
  720.  
  721. plusTable.style = "text-align: center;";
  722.  
  723. // Create checkbox (soooo much better)
  724. const createCheckbox = (text, checked) => {
  725. const optionTr = plusTbody.insertRow(plusTbody.childNodes.length);
  726. const optionTextTd = optionTr.insertCell(0);
  727. const optionInputTd = optionTr.insertCell(1);
  728. const optionInput = document.createElement('input');
  729. optionInputTd.appendChild(optionInput);
  730.  
  731. optionTextTd.textContent = text;
  732. optionInput.type = "checkbox";
  733. optionInput.checked = checked;
  734.  
  735. return optionInput;
  736. }
  737.  
  738. const spoilerInput = createCheckbox("Automatically open spoilers:", settings.autospoiler);
  739. const preloadInput = createCheckbox("Preload images for the pages immediately before and after:", settings.preload);
  740. const errorInput = createCheckbox("Automatically reload Cloudflare 502 error pages:", settings.auto502);
  741. const commandScrollInput = createCheckbox("Scroll back up to the nav bar when switching page:", settings.commandScroll);
  742. const updateInput = createCheckbox("Automatically check for updates:", settings.autoUpdate);
  743. const pixelFixInput = createCheckbox("Change pixel scaling to nearest neighbour:", settings.pixelFix);
  744. const textFixInput = createCheckbox("Attempt to fix text errors (experimental)*:", settings.textFix);
  745.  
  746. const cssTr = plusTbody.insertRow(plusTbody.childNodes.length);
  747. const cssTextTd = cssTr.insertCell(0);
  748. const cssSelectTd = cssTr.insertCell(1);
  749. const cssSelect = document.createElement('select');
  750. cssSelectTd.appendChild(cssSelect);
  751.  
  752. cssTextTd.textContent = "Change style:";
  753.  
  754. const customTr = plusTbody.insertRow(plusTbody.childNodes.length);
  755. const customTextTd = customTr.insertCell(0);
  756. const customCssTd = customTr.insertCell(1);
  757. const customCssInput = document.createElement('input');
  758. customCssTd.appendChild(customCssInput);
  759.  
  760. customTextTd.textContent = "Custom CSS URL:";
  761. customCssInput.style.width = "99px";
  762. customCssInput.value = settings.styleURL;
  763.  
  764. styleOptions.forEach(o => cssSelect.appendChild(new Option(o, o)));
  765.  
  766. saveTr.parentNode.insertBefore(headerTr, saveTr);
  767. saveTr.parentNode.insertBefore(settingsTr, saveTr);
  768. saveTr.parentNode.insertBefore(moreTr, saveTr);
  769. cssSelect.selectedIndex = settings.style;
  770.  
  771. const buttonSpan = document.createElement('span');
  772. const draftButton = document.createElement('input');
  773. const spoilerButton = document.createElement('input');
  774. draftButton.style = 'margin: 0 9.5px;';
  775. draftButton.value = 'Manage Drafts';
  776. draftButton.className = 'major';
  777. draftButton.type = 'button';
  778. spoilerButton.value = 'Manage Spoiler Values';
  779. spoilerButton.className = 'major';
  780. spoilerButton.type = 'button';
  781. buttonSpan.appendChild(draftButton);
  782. buttonSpan.appendChild(spoilerButton);
  783. settingsTd.appendChild(buttonSpan);
  784.  
  785. const draftMsg = window.MSPFA.parseBBCode('Here you can manage the drafts that you have saved for your adventure(s).\n');
  786. const listTable = document.createElement('table');
  787. listTable.style = 'max-height: 250px; overflow-y: scroll; border: 1px solid grey; padding: 2px; width: 100%; text-align: center; vertical-align: middle;';
  788. const listTbody = document.createElement('tbody');
  789. listTable.appendChild(listTbody);
  790.  
  791. if (Object.keys(drafts).length === 0) {
  792. draftButton.disabled = true;
  793. }
  794.  
  795. draftButton.addEventListener('click', () => {
  796. draftMsg.appendChild(listTable);
  797. listTbody.innerHTML = '';
  798. Object.keys(drafts).forEach(adv => {
  799. window.MSPFA.request(0, {
  800. do: "story",
  801. s: adv
  802. }, story => {
  803. if (typeof story !== "undefined") {
  804. const storyTr = listTbody.insertRow(listTable.rows);
  805. const titleLink = document.createElement('a');
  806. Object.assign(titleLink, { className: 'major', href: `/my/stories/pages/?s=${adv}&click=d`, textContent: story.n, target: '_blank' });
  807. storyTr.insertCell(0).appendChild(titleLink);
  808. const deleteButton = document.createElement('input');
  809. Object.assign(deleteButton, { className: 'major', type: 'button', value: 'Delete' });
  810. storyTr.insertCell(1).appendChild(deleteButton);
  811.  
  812. deleteButton.addEventListener('click', () => {
  813. setTimeout(() => {
  814. window.MSPFA.dialog('Delete adventure draft?', document.createTextNode('Are you really sure?\nThis action cannot be undone!'), ["Yes", "No"], (output, form) => {
  815. if (output === "Yes") {
  816. delete drafts[adv];
  817.  
  818. if (settings.drafts[adv]) {
  819. delete settings.drafts[adv];
  820. saveData(settings);
  821. }
  822.  
  823. saveDrafts(drafts);
  824.  
  825. setTimeout(() => {
  826. draftButton.click();
  827. }, 1);
  828.  
  829. if (Object.keys(drafts).length === 0) {
  830. draftButton.disabled = true;
  831. }
  832. }
  833. });
  834. }, 1);
  835. });
  836. }
  837. });
  838. });
  839.  
  840. window.MSPFA.dialog('Manage Drafts', draftMsg, ["Delete All", "Close"], (output, form) => {
  841. if (output === "Delete All") {
  842. setTimeout(() => {
  843. window.MSPFA.dialog('Delete all Drafts?', document.createTextNode('Are you really sure?\nThis action cannot be undone!'), ["Yes", "No"], (output, form) => {
  844. if (output === "Yes") {
  845. drafts = {};
  846. saveDrafts(drafts);
  847.  
  848. if (typeof settings.drafts !== "undefined") {
  849. delete settings.drafts;
  850. saveData(settings);
  851. }
  852.  
  853. draftButton.disabled = true;
  854. }
  855. });
  856. }, 1);
  857. }
  858. });
  859. });
  860.  
  861. if (Object.keys(settings.spoilerValues).length === 0) {
  862. spoilerButton.disabled = true;
  863. }
  864.  
  865. const spoilerMsg = window.MSPFA.parseBBCode('Here you can manage the spoiler values that you have set for your adventure(s).\nClick on an adventure\'s title to see the values.\n');
  866.  
  867. spoilerButton.addEventListener('click', () => {
  868. spoilerMsg.appendChild(listTable);
  869. listTbody.innerHTML = '';
  870. Object.keys(settings.spoilerValues).forEach(adv => {
  871. window.MSPFA.request(0, {
  872. do: "story",
  873. s: adv
  874. }, story => {
  875. if (typeof story !== "undefined") {
  876. const storyTr = listTbody.insertRow(listTable.rows);
  877. const titleLink = document.createElement('a');
  878. Object.assign(titleLink, { className: 'major', href: `/my/stories/pages/?s=${adv}&click=s`, textContent: story.n, target: '_blank' });
  879. storyTr.insertCell(0).appendChild(titleLink);
  880. const deleteButton = document.createElement('input');
  881. Object.assign(deleteButton, { className: 'major', type: 'button', value: 'Delete' });
  882. storyTr.insertCell(1).appendChild(deleteButton);
  883.  
  884. deleteButton.addEventListener('click', () => {
  885. setTimeout(() => {
  886. window.MSPFA.dialog('Delete adventure spoilers?', document.createTextNode('Are you really sure?\nThis action cannot be undone!'), ["Yes", "No"], (output, form) => {
  887. if (output === "Yes") {
  888. delete settings.spoilerValues[adv];
  889. saveData(settings);
  890.  
  891. setTimeout(() => {
  892. spoilerButton.click();
  893. }, 1);
  894.  
  895. if (Object.keys(settings.spoilerValues).length === 0) {
  896. spoilerButton.disabled = true;
  897. }
  898. }
  899. });
  900. }, 1);
  901. });
  902. }
  903. });
  904. });
  905. window.MSPFA.dialog('Manage Spoiler Values', spoilerMsg, ["Delete All", "Close"], (output, form) => {
  906. if (output === "Delete All") {
  907. setTimeout(() => {
  908. window.MSPFA.dialog('Delete all Spoiler Values?', 'Are you sure you want to delete all spoiler values?\nThis action cannot be undone!', ["Yes", "No"], (output, form) => {
  909. if (output === "Yes") {
  910. settings.spoilerValues = {};
  911. saveData(settings);
  912. draftButton.disabled = true;
  913. }
  914. });
  915. }, 1);
  916. }
  917. });
  918. });
  919.  
  920. // Add event listeners
  921. plusTbody.querySelectorAll('input, select').forEach(elm => {
  922. elm.addEventListener("change", () => {
  923. saveBtn.disabled = false;
  924. });
  925. });
  926.  
  927. saveBtn.addEventListener('mouseup', () => {
  928. settings.autospoiler = spoilerInput.checked;
  929. settings.style = cssSelect.selectedIndex;
  930. settings.styleURL = customCssInput.value;
  931. settings.auto502 = errorInput.checked;
  932. settings.textFix = textFixInput.checked;
  933. settings.pixelFix = pixelFixInput.checked;
  934. settings.autoUpdate = updateInput.checked;
  935. settings.commandScroll = commandScrollInput.checked;
  936. settings.preload = preloadInput.checked;
  937. settings.night = false;
  938. console.log(settings);
  939. saveData(settings);
  940.  
  941. updateTheme(settings.style == styleOptions.length - 1 ? settings.styleURL : styleUrls[settings.style]);
  942.  
  943. dropStyle.textContent = dropStyleText + (settings.pixelFix ? ' '+pixelFixText : '');
  944.  
  945. dropStyle.textContent = dropStyleText + (settings.pixelFix ? ' '+pixelFixText : '') + ' *{transition:1s}';
  946. setTimeout(() => {
  947. dropStyle.textContent = dropStyleText + (settings.pixelFix ? ' '+pixelFixText : '');
  948. }, 1000);
  949. });
  950. }
  951. else if (location.pathname === "/my/messages/") { // New buttons
  952. const btnStyle = "margin: 10px 5px;";
  953.  
  954. // Select all read messages button.
  955. const selRead = document.createElement('input');
  956. selRead.style = btnStyle;
  957. selRead.value = "Select Read";
  958. selRead.id = "selectread";
  959. selRead.classList.add("major");
  960. selRead.type = "button";
  961.  
  962. // On click, select all messages with the style attribute indicating it as read.
  963. selRead.addEventListener('mouseup', () => {
  964. document.querySelectorAll('td[style="border-left: 8px solid rgb(221, 221, 221);"] > input').forEach((m) => m.click());
  965. });
  966.  
  967. // Select duplicate message (multiple update notifications).
  968. const selDupe = document.createElement('input');
  969. selDupe.style = btnStyle;
  970. selDupe.value = "Select Same";
  971. selDupe.id = "selectdupe";
  972. selDupe.classList.add("major");
  973. selDupe.type = "button";
  974.  
  975. selDupe.addEventListener('mouseup', evt => {
  976. const temp = document.querySelectorAll('#messages > tr');
  977. const msgs = [];
  978. for (let i = temp.length - 1; i >= 0; i--) {
  979. msgs.push(temp[i]);
  980. }
  981. const titles = [];
  982. msgs.forEach((msg) => {
  983. let title = msg.querySelector('a.major').textContent;
  984. if (/^New update: /.test(title)) { // Select only adventure updates
  985. if (titles.indexOf(title) === -1) {
  986. if (msg.querySelector('td').style.cssText !== "border-left: 8px solid rgb(221, 221, 221);") {
  987. titles.push(title);
  988. }
  989. } else {
  990. msg.querySelector('input').click();
  991. }
  992. }
  993. });
  994. });
  995.  
  996. // Maybe add a "Merge Updates" button?
  997. // [Merge Updates] would create a list of updates, similar to [Select Same]
  998.  
  999. // Add buttons to the page.
  1000. const del = document.querySelector('#deletemsgs');
  1001. del.parentNode.appendChild(newBr());
  1002. del.parentNode.appendChild(selRead);
  1003. del.parentNode.appendChild(selDupe);
  1004. }
  1005. else if (location.pathname === "/my/messages/new/" && location.search) { // Auto-fill user when linked from a user page
  1006. const recipientInput = document.querySelector('#addrecipient');
  1007. recipientInput.value = params.u;
  1008. pageLoad(() => {
  1009. const recipientButton = document.querySelector('#addrecipientbtn');
  1010. if (recipientButton) {
  1011. recipientButton.click();
  1012. if (recipientInput.value === "") { // If the button press doesn't work
  1013. return true;
  1014. }
  1015. }
  1016. });
  1017. }
  1018. else if (location.pathname === "/my/stories/") {
  1019. // Add links to buttons
  1020. pageLoad(() => {
  1021. const adventures = document.querySelectorAll('#stories tr');
  1022. if (adventures.length > 0) {
  1023. adventures.forEach(story => {
  1024. const buttons = story.querySelectorAll('input.major');
  1025. const id = story.querySelector('a').href.replace('https://mspfa.com/', '').replace('&p=1', '');
  1026. if (id) {
  1027. addLink(buttons[0], `/my/stories/info/${id}`);
  1028. addLink(buttons[1], `/my/stories/pages/${id}`);
  1029. }
  1030. });
  1031. return true;
  1032. }
  1033. if (pageLoaded) return true;
  1034. });
  1035.  
  1036. // Add user guides
  1037. const guides = ["A Guide To Uploading Your Comic To MSPFA", "MSPFA Etiquette", "Fanventure Guide for Dummies", "CSS Guide", "HTML and CSS Things", ];
  1038. const links = ["https://docs.google.com/document/d/17QI6Cv_BMbr8l06RrRzysoRjASJ-ruWioEtVZfzvBzU/edit?usp=sharing", "/?s=27631", "/?s=29299", "/?s=21099", "/?s=23711"];
  1039. const authors = ["Farfrom Tile", "Radical Dude 42", "nzar", "MadCreativity", "seymour schlong"];
  1040.  
  1041. const parentTd = document.querySelector('.container > tbody > tr:last-child > td');
  1042. const unofficial = parentTd.querySelector('span');
  1043. unofficial.textContent = "Unofficial Guides";
  1044. const guideTable = document.createElement('table');
  1045. const guideTbody = document.createElement('tbody');
  1046. guideTable.style.width = "100%";
  1047. guideTable.style.textAlign = "center";
  1048.  
  1049. guideTable.appendChild(guideTbody);
  1050. parentTd.appendChild(guideTable);
  1051.  
  1052. for (let i = 0; i < guides.length; i++) {
  1053. const guideTr = guideTbody.insertRow(i);
  1054. const guideTd = guideTr.insertCell(0);
  1055. const guideLink = document.createElement('a');
  1056. guideLink.href = links[i];
  1057. guideLink.textContent = guides[i];
  1058. guideLink.className = "major";
  1059. guideTd.appendChild(guideLink);
  1060. guideTd.appendChild(newBr());
  1061. guideTd.appendChild(document.createTextNode('by '+authors[i]));
  1062. guideTd.appendChild(newBr());
  1063. guideTd.appendChild(newBr());
  1064. }
  1065. }
  1066. else if (location.pathname === "/my/stories/info/" && location.search) {
  1067. // Button links
  1068. addLink(document.querySelector('#userfavs'), `/readers/?s=${params.s}`);
  1069. addLink(document.querySelector('#editpages'), `/my/stories/pages/?s=${params.s}`);
  1070. }
  1071. else if (location.pathname === "/my/stories/pages/" && location.search) {
  1072. const adventureID = params.s;
  1073.  
  1074. if (!drafts[adventureID]) {
  1075. drafts[adventureID] = {}
  1076. }
  1077.  
  1078. // Button links
  1079. addLink(document.querySelector('#editinfo'), `/my/stories/info/?s=${adventureID}`);
  1080.  
  1081. // Default spoiler values
  1082. const replaceButton = document.querySelector('#replaceall');
  1083. const spoilerButton = document.createElement('input');
  1084. spoilerButton.classList.add('major');
  1085. spoilerButton.value = 'Default Spoiler Values';
  1086. spoilerButton.type = 'button';
  1087. spoilerButton.id = 'spoilers';
  1088. replaceButton.parentNode.insertBefore(spoilerButton, replaceButton);
  1089. replaceButton.parentNode.insertBefore(newBr(), replaceButton);
  1090. replaceButton.parentNode.insertBefore(newBr(), replaceButton);
  1091.  
  1092. const spoilerSpan = document.createElement('span');
  1093. const spoilerOpen = document.createElement('input');
  1094. const spoilerClose = document.createElement('input');
  1095. spoilerSpan.appendChild(document.createTextNode('Open button text:'));
  1096. spoilerSpan.appendChild(newBr());
  1097. spoilerSpan.appendChild(spoilerOpen);
  1098. spoilerSpan.appendChild(newBr());
  1099. spoilerSpan.appendChild(newBr());
  1100. spoilerSpan.appendChild(document.createTextNode('Close button text:'));
  1101. spoilerSpan.appendChild(newBr());
  1102. spoilerSpan.appendChild(spoilerClose);
  1103.  
  1104. if (!settings.spoilerValues[adventureID]) {
  1105. settings.spoilerValues[adventureID] = {
  1106. open: 'Show',
  1107. close: 'Hide'
  1108. }
  1109. }
  1110.  
  1111. spoilerOpen.value = settings.spoilerValues[adventureID].open;
  1112. spoilerClose.value = settings.spoilerValues[adventureID].close;
  1113.  
  1114. spoilerButton.addEventListener('click', evt => {
  1115. window.MSPFA.dialog('Default Spoiler Values', spoilerSpan, ['Save', 'Cancel'], (output, form) => {
  1116. if (output === 'Save') {
  1117. settings.spoilerValues[adventureID].open = spoilerOpen.value === '' ? 'Show' : spoilerOpen.value;
  1118. settings.spoilerValues[adventureID].close = spoilerClose.value === '' ? 'Hide' : spoilerClose.value;
  1119. if (settings.spoilerValues[adventureID].open === 'Show' && settings.spoilerValues[adventureID].close === 'Hide') {
  1120. delete settings.spoilerValues[adventureID];
  1121. }
  1122. saveData(settings);
  1123. }
  1124. });
  1125. });
  1126.  
  1127. document.querySelector('input[title="Spoiler"]').addEventListener('click', evt => {
  1128. document.querySelector('#dialog input[name="open"]').value = settings.spoilerValues[adventureID].open;
  1129. document.querySelector('#dialog input[name="close"]').value = settings.spoilerValues[adventureID].close;
  1130. document.querySelector('#dialog input[name="open"]').placeholder = settings.spoilerValues[adventureID].open;
  1131. document.querySelector('#dialog input[name="close"]').placeholder = settings.spoilerValues[adventureID].close;
  1132. });
  1133.  
  1134. // Buttonless spoilers
  1135. const flashButton = document.querySelector('input[title="Flash');
  1136. const newSpoilerButton = document.createElement('input');
  1137. newSpoilerButton.setAttribute('data-tag', 'Buttonless Spoiler');
  1138. newSpoilerButton.title = 'Buttonless Spoiler';
  1139. newSpoilerButton.type = 'button';
  1140. 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");';
  1141.  
  1142. newSpoilerButton.addEventListener('click', evt => {
  1143. const bbe = document.querySelector('#bbtoolbar').parentNode.querySelector('textarea');
  1144. if (bbe) {
  1145. bbe.focus();
  1146. const start = bbe.selectionStart;
  1147. const end = bbe.selectionEnd;
  1148. bbe.value = bbe.value.slice(0, start) + '<div class="spoiler"><div>' + bbe.value.slice(start, end) + '</div></div>' + bbe.value.slice(end);
  1149. bbe.selectionStart = start + 26;
  1150. bbe.selectionEnd = end + 26;
  1151. }
  1152. });
  1153.  
  1154. flashButton.parentNode.insertBefore(newSpoilerButton, flashButton);
  1155.  
  1156. // Open preview in new tab with middle mouse
  1157. document.body.addEventListener('mouseup', evt => {
  1158. if (evt.toElement.value === "Preview" && evt.button === 1) {
  1159. evt.toElement.click(); // TODO: Find a way to prevent the middle mouse scroll after clicking there.
  1160. evt.preventDefault();
  1161. return false;
  1162. }
  1163. });
  1164.  
  1165. // -- Drafts --
  1166. // Accessing draft text
  1167. const accessDraftsButton = document.createElement('input');
  1168. accessDraftsButton.classList.add('major');
  1169. accessDraftsButton.value = 'Saved Drafts';
  1170. accessDraftsButton.type = 'button';
  1171. accessDraftsButton.id = 'drafts';
  1172. replaceButton.parentNode.insertBefore(accessDraftsButton, replaceButton);
  1173. accessDraftsButton.parentNode.insertBefore(newBr(), replaceButton);
  1174. accessDraftsButton.parentNode.insertBefore(newBr(), replaceButton);
  1175.  
  1176. accessDraftsButton.addEventListener('click', () => {
  1177. 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.');
  1178. const draftInputTextarea = document.createElement('textarea');
  1179. draftInputTextarea.placeholder = 'Paste your draft data here';
  1180. draftInputTextarea.style = 'width: 100%; box-sizing: border-box; resize: vertical;';
  1181. draftInputTextarea.rows = 8;
  1182. draftDialog.appendChild(newBr());
  1183. draftDialog.appendChild(newBr());
  1184. draftDialog.appendChild(draftInputTextarea);
  1185. setTimeout(() => {
  1186. draftInputTextarea.focus();
  1187. draftInputTextarea.selectionStart = 0;
  1188. draftInputTextarea.selectionEnd = 0;
  1189. draftInputTextarea.scrollTop = 0;
  1190. }, 1);
  1191.  
  1192. draftInputTextarea.value = JSON.stringify(drafts[adventureID], null, 4);
  1193.  
  1194. window.MSPFA.dialog('Saved Drafts', draftDialog, ['Load Draft', 'Cancel'], (output, form) => {
  1195. if (output === "Load Draft") {
  1196. if (draftInputTextarea.value === '') {
  1197. setTimeout(() => {
  1198. 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) => {
  1199. if (output === "Delete") {
  1200. drafts[adventureID] = {};
  1201.  
  1202. if (settings.drafts[adventureID]) {
  1203. delete settings.drafts[adventureID];
  1204. saveData(settings);
  1205. }
  1206.  
  1207. saveDrafts(drafts);
  1208. }
  1209. });
  1210. }, 1);
  1211. } else if (draftInputTextarea.value !== JSON.stringify(drafts[adventureID], null, 4)) {
  1212. setTimeout(() => {
  1213. 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) => {
  1214. if (output === "Load") {
  1215. let newData = {};
  1216. try { // Just in case the data given is invalid.
  1217. newData = JSON.parse(draftInputTextarea.value);
  1218. } catch (err) {
  1219. console.error(err);
  1220. setTimeout(() => {
  1221. window.MSPFA.dialog('Error', window.MSPFA.parseBBCode('The entered data is invalid.'), ["Okay"]);
  1222. }, 1);
  1223. return;
  1224. }
  1225.  
  1226. drafts[adventureID] = newData;
  1227. saveDrafts(drafts);
  1228. }
  1229. });
  1230. }, 1);
  1231. }
  1232. }
  1233. });
  1234. });
  1235.  
  1236. // Draft stuff
  1237. const msg = document.createElement('span');
  1238. msg.appendChild(document.createTextNode('Command:'));
  1239. msg.appendChild(document.createElement('br'));
  1240.  
  1241. const commandInput = document.createElement('input');
  1242. commandInput.style = 'width: 100%; box-sizing: border-box;';
  1243. commandInput.readOnly = true;
  1244. commandInput.value = 'yes';
  1245.  
  1246. msg.appendChild(commandInput);
  1247. msg.appendChild(document.createElement('br'));
  1248. msg.appendChild(document.createElement('br'));
  1249.  
  1250. msg.appendChild(document.createTextNode('Body:'));
  1251.  
  1252. const bodyInput = document.createElement('textarea');
  1253. bodyInput.style = 'width: 100%; box-sizing: border-box; resize: vertical;';
  1254. bodyInput.readOnly = true;
  1255. bodyInput.rows = 8;
  1256. bodyInput.textContent = '';
  1257.  
  1258. msg.appendChild(bodyInput);
  1259.  
  1260. const showDraftDialog = (pageNum) => {
  1261. const pageElement = document.querySelector(`#p${pageNum}`);
  1262.  
  1263. let shownMessage = msg;
  1264. let optionButtons = [];
  1265.  
  1266. const commandElement = pageElement.querySelector('input[name="cmd"]');
  1267. const pageContentElement = pageElement.querySelector('textarea[name="body"]');
  1268.  
  1269. if (typeof drafts[adventureID][pageNum] === "undefined") {
  1270. shownMessage = document.createTextNode('There is no draft saved for this page.');
  1271. optionButtons = ["Save New", "Close"];
  1272. } else {
  1273. commandInput.value = drafts[adventureID][pageNum].command;
  1274. bodyInput.textContent = drafts[adventureID][pageNum].pageContent;
  1275. optionButtons = ["Save New", "Load", "Delete", "Close"];
  1276. }
  1277.  
  1278. window.MSPFA.dialog(`Page ${pageNum} Draft`, shownMessage, optionButtons, (output, form) => {
  1279. if (output === "Save New") {
  1280. if (typeof drafts[adventureID][pageNum] === "undefined") {
  1281. drafts[adventureID][pageNum] = {
  1282. command: commandElement.value,
  1283. pageContent: pageContentElement.value
  1284. }
  1285. saveDrafts(drafts);
  1286. } else {
  1287. setTimeout(() => {
  1288. 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) => {
  1289. if (output === "Yes") {
  1290. drafts[adventureID][pageNum] = {
  1291. command: commandElement.value,
  1292. pageContent: pageContentElement.value
  1293. }
  1294. saveDrafts(drafts);
  1295. }
  1296. });
  1297. }, 1);
  1298. }
  1299. } else if (output === "Load") {
  1300. if (pageContentElement.value === '' && (commandElement.value === '' || commandElement.value === document.querySelector('#defaultcmd').value)) {
  1301. commandElement.value = drafts[adventureID][pageNum].command;
  1302. pageContentElement.value = drafts[adventureID][pageNum].pageContent;
  1303. pageElement.querySelector('input[value="Save"]').disabled = false;
  1304. } else {
  1305. setTimeout(() => {
  1306. 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) => {
  1307. if (output === "Yes") {
  1308. commandElement.value = drafts[adventureID][pageNum].command;
  1309. pageContentElement.value = drafts[adventureID][pageNum].pageContent;
  1310. pageElement.querySelector('input[value="Save"]').disabled = false;
  1311. }
  1312. });
  1313. }, 1);
  1314. }
  1315. } else if (output === "Delete") {
  1316. setTimeout(() => {
  1317. window.MSPFA.dialog('Delete this draft?', document.createTextNode('This action is unreversable! Are you sure?'), ["Yes", "No"], (output, form) => {
  1318. if (output === "Yes") {
  1319. delete drafts[adventureID][pageNum];
  1320.  
  1321. if (settings.drafts[adventureID] && settings.drafts[adventureID][pageNum]) {
  1322. delete settings.drafts[adventureID][pageNum];
  1323. saveData(settings);
  1324. }
  1325.  
  1326. saveDrafts(drafts);
  1327. }
  1328. });
  1329. }, 1);
  1330. }
  1331. });
  1332. }
  1333.  
  1334. const createDraftButton = (form) => {
  1335. const draftButton = document.createElement('input');
  1336. draftButton.className = 'major draft';
  1337. draftButton.type = 'button';
  1338. draftButton.value = 'Draft';
  1339. draftButton.style = 'margin-right: 9.5px;';
  1340. draftButton.addEventListener('click', () => {
  1341. showDraftDialog(form.id.replace('p', ''));
  1342. });
  1343. return draftButton;
  1344. }
  1345.  
  1346. pageLoad(() => {
  1347. let allPages = document.querySelectorAll('#storypages form:not(#newpage)');
  1348. if (allPages.length !== 0) {
  1349. allPages.forEach(form => {
  1350. const prevButton = form.querySelector('input[name="preview"]');
  1351. prevButton.parentNode.insertBefore(createDraftButton(form), prevButton);
  1352. });
  1353. document.querySelector('input[value="Add"]').addEventListener('click', () => {
  1354. allPages = document.querySelectorAll('#storypages form:not(#newpage)');
  1355. const form = document.querySelector(`#p${allPages.length}`);
  1356. const prevButton = form.querySelector('input[name="preview"]');
  1357. prevButton.parentNode.insertBefore(createDraftButton(form), prevButton);
  1358. });
  1359. return true;
  1360. }
  1361. });
  1362.  
  1363. if (params.click) {
  1364. if (params.click === 's') {
  1365. spoilerButton.click();
  1366. } else if (params.click === 'd') {
  1367. accessDraftsButton.click();
  1368. }
  1369. }
  1370.  
  1371. /* // Removed because apparently MSPFA already does this fine!
  1372. if (hashSearch) {
  1373. pageLoad(() => {
  1374. const element = document.querySelector(hashSearch);
  1375. if (element) {
  1376. if (element.style.display === "none") {
  1377. element.style = '';
  1378. }
  1379. return true;
  1380. }
  1381. });
  1382. }/**/
  1383. }
  1384. else if (location.pathname === "/my/profile/") {
  1385. // Add profile CSS box
  1386. /*
  1387. pageLoad(() => {
  1388. if (window.MSPFA && window.MSPFA.me && window.MSPFA.me.i) {
  1389. // add 15 to char cap for <style></style>
  1390. // replace
  1391. const bio = document.querySelector('textarea[name="userdesc"]');
  1392. const styleRow = document.querySelector('#editprofile tbody').insertRow(7);
  1393. styleRow.style = 'text-align: left;';
  1394. const styleCell = styleRow.insertCell(0);
  1395. styleCell.colSpan = 2;
  1396. const styleTextarea = document.createElement('textarea');
  1397. styleTextarea.style = 'width: 100%; box-sizing: border-box; resize: vertical;';
  1398. styleTextarea.rows = 8;
  1399. styleCell.appendChild(document.createTextNode('Custom profile style:'));
  1400. styleCell.appendChild(newBr());
  1401. styleCell.appendChild(styleTextarea);
  1402.  
  1403. styleTextarea.value = /<style>(.*?)<\/style>/i.exec(bio.value)[1];
  1404. bio.value = bio.value.replace(/<style>(.*?)<\/style>/i, '');
  1405.  
  1406. return true;
  1407. }
  1408. },0.25);/**/
  1409. }
  1410. else if (location.pathname === "/user/") {
  1411. // Button links
  1412. pageLoad(() => {
  1413. const msgButton = document.querySelector('#sendmsg');
  1414. if (msgButton) {
  1415. addLink(msgButton, `/my/messages/new/?u=${params.u}`);
  1416. addLink(document.querySelector('#favstories'), `/favs/?u=${params.u}`);
  1417. return true;
  1418. }
  1419. });
  1420.  
  1421. // Add extra user stats
  1422. pageLoad(() => {
  1423. if (window.MSPFA) {
  1424. const stats = document.querySelector('#userinfo table');
  1425.  
  1426. const joinTr = stats.insertRow(1);
  1427. const joinTextTd = joinTr.insertCell(0);
  1428. joinTextTd.appendChild(document.createTextNode("Account created:"));
  1429. const joinDate = joinTr.insertCell(1);
  1430. const joinTime = document.createElement('b');
  1431. joinTime.textContent = "Loading...";
  1432. joinDate.appendChild(joinTime);
  1433.  
  1434. const advCountTr = stats.insertRow(2);
  1435. const advTextTd = advCountTr.insertCell(0);
  1436. advTextTd.appendChild(document.createTextNode("Adventures created:"));
  1437. const advCount = advCountTr.insertCell(1);
  1438. const advCountText = document.createElement('b');
  1439. advCountText.textContent = "Loading...";
  1440. advCount.appendChild(advCountText);
  1441.  
  1442. // Show adventure creation date
  1443. window.MSPFA.request(0, {
  1444. do: "user",
  1445. u: params.u
  1446. }, user => {
  1447. if (typeof user !== "undefined") {
  1448. joinTime.textContent = new Date(user.d).toString().split(' ').splice(1, 4).join(' ');
  1449. }
  1450.  
  1451. // Show created adventures
  1452. window.MSPFA.request(0, {
  1453. do: "editor",
  1454. u: params.u
  1455. }, s => {
  1456. if (typeof s !== "undefined") {
  1457. advCountText.textContent = s.length;
  1458. }
  1459.  
  1460. // Show favourites
  1461. if (document.querySelector('#favstories').style.display !== 'none') {
  1462. const favCountTr = stats.insertRow(3);
  1463. const favTextTd = favCountTr.insertCell(0);
  1464. favTextTd.appendChild(document.createTextNode("Adventures favorited:"));
  1465. const favCount = favCountTr.insertCell(1);
  1466. const favCountText = document.createElement('b');
  1467. favCountText.textContent = "Loading...";
  1468. window.MSPFA.request(0, {
  1469. do: "favs",
  1470. u: params.u
  1471. }, s => {
  1472. if (typeof s !== "undefined") {
  1473. favCountText.textContent = s.length;
  1474. }
  1475. });
  1476. favCount.appendChild(favCountText);
  1477. }
  1478. });
  1479. });
  1480.  
  1481. return true;
  1482. }
  1483. });
  1484. }
  1485. else if (location.pathname === "/favs/" && location.search) {
  1486. // Button links
  1487. pageLoad(() => {
  1488. const stories = document.querySelectorAll('#stories tr');
  1489. let favCount = 0;
  1490.  
  1491. if (stories.length > 0) {
  1492. stories.forEach(story => {
  1493. favCount++;
  1494. const id = story.querySelector('a').href.replace('https://mspfa.com/', '');
  1495. pageLoad(() => {
  1496. if (window.MSPFA.me.i) {
  1497. addLink(story.querySelector('.edit.major'), `/my/stories/info/${id}`);
  1498. return true;
  1499. }
  1500. if (pageLoaded) return true;
  1501. });
  1502. addLink(story.querySelector('.rss.major'), `/rss/${id}`);
  1503. });
  1504.  
  1505. // Fav count
  1506. const username = document.querySelector('#username');
  1507. username.parentNode.appendChild(newBr());
  1508. username.parentNode.appendChild(newBr());
  1509. username.parentNode.appendChild(document.createTextNode(`Favorited adventures: ${favCount}`));
  1510.  
  1511. return true;
  1512. }
  1513. if (pageLoaded) return true;
  1514. });
  1515. }
  1516. else if (location.pathname === "/search/" && location.search) {
  1517. // Character and word statistics
  1518. const statTable = document.createElement('table');
  1519. const statTbody = document.createElement('tbody');
  1520. const statTr = statTbody.insertRow(0);
  1521. const charCount = statTr.insertCell(0);
  1522. const wordCount = statTr.insertCell(0);
  1523. const statParentTr = document.querySelector('#pages').parentNode.parentNode.insertRow(2);
  1524. const statParentTd = statParentTr.insertCell(0);
  1525.  
  1526. const statHeaderTr = statTbody.insertRow(0);
  1527. const statHeader = document.createElement('th');
  1528. statHeader.colSpan = '2';
  1529.  
  1530. statHeaderTr.appendChild(statHeader);
  1531. statHeader.textContent = 'Statistics may not be entirely accurate.';
  1532.  
  1533. statTable.style.width = "100%";
  1534.  
  1535. charCount.textContent = "Character count: loading...";
  1536. wordCount.textContent = "Word count: loading...";
  1537.  
  1538. statTable.appendChild(statTbody);
  1539. statParentTd.appendChild(statTable);
  1540.  
  1541. pageLoad(() => {
  1542. if (document.querySelector('#pages br')) {
  1543. const bbc = window.MSPFA.BBC.slice();
  1544. bbc.splice(0, 3);
  1545.  
  1546. window.MSPFA.request(0, {
  1547. do: "story",
  1548. s: params.s
  1549. }, story => {
  1550. if (typeof story !== "undefined") {
  1551. const pageContent = [];
  1552. story.p.forEach(p => {
  1553. pageContent.push(p.c);
  1554. pageContent.push(p.b);
  1555. });
  1556.  
  1557. const storyText = pageContent.join(' ')
  1558. .replace(/\n/g, ' ')
  1559. .replace(bbc[0][0], '$1')
  1560. .replace(bbc[1][0], '$1')
  1561. .replace(bbc[2][0], '$1')
  1562. .replace(bbc[3][0], '$1')
  1563. .replace(bbc[4][0], '$2')
  1564. .replace(bbc[5][0], '$3')
  1565. .replace(bbc[6][0], '$3')
  1566. .replace(bbc[7][0], '$3')
  1567. .replace(bbc[8][0], '$3')
  1568. .replace(bbc[9][0], '$3')
  1569. .replace(bbc[10][0], '$2')
  1570. .replace(bbc[11][0], '$1')
  1571. .replace(bbc[12][0], '$3')
  1572. .replace(bbc[13][0], '$3')
  1573. .replace(bbc[14][0], '')
  1574. .replace(bbc[16][0], '$1')
  1575. .replace(bbc[17][0], '$2 $4 $5')
  1576. .replace(bbc[18][0], '$2 $4 $5')
  1577. .replace(bbc[19][0], '')
  1578. .replace(bbc[20][0], '')
  1579. .replace(/<(.*?)>/g, '');
  1580.  
  1581. wordCount.textContent = `Word count: ${storyText.split(/ +/g).length}`;
  1582. charCount.textContent = `Character count: ${storyText.replace(/ +/g, '').length}`;
  1583. }
  1584. });
  1585. return true;
  1586. }
  1587. });
  1588. }
  1589. else if (location.pathname === "/stories/" && location.search) {
  1590. const adventureList = document.querySelector('#doit');
  1591. const resultAmount = document.createElement('span');
  1592. adventureList.parentNode.appendChild(resultAmount);
  1593.  
  1594. pageLoad(() => {
  1595. if (window.MSPFA) {
  1596. window.MSPFA.request(0, {
  1597. do: "stories",
  1598. n: params.n,
  1599. t: params.t,
  1600. h: params.h,
  1601. o: params.o,
  1602. p: params.p,
  1603. m: 20000
  1604. }, (s) => {
  1605. resultAmount.textContent = `Number of results: ${s.length}`;
  1606. return true;
  1607. });
  1608. return true;
  1609. }
  1610. },1);
  1611.  
  1612. pageLoad(() => {
  1613. const stories = document.querySelector('#stories');
  1614. if (stories.childNodes.length > 0) {
  1615. stories.querySelectorAll('tr').forEach(story => {
  1616. const storyID = story.querySelector('a.major').href.split('&')[0].replace(/\D/g, '');
  1617. addLink(story.querySelector('.rss'), `/rss/?s=${storyID}`);
  1618.  
  1619. pageLoad(() => {
  1620. if (window.MSPFA.me.i) {
  1621. addLink(story.querySelector('.edit.major'), `/my/stories/info/?s=${storyID}`);
  1622. return true;
  1623. }
  1624. if (pageLoaded) return true;
  1625. });
  1626. });
  1627. return true;
  1628. }
  1629. if (pageLoaded) return true;
  1630. });
  1631. }
  1632. })();