MSPFA extras

Adds custom quality of life features to MSPFA.

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

  1. // ==UserScript==
  2. // @name MSPFA extras
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.7.5
  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. (function() {
  21. 'use strict';
  22.  
  23. const currentVersion = "1.7.5";
  24. console.log(`MSPFA extras script v${currentVersion} by seymour schlong`);
  25.  
  26. const debug = false;
  27.  
  28. /**
  29. * https://github.com/GrantGryczan/MSPFA/projects/1?fullscreen=true
  30. * Github to-do completion list (and other stuff too)
  31. *
  32. * https://github.com/GrantGryczan/MSPFA/issues/26 - Dropdown menu - February 23rd, 2020
  33. * https://github.com/GrantGryczan/MSPFA/issues/18 - MSPFA themes - February 23rd, 2020
  34. * https://github.com/GrantGryczan/MSPFA/issues/32 - Adventure creation dates - February 23rd, 2020
  35. * https://github.com/GrantGryczan/MSPFA/issues/32 - User creation dates - February 23rd, 2020
  36. * https://github.com/GrantGryczan/MSPFA/issues/40 - Turn certain buttons into links - July 21st, 2020
  37. * https://github.com/GrantGryczan/MSPFA/issues/41 - Word and character count - July 21st, 2020
  38. * https://github.com/GrantGryczan/MSPFA/issues/57 - Default spoiler values - August 7th, 2020
  39. * https://github.com/GrantGryczan/MSPFA/issues/62 - Buttonless spoilers - August 7th, 2020
  40. * https://github.com/GrantGryczan/MSPFA/issues/52 - Hash URLs - August 8th, 2020
  41. * - Page drafts - August 8th, 2020
  42. * - Edit pages button - August 8th, 2020
  43. * - Image preloading - August 20th, 2020
  44. * https://github.com/GrantGryczan/MSPFA/issues/19 - Manage game saves - August 22nd, 2020
  45. *
  46. * Extension to-do... maybe...
  47. *
  48. * 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.
  49. * When adding a new page, store it in an array and if that array length is > 1 when someone tries to save, prompt them to press Save All?
  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, target) => {
  79. const link = document.createElement('a');
  80. link.href = url;
  81. link.draggable = false;
  82. if (elm.parentNode) elm.parentNode.insertBefore(link, elm);
  83. if (target) link.target = target;
  84. link.appendChild(elm);
  85. return link;
  86. };
  87.  
  88. // Easy br element
  89. const newBr = () => {
  90. return document.createElement('br');
  91. }
  92.  
  93. // Make creating label elements easier
  94. const createLabel = (text, id) => {
  95. const newLabel = document.createElement('label');
  96. newLabel.textContent = text;
  97. newLabel.setAttribute('for', id);
  98. return newLabel;
  99. }
  100.  
  101. let settings = {};
  102. let drafts = {};
  103.  
  104. const defaultSettings = {
  105. autospoiler: false,
  106. style: 0,
  107. styleURL: "",
  108. night: false,
  109. auto502: true,
  110. textFix: false,
  111. pixelFix: false,
  112. intro: false,
  113. commandScroll: false,
  114. preload: true,
  115. dialogKeys: true,
  116. dialogFocus: false,
  117. spoilerValues: {}
  118. }
  119.  
  120. let pageLoaded = false;
  121.  
  122. const loadDrafts = () => {
  123. if (localStorage.mspfadrafts) {
  124. drafts = JSON.parse(localStorage.mspfadrafts);
  125. }
  126. }
  127. loadDrafts();
  128.  
  129. // Load any previous settings from localStorage
  130. if (localStorage.mspfaextra) {
  131. Object.assign(settings, JSON.parse(localStorage.mspfaextra));
  132.  
  133. // Get draft data from settings
  134. if (typeof settings.drafts === "object") {
  135. if (Object.keys(settings.drafts).length > 0 && Object.keys(drafts).length === 0) {
  136. drafts = settings.drafts;
  137. }
  138. }
  139. saveDrafts(drafts);
  140. }
  141.  
  142. // If any settings are undefined, re-set to their default state. (For older users when new things get stored)
  143. const checkSettings = () => {
  144. const defaultSettingsKeys = Object.keys(defaultSettings);
  145. for (let i = 0; i < defaultSettingsKeys.length; i++) {
  146. if (typeof settings[defaultSettingsKeys[i]] === "undefined") {
  147. settings[defaultSettingsKeys[i]] = defaultSettings[defaultSettingsKeys[i]];
  148. }
  149. }
  150. saveData(settings);
  151. }
  152.  
  153. checkSettings();
  154.  
  155. if (GM_info && GM_info.scriptHandler !== "Tampermonkey" && !settings.warned) {
  156. alert(`It appears that you're running the MSPFA extras script with ${GM_info.scriptHandler}.\nUnfortunately, this script cannot run at its full potential because of that.\nTry switching to Tampermonkey if you want to use more of the features!\n(this message will only appear once.)`);
  157. settings.warned = true;
  158. saveData(settings);
  159. }
  160.  
  161. // Scrolls you to where you need to be
  162. const hashSearch = location.href.replace(location.origin + location.pathname, '').replace(location.search, '');
  163. if (hashSearch !== '') {
  164. pageLoad(() => {
  165. const idElement = document.querySelector(hashSearch);
  166. if (idElement) {
  167. const selected = document.querySelector(hashSearch);
  168. selected.scrollIntoView();
  169. selected.style.outline = '3px solid black';
  170. selected.style.transition = '0.5s';
  171. pageLoad(() => {
  172. if (pageLoaded) {
  173. selected.style.outline = '0px solid black';
  174. }
  175. });
  176.  
  177. return true;
  178. }
  179. }, 1);
  180. }
  181.  
  182. // Ripped shamelessly right from mspfa lol (URL search parameters -- story ID, page num, etc.)
  183. let rawParams;
  184. if (location.href.indexOf("#") != -1) {
  185. rawParams = location.href.slice(0, location.href.indexOf("#"));
  186. } else {
  187. rawParams = location.href;
  188. }
  189. if (rawParams.indexOf("?") != -1) {
  190. rawParams = rawParams.slice(rawParams.indexOf("?") + 1).split("&");
  191. } else {
  192. rawParams = [];
  193. }
  194. const params = {};
  195. for (let i = 0; i < rawParams.length; i++) {
  196. try {
  197. const p = rawParams[i].split("=");
  198. params[p[0]] = decodeURIComponent(p[1]);
  199. } catch (err) {}
  200. }
  201.  
  202. if (debug) {
  203. console.log('URL parameters:');
  204. console.log(params);
  205. }
  206.  
  207. // Functions to get/change data from the console
  208. window.MSPFAe = {
  209. getSettings: () => {
  210. return settings;
  211. },
  212. getSettingsString: (formatted) => {
  213. if (formatted) {
  214. console.log(JSON.stringify(settings, null, 4));
  215. } else {
  216. console.log(JSON.stringify(settings));
  217. }
  218. },
  219. getDrafts: () => {
  220. loadDrafts();
  221. return drafts;
  222. },
  223. getDraftsString: (formatted) => {
  224. loadDrafts();
  225. if (formatted) {
  226. console.log(JSON.stringify(drafts, null, 4));
  227. } else {
  228. console.log(JSON.stringify(drafts));
  229. }
  230. },
  231. changeSettings: (newSettings) => {
  232. console.log('Settings updated');
  233. console.log(settings);
  234. Object.assign(settings, newSettings);
  235. saveData(settings);
  236. },
  237. changeSettingsString: (fullString) => {
  238. try {
  239. JSON.parse(fullString);
  240. } catch (err) {
  241. console.error(err);
  242. return;
  243. }
  244. settings = JSON.parse(fullString);
  245. checkSettings();
  246. console.log(settings);
  247. },
  248. getParams: params
  249. }
  250.  
  251. // Error reloading
  252. window.addEventListener("load", () => {
  253. // Reload the page if 502 CloudFlare error page appears
  254. if (settings.auto502 && document.querySelector('#cf-wrapper')) {
  255. window.location.reload();
  256. }
  257.  
  258. // Wait five seconds, then refresh the page
  259. if (document.body.textContent === "Your client is sending data to MSPFA too quickly. Wait a moment before continuing.") {
  260. setTimeout(() => {
  261. window.location.reload();
  262. }, 5000);
  263. }
  264.  
  265. pageLoaded = true;
  266. });
  267.  
  268. // Delete any unchanged spoiler values
  269. if (location.pathname !== "/my/stories/pages/") {
  270. // Go through spoiler values and remove any that aren't unique
  271. Object.keys(settings.spoilerValues).forEach(adventure => {
  272. if (settings.spoilerValues[adventure].open === "Show" && settings.spoilerValues[adventure].close === "Hide") {
  273. delete settings.spoilerValues[adventure];
  274. } else if (settings.spoilerValues[adventure].open === '' && settings.spoilerValues[adventure].close === '') {
  275. delete settings.spoilerValues[adventure];
  276. }
  277. });
  278. }
  279.  
  280. const styleOptions = ["Standard", "Low Contrast", "Light", "Dark", "Felt", "Trickster", "Custom"];
  281. const styleUrls = ['', '/css/theme1.css', '/css/theme2.css', 'https://pipe.miroware.io/5b52ba1d94357d5d623f74aa/mspfa/themes/dark.css', '/css/theme4.css', '/css/theme5.css'];
  282.  
  283. const createDropdown = (parent, explore) => {
  284. const dropDiv = document.createElement('div');
  285. dropDiv.className = 'dropdown';
  286. dropDiv.style.display = 'inline-block';
  287.  
  288. const dropContent = document.createElement('div');
  289. dropContent.className = 'dropdown-content';
  290. dropContent.style.display = 'none';
  291.  
  292. dropDiv.addEventListener('mouseenter', evt => {
  293. dropContent.style.display = 'block';
  294. dropContent.style.color = getComputedStyle(parent).color;
  295. dropContent.querySelectorAll('a').forEach(link => {
  296. link.style.color = getComputedStyle(parent).color;
  297. });
  298. });
  299.  
  300. if (!explore) {
  301. dropDiv.addEventListener('mouseleave', evt => {
  302. dropContent.style.display = 'none';
  303. });
  304. }
  305.  
  306. parent.parentNode.insertBefore(dropDiv, parent);
  307. dropDiv.appendChild(parent);
  308. dropDiv.appendChild(dropContent);
  309. return [dropDiv, dropContent];
  310. }
  311.  
  312. // "MY MSPFA" dropdown
  313. const myLink = document.querySelector('nav a[href="/my/"]');
  314. if (myLink) {
  315. const dropContent = createDropdown(myLink)[1];
  316.  
  317. const dLinks = [];
  318. dLinks[0] = [ 'Messages', 'My Adventures', 'Settings' ];
  319. dLinks[1] = [ '/my/messages/', '/my/stories/', '/my/settings/' ];
  320.  
  321. for (let i = 0; i < dLinks[0].length; i++) {
  322. const newLink = document.createElement('a');
  323. newLink.textContent = dLinks[0][i];
  324. newLink.href = dLinks[1][i];
  325. dropContent.appendChild(newLink);
  326. }
  327.  
  328. // Append "My Profile" to the dropdown list if you're signed in
  329. pageLoad(() => {
  330. if (window.MSPFA) {
  331. if (window.MSPFA.me.n) {
  332. const newFavLink = document.createElement('a');
  333. newFavLink.textContent = "My Favourites";
  334. newFavLink.href = `/favs/?u=${window.MSPFA.me.i}`;
  335.  
  336. const newMyLink = document.createElement('a');
  337. newMyLink.textContent = "My Profile";
  338. newMyLink.href = `/user/?u=${window.MSPFA.me.i}`;
  339.  
  340. dropContent.appendChild(newFavLink);
  341. dropContent.appendChild(newMyLink);
  342.  
  343. // Move SETTINGS to the bottom
  344. dropContent.appendChild(dropContent.querySelectorAll('a')[2]);
  345. return true;
  346. }
  347. }
  348. },.1);
  349. }
  350.  
  351. // "RANDOM" dropdown
  352. const randomLink = document.querySelector('nav a[href="/random/"]');
  353. if (randomLink) {
  354. // Thank you @MadCreativity 🙏
  355. const dropContent = createDropdown(randomLink)[1];
  356.  
  357. (async () => {
  358. const dLinks = [];
  359. dLinks[0] = [ 'Recent ongoing' ];
  360. dLinks[1] = [ await fetch(`https://mspfa-extras-server.herokuapp.com/api/random`).then(e => e.text()) ];
  361.  
  362. for (let i = 0; i < dLinks[0].length; i++) {
  363. const newLink = document.createElement('a');
  364. newLink.textContent = dLinks[0][i];
  365. newLink.href = dLinks[1][i];
  366. dropContent.appendChild(newLink);
  367. }
  368. })()
  369. }
  370.  
  371. // "EXPLORE" dropdown
  372. const exploreLink = document.querySelector('nav a[href="/stories/"');
  373. if (exploreLink) {
  374. const dropdown = createDropdown(exploreLink, true);
  375. const dropDiv = dropdown[0];
  376. const dropContent = dropdown[1];
  377.  
  378. const userLink = document.createElement('a');
  379. userLink.textContent = 'User Search';
  380. userLink.href = '/?s=36596&p=7'
  381. dropContent.appendChild(userLink);
  382.  
  383. const exploreInput = document.createElement('input');
  384. Object.assign(exploreInput, { type: 'text', placeholder: 'Search...', id: 'dropdown-explore' });
  385. dropContent.appendChild(exploreInput);
  386. exploreInput.addEventListener('keydown', ke => {
  387. if (ke.code === 'Enter') {
  388. const searchLink = `/stories/?go=1&n=${encodeURIComponent(exploreInput.value)}&t=&h=14&o=favs&p=p&m=50&load=true`;
  389. if (ke.altKey || ke.ctrlKey) {
  390. window.open(searchLink, '_blank').focus();
  391. } else {
  392. location.href = searchLink;
  393. }
  394. return;
  395. }
  396. });
  397. dropDiv.addEventListener('mouseleave', evt => {
  398. // If input is focused
  399. if (document.activeElement !== exploreInput) {
  400. dropContent.style.display = 'none';
  401. }
  402. });
  403. document.body.addEventListener('click', evt => {
  404. if (document.activeElement !== exploreInput) {
  405. dropContent.style.display = 'none';
  406. }
  407. });
  408. }
  409.  
  410. document.querySelector('header .mspfalogo').parentNode.draggable = false;
  411. addLink(document.querySelector('footer .mspfalogo'), 'javascript:void(0);');
  412.  
  413. // Message that shows when you first get the script
  414. const showIntroDialog = () => {
  415. 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]');
  416. window.MSPFA.dialog("MSPFA extras message", msg, ["Okay"]);
  417. }
  418.  
  419. // Check if show intro dialog has displayed
  420. if (!settings.intro) {
  421. pageLoad(() => {
  422. if (window.MSPFA) {
  423. showIntroDialog();
  424. settings.intro = true;
  425. saveData(settings);
  426. return true;
  427. }
  428. });
  429. }
  430.  
  431. const details = document.querySelector('#details');
  432.  
  433. // Add 'link' at the bottom to show the intro dialog again
  434. const introLink = document.createElement('a');
  435. introLink.textContent = 'View Script Message';
  436. introLink.href = 'javascript:void(0);';
  437. introLink.addEventListener('click', showIntroDialog);
  438. details.appendChild(introLink);
  439.  
  440. // vbar!!!!
  441. const vbar = document.createElement('span');
  442. Object.assign(vbar, {className: 'vbar', textContent: '|'});
  443. details.appendChild(document.createTextNode(' '));
  444. details.appendChild(vbar);
  445. details.appendChild(document.createTextNode(' '));
  446.  
  447. // if you really enjoy the script and has some extra moneys 🥺
  448. const donateLink = document.createElement('a');
  449. donateLink.textContent = 'Donate';
  450. donateLink.href = 'https://ko-fi.com/ironbean';
  451. donateLink.target = "blank";
  452. details.appendChild(donateLink);
  453.  
  454. // Theme stuff
  455. const theme = document.createElement('link');
  456. Object.assign(theme, { id: 'theme', type: 'text/css', rel: 'stylesheet' });
  457. const updateTheme = (src) => {
  458. theme.href = src;
  459. }
  460. if (!document.querySelector('#theme')) {
  461. document.querySelector('head').appendChild(theme);
  462. if (settings.night) {
  463. updateTheme(styleUrls[3]);
  464. } else {
  465. updateTheme(settings.style == styleOptions.length - 1 ? settings.styleURL : styleUrls[settings.style]);
  466. }
  467. }
  468.  
  469. const pixelText = () => {
  470. return settings.pixelFix ? 'body { image-rendering: pixelated; image-rendering: -moz-crisp-edges; } .cellicon { image-rendering: -webkit-optimize-contrast !important; }' : '';
  471. }
  472.  
  473. // Dropdown menu and pixelated scaling
  474. const mspfaeCSS = document.createElement('link');
  475. Object.assign(mspfaeCSS, { id: 'script-css', type: 'text/css', rel: 'stylesheet', href: 'https://pipe.miroware.io/5b52ba1d94357d5d623f74aa/mspfa/mspfae.css' });
  476. document.querySelector('head').appendChild(mspfaeCSS);
  477.  
  478. const extraStyle = document.createElement('style');
  479. if (!document.querySelector('#extra-style')) {
  480. extraStyle.id = 'extra-style';
  481. extraStyle.textContent = pixelText();
  482. document.querySelector('head').appendChild(extraStyle);
  483. }
  484.  
  485. let nightSwitch = [];
  486.  
  487. // Enabling night mode.
  488. document.querySelector('footer .mspfalogo').addEventListener('click', evt => {
  489. settings.night = !settings.night;
  490. saveData(settings);
  491.  
  492. for (let i = 0; i < nightSwitch.length; i++) {
  493. clearTimeout(nightSwitch[i]);
  494. }
  495. nightSwitch = [];
  496.  
  497. // Transition to make it feel nicer on the eyes
  498. extraStyle.textContent = pixelText();
  499. extraStyle.textContent = pixelText() + ' *{transition:1.5s;}';
  500.  
  501. if (settings.night) {
  502. updateTheme(styleUrls[3]);
  503. } else {
  504. updateTheme(settings.style == styleOptions.length - 1 ? settings.styleURL : styleUrls[settings.style]);
  505. }
  506.  
  507. nightSwitch.push(setTimeout(() => {
  508. extraStyle.textContent = pixelText();
  509. }, 1500));
  510. });
  511.  
  512. // Enable keyboard controls for some dialog boxes (enter/esc to accept/close)
  513. const dialog = document.querySelector('#dialog');
  514. document.addEventListener('keydown', evt => {
  515. if (settings.dialogKeys && !dialog.textContent.includes('BBCode')) {
  516. if (dialog.style.display === '' && (evt.code === 'Enter' || evt.code === "Escape") && (document.activeElement === document.body || settings.dialogFocus)) {
  517. let buttons = dialog.querySelectorAll('button');
  518. if (buttons.length === 1) {
  519. buttons[0].click();
  520. } else if (buttons.length === 2) {
  521. if (["Okay", "Yes"].indexOf(buttons[0].textContent) !== -1 && evt.code === "Enter") {
  522. buttons[0].click();
  523. }
  524. }
  525. if (["Cancel", "Close", "No"].indexOf(buttons[buttons.length - 1].textContent) !== -1 && evt.code === "Escape") {
  526. buttons[buttons.length - 1].click();
  527. }
  528. }
  529. }
  530. });
  531.  
  532. if (location.pathname.includes('//')) {
  533. location.href = location.pathname.replace(/\/\//g, '/') + location.search;
  534. }
  535.  
  536. if (location.pathname === "/" || location.pathname === "/preview/") {
  537. if (location.search) {
  538. // Remove the current theme if the adventure has CSS (to prevent conflicts);
  539. if (settings.style > 0) {
  540. pageLoad(() => {
  541. if (window.MSPFA) {
  542. if (window.MSPFA.story && window.MSPFA.story.y && (window.MSPFA.story.y.toLowerCase().includes('import') || window.MSPFA.story.y.includes('{'))) {
  543. if (!settings.night) updateTheme('');
  544. return true;
  545. }
  546. }
  547. if (pageLoaded) return true;
  548. });
  549. }
  550.  
  551. // Preload adjacent pages
  552. if (settings.preload) {
  553. const preloadImages = document.createElement('div');
  554. preloadImages.id = 'preload';
  555. document.querySelector('#container').appendChild(preloadImages);
  556. window.MSPFA.slide.push(p => {
  557. preloadImages.innerHTML = '';
  558. if (window.MSPFA.story.p[p-2]) {
  559. let page = window.MSPFA.parseBBCode(window.MSPFA.story.p[p-2].b);
  560. page.querySelectorAll('img').forEach(image => {
  561. preloadImages.appendChild(image);
  562. });
  563. page.innerHTML = '';
  564. }
  565. if (window.MSPFA.story.p[p]) {
  566. let page = window.MSPFA.parseBBCode(window.MSPFA.story.p[p].b);
  567. page.querySelectorAll('img').forEach(image => {
  568. preloadImages.appendChild(image);
  569. });
  570. page.innerHTML = '';
  571. }
  572. });
  573. }
  574.  
  575. // Automatic spoiler opening
  576. if (settings.autospoiler) {
  577. window.MSPFA.slide.push((p) => {
  578. document.querySelectorAll('#slide .spoiler:not(.open) > div:first-child > input').forEach(sb => sb.click());
  579. });
  580. }
  581.  
  582. // Scroll up to the nav bar when changing page so you don't have to scroll down as much =)
  583. if (settings.commandScroll) {
  584. const heightTop = document.querySelector('nav').getBoundingClientRect().top - document.body.getBoundingClientRect().top;
  585. let temp = -2; // To prevent moving the page down when loading it for the first time
  586. window.MSPFA.slide.push((p) => {
  587. if (temp < 0) {
  588. temp++;
  589. } else {
  590. window.scroll(0, heightTop);
  591. }
  592. });
  593. }
  594.  
  595. // Show creation date
  596. pageLoad(() => {
  597. let infoTd = document.querySelector('#infobox tr td:nth-child(2)');
  598. if (infoTd) {
  599. infoTd.appendChild(document.createTextNode('Creation date: ' + new Date(window.MSPFA.story.d).toString().split(' ').splice(1, 3).join(' ')));
  600. infoTd.appendChild(newBr());
  601. infoTd.appendChild(document.createTextNode('Last update: ' + new Date(window.MSPFA.story.u).toString().split(' ').splice(1, 3).join(' ')));
  602. return true;
  603. }
  604. });
  605.  
  606. // Hash scrolling and opening infobox or commmentbox
  607. if (['#infobox', '#commentbox', '#newcomment', '#latestpages'].indexOf(hashSearch) !== -1) {
  608. pageLoad(() => {
  609. if (document.querySelector(hashSearch)) {
  610. if (hashSearch === '#infobox') {
  611. document.querySelector('input[data-open="Show Adventure Info"]').click();
  612. } else if (hashSearch === '#commentbox' || hashSearch === '#newcomment') {
  613. document.querySelector('input[data-open="Show Comments"]').click();
  614. } else if (hashSearch === '#latestpages') {
  615. document.querySelector('input[data-open="Show Adventure Info"]').click();
  616. document.querySelector('input[data-open="Show Latest Pages"]').click();
  617. }
  618. return true;
  619. }
  620. });
  621. }
  622.  
  623. // Attempt to fix text errors
  624. if (settings.textFix && location.pathname !== "/preview/") {
  625. pageLoad(() => {
  626. if (window.MSPFA.story && window.MSPFA.story.p) {
  627. // russian/bulgarian is not possible =(
  628. const currentPage = parseInt(/^\?s(?:.*?)&p=([\d]*)$/.exec(location.search)[1]);
  629. const library = [
  630. ["&acirc;��", "'"],
  631. ["&Atilde;�", "Ñ"],
  632. ["&Atilde;&plusmn;", "ñ"],
  633. ["&Atilde;&sup3;", "ó"],
  634. ["&Atilde;&iexcl;", "á"],
  635. ["&Auml;�", "ą"],
  636. ["&Atilde;&shy;", "í"],
  637. ["&Atilde;&ordm;", "ú"],
  638. ["&Atilde;&copy;", "é"],
  639. ["&Aring;�", "ł"],
  640. ["&Aring;&frac14;", "ż"],
  641. ["&Acirc;&iexcl;", "¡"],
  642. ["&Acirc;&iquest;", "¿"],
  643. ["N&Acirc;&ordm;", "#"]
  644. ];
  645. // https://mspfa.com/?s=5280&p=51 -- unknown error
  646.  
  647. const replaceTerms = (p) => {
  648. library.forEach(term => {
  649. if (window.MSPFA.story.p[p]) {
  650. window.MSPFA.story.p[p].c = window.MSPFA.story.p[p].c.replace(new RegExp(term[0], 'g'), term[1]);
  651. window.MSPFA.story.p[p].b = window.MSPFA.story.p[p].b.replace(new RegExp(term[0], 'g'), term[1]);
  652. }
  653. });
  654. };
  655.  
  656. replaceTerms(currentPage-1);
  657.  
  658. window.MSPFA.slide.push(p => {
  659. replaceTerms(p);
  660. replaceTerms(p-2);
  661. });
  662. return true;
  663. }
  664. });
  665. }
  666.  
  667. // Turn buttons into links
  668. const pageButton = document.createElement('button');
  669. const pageLink = addLink(pageButton, `/my/stories/pages/?s=${params.s}#p${params.p}`);
  670. pageButton.className = 'pages edit major';
  671. pageButton.type = 'button';
  672. pageButton.title = 'Edit Pages';
  673.  
  674. // Edit pages button & button link
  675. pageLoad(() => {
  676. const infoButton = document.querySelector('.edit.major');
  677. if (infoButton) {
  678. pageLoad(() => {
  679. if (window.MSPFA.me.i) {
  680. infoButton.title = "Edit Info";
  681. infoButton.parentNode.insertBefore(pageLink, infoButton);
  682. infoButton.parentNode.insertBefore(document.createTextNode(' '), infoButton);
  683. addLink(infoButton, `/my/stories/info/?s=${params.s}`);
  684. pageButton.style.display = document.querySelector('.edit.major:not(.pages)').style.display;
  685.  
  686. // Change change page link when switching pages
  687. window.MSPFA.slide.push(p => {
  688. const newSearch = location.search.split('&p=');
  689. pageLink.href = `/my/stories/pages/?s=${params.s}#p${newSearch[1].split('#')[0]}`;
  690. });
  691. return true;
  692. }
  693. });
  694. addLink(document.querySelector('.rss.major'), `/rss/?s=${params.s}`);
  695. return true;
  696. }
  697. });
  698.  
  699. // Add "Reply" button next to comment gear
  700. setInterval(() => {
  701. if (document.querySelector('#commentbox > .spoiler.open')) {
  702. document.querySelectorAll('.gear').forEach(gear => {
  703. if (!gear.parentNode.querySelector('.reply')) {
  704. const replyDiv = document.createElement('div');
  705. replyDiv.className = 'reply';
  706. gear.insertAdjacentElement('afterEnd', replyDiv);
  707. gear.insertAdjacentHTML('afterEnd', '<span style="float: right"> </span>');
  708. const userID = gear.parentNode.parentNode.classList[2].replace('u', '');
  709.  
  710. replyDiv.addEventListener('click', () => {
  711. const commentBox = document.querySelector('#commentbox textarea');
  712. commentBox.value = `[user]${userID}[/user], ${commentBox.value}`;
  713. commentBox.focus();
  714. commentBox.parentNode.scrollIntoView();
  715. });
  716. }
  717. });
  718. }
  719. }, 500);
  720. }
  721. }
  722. else if (location.pathname === "/my/") {
  723. const editStories = document.querySelector('#editstories');
  724. editStories.classList.remove('alt');
  725. const parent = editStories.parentNode;
  726. const viewSaves = document.createElement('a');
  727. Object.assign(viewSaves, { id: 'viewsaves', className: 'major', textContent: 'View Adventure Saves' });
  728.  
  729. parent.appendChild(viewSaves);
  730. parent.appendChild(newBr());
  731. parent.appendChild(newBr());
  732.  
  733. pageLoad(() => {
  734. if (window.MSPFA && window.MSPFA.me && window.MSPFA.me.i) {
  735. viewSaves.href = `/?s=36596&p=6`;
  736. return true;
  737. }
  738. },.1);
  739.  
  740. const messages = document.querySelector('#messages');
  741.  
  742. pageLoad(() => {
  743. if (messages.textContent.includes('(')) {
  744. document.title = document.title + messages.textContent.toLowerCase().replace('messages', '');
  745. return true;
  746. }
  747. });
  748. }
  749. else if (location.pathname === "/my/settings/") { // Custom settings
  750. const saveBtn = document.querySelector('#savesettings');
  751.  
  752. const table = document.querySelector("#editsettings tbody");
  753. let saveTr = table.querySelectorAll("tr");
  754. saveTr = saveTr[saveTr.length - 1];
  755.  
  756. const headerTr = document.createElement('tr');
  757. const header = document.createElement('th');
  758. Object.assign(header, { id: 'extraSettings', textContent: 'Extra Settings' });
  759. headerTr.appendChild(header);
  760.  
  761. const moreTr = document.createElement('tr');
  762. const more = document.createElement('td');
  763. 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.";
  764. moreTr.appendChild(more);
  765.  
  766. const settingsTr = document.createElement('tr');
  767. const localMsg = document.createElement('span');
  768. const settingsTd = document.createElement('td');
  769. 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!";
  770. const plusTable = document.createElement('table');
  771. const plusTbody = document.createElement('tbody');
  772. plusTable.appendChild(plusTbody);
  773. settingsTd.appendChild(localMsg);
  774. settingsTd.appendChild(newBr());
  775. settingsTd.appendChild(newBr());
  776. settingsTd.appendChild(plusTable);
  777. settingsTr.appendChild(settingsTd);
  778.  
  779. plusTable.style = "text-align: center;";
  780.  
  781. // Create checkbox (soooo much better)
  782. const createCheckbox = (text, checked, id) => {
  783. const optionTr = plusTbody.insertRow(plusTbody.childNodes.length);
  784. const optionTextTd = optionTr.insertCell(0);
  785. const optionLabel = createLabel(text, id);
  786. const optionInputTd = optionTr.insertCell(1);
  787. const optionInput = document.createElement('input');
  788. optionInputTd.appendChild(optionInput);
  789.  
  790. optionTextTd.appendChild(optionLabel);
  791. optionInput.type = "checkbox";
  792. optionInput.checked = checked;
  793. optionInput.id = id;
  794.  
  795. return optionInput;
  796. }
  797.  
  798. const spoilerInput = createCheckbox("Automatically open spoilers:", settings.autospoiler, 'autospoiler');
  799. const preloadInput = createCheckbox("Preload images for the pages immediately before and after:", settings.preload, 'preload');
  800. const errorInput = createCheckbox("Automatically reload Cloudflare 502 error pages:", settings.auto502, 'auto502');
  801. const commandScrollInput = createCheckbox("Scroll back up to the nav bar when switching page:", settings.commandScroll, 'commandScroll');
  802. const dialogKeysInput = createCheckbox("Use enter/escape keys to accept/exit control some dialogs:", settings.dialogKeys, 'dialogKeys');
  803. const dialogFocusInput = createCheckbox("Let keys work while dialog isn't focused (above required):", settings.dialogFocus, 'dialogFocus');
  804. const pixelFixInput = createCheckbox("Change pixel scaling to nearest neighbour:", settings.pixelFix, 'pixelFix');
  805. const textFixInput = createCheckbox("Attempt to fix text errors (experimental)*:", settings.textFix, 'textFix');
  806.  
  807. const cssTr = plusTbody.insertRow(plusTbody.childNodes.length);
  808. const cssTextTd = cssTr.insertCell(0);
  809. const cssSelectTd = cssTr.insertCell(1);
  810. const cssSelect = document.createElement('select');
  811. cssSelectTd.appendChild(cssSelect);
  812.  
  813. cssTextTd.textContent = "Change style:";
  814.  
  815. const customTr = plusTbody.insertRow(plusTbody.childNodes.length);
  816. const customTextTd = customTr.insertCell(0);
  817. const customCssTd = customTr.insertCell(1);
  818. const customCssInput = document.createElement('input');
  819. customCssTd.appendChild(customCssInput);
  820.  
  821. customTextTd.textContent = "Custom CSS URL:";
  822. customCssInput.style.width = "99px";
  823. customCssInput.value = settings.styleURL;
  824.  
  825. styleOptions.forEach(o => cssSelect.appendChild(new Option(o, o)));
  826.  
  827. saveTr.parentNode.insertBefore(headerTr, saveTr);
  828. saveTr.parentNode.insertBefore(settingsTr, saveTr);
  829. saveTr.parentNode.insertBefore(moreTr, saveTr);
  830. cssSelect.selectedIndex = settings.style;
  831.  
  832. const buttonSpan = document.createElement('span');
  833. const draftButton = document.createElement('input');
  834. const spoilerButton = document.createElement('input');
  835. draftButton.value = 'Manage Drafts';
  836. draftButton.className = 'major';
  837. draftButton.type = 'button';
  838. spoilerButton.value = 'Manage Spoiler Values';
  839. spoilerButton.className = 'major';
  840. spoilerButton.type = 'button';
  841. buttonSpan.appendChild(draftButton);
  842. buttonSpan.appendChild(document.createTextNode(' '));
  843. buttonSpan.appendChild(spoilerButton);
  844. settingsTd.appendChild(buttonSpan);
  845.  
  846. const draftMsg = window.MSPFA.parseBBCode('Here you can manage the drafts that you have saved for your adventure(s).\n');
  847. const listTable = document.createElement('table');
  848. listTable.id = 'draft-table';
  849. const listTbody = document.createElement('tbody');
  850. listTable.appendChild(listTbody);
  851.  
  852. const draftsEmpty = () => {
  853. loadDrafts();
  854. let empty = true;
  855. Object.keys(drafts).forEach(adv => {
  856. if (empty) {
  857. const length = typeof drafts[adv].cachedTitle === "undefined" ? 0 : 1;
  858. if (Object.keys(drafts[adv]).length > length) {
  859. empty = false;
  860. }
  861. }
  862. });
  863. return empty;
  864. }
  865.  
  866. setInterval(() => {
  867. draftButton.disabled = draftsEmpty();
  868. }, 1000);
  869.  
  870. draftButton.addEventListener('click', () => {
  871. draftMsg.appendChild(listTable);
  872. listTbody.innerHTML = '';
  873. loadDrafts();
  874.  
  875. const addAdv = (story, name) => {
  876. const storyTr = listTbody.insertRow(listTable.rows);
  877. const titleLink = document.createElement('a');
  878. Object.assign(titleLink, { className: 'major', href: `/my/stories/pages/?s=${story}&click=d`, textContent: name, 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 draft?', document.createTextNode('Are you really sure?\nThis action cannot be undone!'), ["Yes", "No"], (output, form) => {
  887. if (output === "Yes") {
  888. loadDrafts();
  889. drafts[story] = {};
  890.  
  891. if (settings.drafts && settings.drafts[story]) {
  892. delete settings.drafts[story];
  893. saveData(settings);
  894. }
  895.  
  896. saveDrafts(drafts);
  897.  
  898. setTimeout(() => {
  899. draftButton.click();
  900. }, 1);
  901.  
  902. if (draftsEmpty) {
  903. draftButton.disabled = true;
  904. }
  905. }
  906. });
  907. }, 1);
  908. });
  909. }
  910.  
  911. Object.keys(drafts).forEach(adv => {
  912. const length = typeof drafts[adv].cachedTitle === "undefined" ? 0 : 1;
  913. if (Object.keys(drafts[adv]).length > length) {
  914. if (!!length) {
  915. addAdv(adv, drafts[adv].cachedTitle);
  916. }
  917. else {
  918. window.MSPFA.request(0, {
  919. do: "story",
  920. s: adv
  921. }, story => {
  922. if (typeof story !== "undefined") {
  923. console.log(story);
  924. addAdv(adv, story.n);
  925. }
  926. });
  927. }
  928. }
  929. });
  930.  
  931. window.MSPFA.dialog('Manage Drafts', draftMsg, ["Delete All", "Close"], (output, form) => {
  932. if (output === "Delete All") {
  933. setTimeout(() => {
  934. window.MSPFA.dialog('Delete all Drafts?', document.createTextNode('Are you really sure?\nThis action cannot be undone!'), ["Yes", "No"], (output, form) => {
  935. if (output === "Yes") {
  936. Object.keys(drafts).forEach(adv => {
  937. drafts[adv] = {};
  938. });
  939. saveDrafts(drafts);
  940.  
  941. if (typeof settings.drafts !== "undefined") {
  942. delete settings.drafts;
  943. saveData(settings);
  944. }
  945.  
  946. draftButton.disabled = true;
  947. }
  948. });
  949. }, 1);
  950. }
  951. });
  952. });
  953.  
  954. if (Object.keys(settings.spoilerValues).length === 0) {
  955. spoilerButton.disabled = true;
  956. }
  957.  
  958. 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');
  959.  
  960. spoilerButton.addEventListener('click', () => {
  961. spoilerMsg.appendChild(listTable);
  962. listTbody.innerHTML = '';
  963. Object.keys(settings.spoilerValues).forEach(adv => {
  964. window.MSPFA.request(0, {
  965. do: "story",
  966. s: adv
  967. }, story => {
  968. if (typeof story !== "undefined") {
  969. const storyTr = listTbody.insertRow(listTable.rows);
  970. const titleLink = document.createElement('a');
  971. Object.assign(titleLink, { className: 'major', href: `/my/stories/pages/?s=${adv}&click=s`, textContent: story.n, target: '_blank' });
  972. storyTr.insertCell(0).appendChild(titleLink);
  973. const deleteButton = document.createElement('input');
  974. Object.assign(deleteButton, { className: 'major', type: 'button', value: 'Delete' });
  975. storyTr.insertCell(1).appendChild(deleteButton);
  976.  
  977. deleteButton.addEventListener('click', () => {
  978. setTimeout(() => {
  979. window.MSPFA.dialog('Delete adventure spoilers?', document.createTextNode('Are you really sure?\nThis action cannot be undone!'), ["Yes", "No"], (output, form) => {
  980. if (output === "Yes") {
  981. delete settings.spoilerValues[adv];
  982. saveData(settings);
  983.  
  984. setTimeout(() => {
  985. spoilerButton.click();
  986. }, 1);
  987.  
  988. if (Object.keys(settings.spoilerValues).length === 0) {
  989. spoilerButton.disabled = true;
  990. }
  991. }
  992. });
  993. }, 1);
  994. });
  995. }
  996. });
  997. });
  998. window.MSPFA.dialog('Manage Spoiler Values', spoilerMsg, ["Delete All", "Close"], (output, form) => {
  999. if (output === "Delete All") {
  1000. setTimeout(() => {
  1001. 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) => {
  1002. if (output === "Yes") {
  1003. settings.spoilerValues = {};
  1004. saveData(settings);
  1005. spoilerButton.disabled = true;
  1006. }
  1007. });
  1008. }, 1);
  1009. }
  1010. });
  1011. });
  1012.  
  1013. // Add event listeners
  1014. plusTbody.querySelectorAll('input, select').forEach(elm => {
  1015. elm.addEventListener("change", () => {
  1016. saveBtn.disabled = false;
  1017. });
  1018. });
  1019.  
  1020. saveBtn.addEventListener('mouseup', () => {
  1021. settings.autospoiler = spoilerInput.checked;
  1022. settings.style = cssSelect.selectedIndex;
  1023. settings.styleURL = customCssInput.value;
  1024. settings.auto502 = errorInput.checked;
  1025. settings.textFix = textFixInput.checked;
  1026. settings.pixelFix = pixelFixInput.checked;
  1027. settings.dialogKeys = dialogKeysInput.checked;
  1028. settings.dialogFocus = dialogFocusInput.checked;
  1029. settings.commandScroll = commandScrollInput.checked;
  1030. settings.preload = preloadInput.checked;
  1031. settings.night = false;
  1032. console.log(settings);
  1033. saveData(settings);
  1034.  
  1035. updateTheme(settings.style == styleOptions.length - 1 ? settings.styleURL : styleUrls[settings.style]);
  1036.  
  1037. extraStyle.textContent = pixelText() + ' *{transition:1s}';
  1038.  
  1039. extraStyle.textContent = pixelText();
  1040. setTimeout(() => {
  1041. extraStyle.textContent = pixelText();
  1042. }, 1000);
  1043. });
  1044. }
  1045. else if (location.pathname === "/my/messages/") { // New buttons
  1046. // Select all read messages button.
  1047. const selRead = document.createElement('input');
  1048. Object.assign(selRead, { value: 'Select Read', className: 'major', type: 'button' });
  1049.  
  1050. // On click, select all messages with the style attribute indicating it as read.
  1051. selRead.addEventListener('mouseup', () => {
  1052. document.querySelectorAll('td[style="border-left: 8px solid rgb(221, 221, 221);"] > input').forEach((m) => m.click());
  1053. });
  1054.  
  1055. // Select duplicate message (multiple update notifications).
  1056. const selDupe = document.createElement('input');
  1057. Object.assign(selDupe, { value: 'Select Same', className: 'major', type: 'button', style: 'margin-top: 6px' });
  1058.  
  1059. selDupe.addEventListener('mouseup', evt => {
  1060. const temp = document.querySelectorAll('#messages > tr');
  1061. const msgs = [];
  1062. for (let i = temp.length - 1; i >= 0; i--) {
  1063. msgs.push(temp[i]);
  1064. }
  1065. const titles = [];
  1066. msgs.forEach((msg) => {
  1067. const title = msg.querySelector('a.major').textContent;
  1068. // Select only adventure updates
  1069. if (/^New update: /.test(title)) {
  1070. if (titles.indexOf(title) === -1) {
  1071. if (msg.querySelector('td').style.cssText !== "border-left: 8px solid rgb(221, 221, 221);") {
  1072. titles.push(title);
  1073. }
  1074. } else {
  1075. msg.querySelector('input').click();
  1076. }
  1077. }
  1078. });
  1079. });
  1080.  
  1081. // Prune button
  1082. const pruneButton = document.createElement('input');
  1083. Object.assign(pruneButton, { type: 'button', value: 'Prune', className: 'major' });
  1084.  
  1085. pruneButton.addEventListener('click', () => {
  1086. const ageInput = document.createElement('input');
  1087. Object.assign(ageInput, { type: 'number', min: 1, max: 10, value: 1 });
  1088.  
  1089. const msgState = document.createElement('select');
  1090. ['all', 'all unread', 'all read'].forEach(option => {
  1091. const op = document.createElement('option');
  1092. op.textContent = option;
  1093. msgState.appendChild(op);
  1094. });
  1095.  
  1096. const timeUnit = document.createElement('select');
  1097. ['month(s)', 'week(s)', 'day(s)'].forEach(option => {
  1098. const op = document.createElement('option');
  1099. op.textContent = option;
  1100. timeUnit.appendChild(op);
  1101. });
  1102. timeUnit.childNodes[1].setAttribute('selected', 'selected');
  1103.  
  1104. const msg = document.createElement('span');
  1105. msg.appendChild(document.createTextNode('Prune '));
  1106. msg.appendChild(msgState);
  1107. msg.appendChild(document.createTextNode(' messages older than '));
  1108. msg.appendChild(ageInput);
  1109. msg.appendChild(timeUnit);
  1110.  
  1111. window.MSPFA.dialog('Prune messages', msg, ['Prune', 'Cancel'], (output, form) => {
  1112. if (output === 'Prune') {
  1113. document.querySelector('#messages').childNodes.forEach(node => {
  1114. if (node.firstChild.firstChild.checked) {
  1115. node.firstChild.firstChild.click();
  1116. }
  1117.  
  1118. const selectedState = msgState.selectedOptions[0].textContent;
  1119. const selectedUnit = timeUnit.selectedOptions[0].textContent;
  1120.  
  1121. if (selectedState === 'all unread') {
  1122. if (node.firstChild.style.borderLeftColor === 'rgb(221, 221, 221)') {
  1123. return;
  1124. }
  1125. }
  1126. else if (selectedState === 'all read') {
  1127. if (node.firstChild.style.borderLeftColor === 'rgb(92, 174, 223)') {
  1128. return;
  1129. }
  1130. }
  1131. const dateText = node.childNodes[2].childNodes[2].textContent.split(' - ');
  1132. const messageDate = new Date(dateText[dateText.length-1]);
  1133. const currentDate = Date.now();
  1134. const diff = Math.floor(Math.round((currentDate-messageDate)/(1000*60*60))/24); // Difference in days
  1135.  
  1136. if (selectedUnit === 'month(s)') diff = Math.floor(diff / 30);
  1137. else if (selectedUnit === 'week(s)') diff = Math.floor(diff / 7);
  1138.  
  1139. if (diff >= ageInput.value) {
  1140. node.firstChild.firstChild.click();
  1141. }
  1142. });
  1143.  
  1144. setTimeout(() => {
  1145. document.querySelector('input[value=Delete]').click();
  1146. }, 1);
  1147. }
  1148. });
  1149. });
  1150.  
  1151. // Maybe add a "Merge Updates" button?
  1152. // [Merge Updates] would create a list of updates, similar to [Select Same]
  1153.  
  1154. // Add buttons to the page.
  1155. const del = document.querySelector('#deletemsgs');
  1156. del.parentNode.appendChild(newBr());
  1157. del.parentNode.appendChild(selRead);
  1158. del.parentNode.appendChild(document.createTextNode(' '));
  1159. del.parentNode.appendChild(selDupe);
  1160. del.parentNode.appendChild(document.createTextNode(' '));
  1161. del.parentNode.appendChild(pruneButton);
  1162.  
  1163. // Click the green cube to open the update/comment in a new tab, and mark notification as read.
  1164. pageLoad(() => {
  1165. if (document.querySelector('#messages').childNodes.length > 0) {
  1166. if (document.querySelector('#messages').textContent === 'No new messages were found.') {
  1167. // Disable some buttons if there are no messages.
  1168. pruneButton.disabled = true;
  1169. selDupe.disabled = true;
  1170. return true;
  1171. } else {
  1172. document.querySelector('#messages').childNodes.forEach(node => {
  1173. if (node.textContent.includes('New update:') && node.textContent.includes('MS Paint Fan Adventures')) {
  1174. const link = addLink(node.querySelector('.cellicon'), node.querySelector('.spoiler a').href);
  1175. link.addEventListener('mouseup', () => {
  1176. const spoiler = node.querySelector('.spoiler');
  1177. const button = spoiler.querySelector('input');
  1178. spoiler.className = 'spoiler closed';
  1179. button.click();
  1180. button.click();
  1181. });
  1182. }
  1183. else if (node.textContent.includes('New comment on ') && node.textContent.includes('MS Paint Fan Adventures')) {
  1184. const link = addLink(node.querySelector('.cellicon'), node.querySelectorAll('.spoiler a')[1].href + '#commentbox');
  1185. link.addEventListener('mouseup', () => {
  1186. const spoiler = node.querySelector('.spoiler');
  1187. const button = spoiler.querySelector('input');
  1188. spoiler.className = 'spoiler closed';
  1189. button.click();
  1190. button.click();
  1191. });
  1192. }
  1193. });
  1194. return true;
  1195. }
  1196. }
  1197. });
  1198. }
  1199. else if (location.pathname === "/my/messages/new/" && location.search) { // Auto-fill user when linked from a user page
  1200. const recipientInput = document.querySelector('#addrecipient');
  1201. recipientInput.value = params.u;
  1202. pageLoad(() => {
  1203. const recipientButton = document.querySelector('#addrecipientbtn');
  1204. if (recipientButton) {
  1205. recipientButton.click();
  1206. if (recipientInput.value === "") { // If the button press doesn't work
  1207. return true;
  1208. }
  1209. }
  1210. });
  1211. }
  1212. else if (location.pathname === "/my/stories/") {
  1213. // Add links to buttons
  1214. pageLoad(() => {
  1215. const adventures = document.querySelectorAll('#stories tr');
  1216. if (adventures.length > 0) {
  1217. adventures.forEach(story => {
  1218. const buttons = story.querySelectorAll('input.major');
  1219. const id = story.querySelector('a').href.replace('https://mspfa.com/', '').replace('&p=1', '');
  1220. if (id) {
  1221. addLink(buttons[0], `/my/stories/info/${id}`);
  1222. addLink(buttons[1], `/my/stories/pages/${id}`);
  1223. addLink(story.querySelector('img'), `/${id}&p=1`);
  1224. }
  1225. });
  1226. return true;
  1227. }
  1228. if (pageLoaded) return true;
  1229. });
  1230.  
  1231. // Add user guides
  1232. const guides = ["A Guide To Uploading Your Comic To MSPFA", "MSPFA Etiquette", "Fanventure Guide for Dummies", "CSS Guide", "HTML and CSS Things", ];
  1233. const links = ["https://docs.google.com/document/d/17QI6Cv_BMbr8l06RrRzysoRjASJ-ruWioEtVZfzvBzU/edit?usp=sharing", "/?s=27631", "/?s=29299", "/?s=21099", "/?s=23711"];
  1234. const authors = ["Farfrom Tile", "Radical Dude 42", "nzar", "MadCreativity", "seymour schlong"];
  1235.  
  1236. const parentTd = document.querySelector('.container > tbody > tr:last-child > td');
  1237. const unofficial = parentTd.querySelector('span');
  1238. unofficial.textContent = "Unofficial Guides";
  1239. const guideTable = document.createElement('table');
  1240. const guideTbody = document.createElement('tbody');
  1241. guideTable.style.width = "100%";
  1242. guideTable.style.textAlign = "center";
  1243.  
  1244. guideTable.appendChild(guideTbody);
  1245. parentTd.appendChild(guideTable);
  1246.  
  1247. for (let i = 0; i < guides.length; i++) {
  1248. const guideTr = guideTbody.insertRow(i);
  1249. const guideTd = guideTr.insertCell(0);
  1250. const guideLink = document.createElement('a');
  1251. Object.assign(guideLink, { href: links[i], textContent: guides[i], className: 'major' });
  1252. guideTd.appendChild(guideLink);
  1253. guideTd.appendChild(newBr());
  1254. guideTd.appendChild(document.createTextNode('by '+authors[i]));
  1255. guideTd.appendChild(newBr());
  1256. guideTd.appendChild(newBr());
  1257. }
  1258. }
  1259. else if (location.pathname === "/my/stories/info/" && location.search) {
  1260. // Button links
  1261. addLink(document.querySelector('#userfavs'), `/readers/?s=${params.s}`);
  1262. addLink(document.querySelector('#editpages'), `/my/stories/pages/?s=${params.s}`);
  1263.  
  1264. // Download adventure data
  1265. if (params.s !== 'new') {
  1266. const downloadButton = document.createElement('input');
  1267. Object.assign(downloadButton, { className: 'major', value: 'Export Data', type: 'button', style: 'margin-top: 6px' });
  1268. const downloadLink = document.createElement('a');
  1269. window.MSPFA.request(0, {
  1270. do: "story",
  1271. s: params.s
  1272. }, (s) => {
  1273. if (s) {
  1274. downloadLink.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(JSON.stringify(s, null, 4)));
  1275. }
  1276. });
  1277. downloadLink.setAttribute('download', `${params.s}.json`);
  1278. downloadLink.appendChild(downloadButton);
  1279. document.querySelector('#savestory').parentNode.appendChild(newBr());
  1280. document.querySelector('#savestory').parentNode.appendChild(downloadLink);
  1281. }
  1282. }
  1283. else if (location.pathname === "/my/stories/pages/" && location.search) {
  1284. const adventureID = params.s;
  1285.  
  1286. const notifyLabel = createLabel('Notify readers of new pages during this editing session: ', 'notifyreaders');
  1287. const notifyButton = document.querySelector('#notifyreaders');
  1288. notifyButton.previousSibling.textContent = '';
  1289. notifyButton.parentNode.insertBefore(notifyLabel, notifyButton);/**/
  1290.  
  1291. if (!drafts[adventureID]) {
  1292. drafts[adventureID] = {};
  1293. saveDrafts(drafts);
  1294. }
  1295.  
  1296. pageLoad(() => {
  1297. if (document.querySelector('#storyname').textContent !== '-') {
  1298. drafts[adventureID].cachedTitle = document.querySelector('#storyname').textContent;
  1299. saveDrafts(drafts);
  1300. return true;
  1301. }
  1302. });
  1303.  
  1304. // Button links
  1305. addLink(document.querySelector('#editinfo'), `/my/stories/info/?s=${adventureID}`);
  1306.  
  1307. // Default spoiler values
  1308. const replaceButton = document.querySelector('#replaceall');
  1309. const spoilerButton = document.createElement('input');
  1310. Object.assign(spoilerButton, { className: 'major', value: 'Default Spoiler Values', type: 'button'});
  1311. replaceButton.parentNode.insertBefore(spoilerButton, replaceButton);
  1312. replaceButton.parentNode.insertBefore(newBr(), replaceButton);
  1313. replaceButton.parentNode.insertBefore(newBr(), replaceButton);
  1314.  
  1315. if (!settings.spoilerValues[adventureID]) {
  1316. settings.spoilerValues[adventureID] = {
  1317. open: 'Show',
  1318. close: 'Hide'
  1319. }
  1320. }
  1321.  
  1322. spoilerButton.addEventListener('click', evt => {
  1323. const spoilerSpan = document.createElement('span');
  1324. const spoilerOpen = document.createElement('input');
  1325. const spoilerClose = document.createElement('input');
  1326. spoilerSpan.appendChild(document.createTextNode('Open button text:'));
  1327. spoilerSpan.appendChild(newBr());
  1328. spoilerSpan.appendChild(spoilerOpen);
  1329. spoilerSpan.appendChild(newBr());
  1330. spoilerSpan.appendChild(newBr());
  1331. spoilerSpan.appendChild(document.createTextNode('Close button text:'));
  1332. spoilerSpan.appendChild(newBr());
  1333. spoilerSpan.appendChild(spoilerClose);
  1334.  
  1335. spoilerOpen.value = settings.spoilerValues[adventureID].open;
  1336. spoilerClose.value = settings.spoilerValues[adventureID].close;
  1337.  
  1338. window.MSPFA.dialog('Default Spoiler Values', spoilerSpan, ['Save', 'Cancel'], (output, form) => {
  1339. if (output === 'Save') {
  1340. settings.spoilerValues[adventureID].open = spoilerOpen.value === '' ? 'Show' : spoilerOpen.value;
  1341. settings.spoilerValues[adventureID].close = spoilerClose.value === '' ? 'Hide' : spoilerClose.value;
  1342. if (settings.spoilerValues[adventureID].open === 'Show' && settings.spoilerValues[adventureID].close === 'Hide') {
  1343. delete settings.spoilerValues[adventureID];
  1344. }
  1345. saveData(settings);
  1346. }
  1347. });
  1348. });
  1349.  
  1350. document.querySelector('input[title="Spoiler"]').addEventListener('click', evt => {
  1351. document.querySelector('#dialog input[name="open"]').value = document.querySelector('#dialog input[name="open"]').placeholder = settings.spoilerValues[adventureID].open;
  1352. document.querySelector('#dialog input[name="close"]').value = document.querySelector('#dialog input[name="close"]').placeholder = settings.spoilerValues[adventureID].close;
  1353. });
  1354.  
  1355. // --- Custom BBToolbar buttons
  1356. // Buttonless spoilers
  1357. const flashButton = document.querySelector('input[title=Flash]');
  1358. const newSpoilerButton = document.createElement('input');
  1359. newSpoilerButton.setAttribute('data-tag', 'Buttonless Spoiler');
  1360. Object.assign(newSpoilerButton, { title: 'Buttonless Spoiler', type: 'button', style: 'background-position: -66px -88px;' });
  1361.  
  1362. newSpoilerButton.addEventListener('click', evt => {
  1363. const bbe = document.querySelector('#bbtoolbar').parentNode.querySelector('textarea');
  1364. if (bbe) {
  1365. bbe.focus();
  1366. const start = bbe.selectionStart;
  1367. const end = bbe.selectionEnd;
  1368. bbe.value = bbe.value.slice(0, start) + '<div class="spoiler"><div>' + bbe.value.slice(start, end) + '</div></div>' + bbe.value.slice(end);
  1369. bbe.selectionStart = start + 26;
  1370. bbe.selectionEnd = end + 26;
  1371. }
  1372. });
  1373.  
  1374. flashButton.parentNode.insertBefore(newSpoilerButton, flashButton);
  1375.  
  1376. // Audio button
  1377. const audioButton = document.createElement('input');
  1378. Object.assign(audioButton, { title: 'Audio Player', type: 'button', style: 'background-position: -22px -110px' });
  1379.  
  1380. audioButton.addEventListener('click', evt => {
  1381. const bbe = document.querySelector('#bbtoolbar').parentNode.querySelector('textarea');
  1382. if (bbe) {
  1383. const msg = window.MSPFA.parseBBCode('Audio URL:<br>');
  1384. const audioInput = document.createElement('input');
  1385. Object.assign(audioInput, { type: 'url', name: 'audio-url', required: true });
  1386.  
  1387. const autoplayButton = document.createElement('input');
  1388. autoplayButton.type = 'checkbox';
  1389. autoplayButton.id = 'autoplay';
  1390. autoplayButton.checked = true;
  1391.  
  1392. const loopButton = document.createElement('input');
  1393. loopButton.type = 'checkbox';
  1394. loopButton.id = 'loop';
  1395. loopButton.checked = true;
  1396.  
  1397. const controlsButton = document.createElement('input');
  1398. controlsButton.type = 'checkbox';
  1399. controlsButton.id = 'controls';
  1400.  
  1401. msg.appendChild(audioInput);
  1402. msg.appendChild(newBr());
  1403. msg.appendChild(createLabel('Autoplay: ', 'autoplay'));
  1404. msg.appendChild(autoplayButton);
  1405. msg.appendChild(newBr());
  1406. msg.appendChild(createLabel('Loop: ', 'loop'));
  1407. msg.appendChild(loopButton);
  1408. msg.appendChild(newBr());
  1409. msg.appendChild(createLabel('Show controls: ', 'controls'));
  1410. msg.appendChild(controlsButton);
  1411. msg.appendChild(newBr());
  1412.  
  1413. window.MSPFA.dialog("Audio Player", msg, ["Okay", "Cancel"], (output, form) => {
  1414. if (output == "Okay") {
  1415. bbe.focus();
  1416. const start = bbe.selectionStart;
  1417. const end = bbe.selectionEnd;
  1418. const properties = `"${autoplayButton.checked ? ' autoplay' : ''}${loopButton.checked ? ' loop' : ''}${controlsButton.checked ? ' controls' : ''}`;
  1419. bbe.value = bbe.value.slice(0, start) + '<audio src="' + audioInput.value + properties +'>' + bbe.value.slice(start);
  1420. bbe.selectionStart = start + properties.length + audioInput.value.length + 13;
  1421. bbe.selectionEnd = end + properties.length + audioInput.value.length + 13;
  1422. }
  1423.  
  1424. });
  1425.  
  1426. audioInput.select();
  1427. }
  1428. });
  1429.  
  1430. flashButton.insertAdjacentElement('afterEnd', audioButton);
  1431.  
  1432. // YouTube button
  1433. const youtubeButton = document.createElement('input');
  1434. Object.assign(youtubeButton, { title: 'YouTube Video', type: 'button', style: 'background-position: 0px -110px' });
  1435.  
  1436. youtubeButton.addEventListener('click', evt => {
  1437. const bbe = document.querySelector('#bbtoolbar').parentNode.querySelector('textarea');
  1438. if (bbe) {
  1439. const msg = window.MSPFA.parseBBCode('Video URL:<br>');
  1440. const videoUrl = document.createElement('input');
  1441. videoUrl.type = 'url';
  1442. videoUrl.name = 'youtube';
  1443. videoUrl.required = true;
  1444.  
  1445. const autoplayButton = document.createElement('input');
  1446. autoplayButton.type = 'checkbox';
  1447. autoplayButton.checked = true;
  1448. autoplayButton.id = 'autoplay';
  1449.  
  1450. const controlsButton = document.createElement('input');
  1451. controlsButton.type = 'checkbox';
  1452. controlsButton.checked = true;
  1453. controlsButton.id = 'controls';
  1454.  
  1455. const fullscreenButton = document.createElement('input');
  1456. fullscreenButton.type = 'checkbox';
  1457. fullscreenButton.checked = true;
  1458. fullscreenButton.id = 'fullscreen';
  1459.  
  1460. const widthInput = document.createElement('input');
  1461. Object.assign(widthInput, { type: 'number', required: true, value: 650, style: 'width: 5em' });
  1462.  
  1463. const heightInput = document.createElement('input');
  1464. Object.assign(heightInput, { type: 'number', required: true, value: 450, style: 'width: 5em' });
  1465.  
  1466. msg.appendChild(videoUrl);
  1467. msg.appendChild(newBr());
  1468. msg.appendChild(createLabel('Autoplay: ', 'autoplay'));
  1469. msg.appendChild(autoplayButton);
  1470. msg.appendChild(newBr());
  1471. msg.appendChild(createLabel('Show controls: ', 'controls'));
  1472. msg.appendChild(controlsButton);
  1473. msg.appendChild(newBr());
  1474. msg.appendChild(createLabel('Allow fullscreen: ', 'fullscreen'));
  1475. msg.appendChild(fullscreenButton);
  1476. msg.appendChild(newBr());
  1477. msg.appendChild(document.createTextNode('Embed size: '));
  1478. msg.appendChild(widthInput);
  1479. msg.appendChild(document.createTextNode('x'));
  1480. msg.appendChild(heightInput);
  1481.  
  1482. window.MSPFA.dialog("YouTube Embed", msg, ["Okay", "Cancel"], (output, form) => {
  1483. if (output == "Okay") {
  1484. let videoID = videoUrl.value.split('/');
  1485. videoID = videoID[videoID.length-1].replace('watch?v=', '').split('&')[0];
  1486.  
  1487. bbe.focus();
  1488. const start = bbe.selectionStart;
  1489. const end = bbe.selectionEnd;
  1490. const iframeContent = `<iframe width="${widthInput.value}" height="${heightInput.value}" src="https://www.youtube.com/embed/${videoID}?autoplay=${+autoplayButton.checked}&controls=${+controlsButton.checked}" frameborder="0" allow="accelerometer; ${autoplayButton.checked ? 'autoplay; ' : ''}encrypted-media;"${fullscreenButton.checked ? ' allowfullscreen' : ''}></iframe>`;
  1491. bbe.value = bbe.value.slice(0, start) + iframeContent + bbe.value.slice(start);
  1492. bbe.selectionStart = start + iframeContent + 13;
  1493. bbe.selectionEnd = end + iframeContent + 13;
  1494. }
  1495.  
  1496. });
  1497.  
  1498. videoUrl.select();
  1499. }
  1500. });
  1501.  
  1502. flashButton.insertAdjacentElement('afterEnd', youtubeButton);
  1503. flashButton.insertAdjacentText('afterEnd', ' ');
  1504.  
  1505. // Get preview link
  1506. const getPreviewLink = (form) => {
  1507. const page = parseInt(form.querySelector('a.major').textContent.replace('Page ', ''));
  1508. return "/preview/?s=" + params.s + "&p=" + page + "&d=" + encodeURIComponent(JSON.stringify({
  1509. p: page,
  1510. c: form.querySelector('input[name=cmd]').value,
  1511. b: form.querySelector('textarea[name=body]').value,
  1512. n: form.querySelector('input[name=next]').value,
  1513. k: !form.querySelector('input[name=usekeys]').checked
  1514. }));
  1515. }
  1516.  
  1517. // -- Drafts --
  1518. // Accessing draft text
  1519. const accessDraftsButton = document.createElement('input');
  1520. Object.assign(accessDraftsButton, { className: 'major', value: 'Saved Drafts', type: 'button' });
  1521. replaceButton.parentNode.insertBefore(accessDraftsButton, replaceButton);
  1522. accessDraftsButton.parentNode.insertBefore(document.createTextNode(' '), replaceButton);
  1523. //accessDraftsButton.parentNode.insertBefore(newBr(), replaceButton);
  1524. //accessDraftsButton.parentNode.insertBefore(newBr(), replaceButton);
  1525.  
  1526. accessDraftsButton.addEventListener('click', () => {
  1527. loadDrafts();
  1528.  
  1529. const draftDialog = window.MSPFA.parseBBCode('Use the textbox below to copy out the data and save to a file somewhere else, or click the download button below.\nYou can also paste in data to replace the current drafts to ones stored there.');
  1530. const draftInputTextarea = document.createElement('textarea');
  1531. draftInputTextarea.placeholder = 'Paste your draft data here';
  1532. draftInputTextarea.style = 'width: 100%; box-sizing: border-box; resize: vertical;';
  1533.  
  1534. const downloadLink = document.createElement('a');
  1535. downloadLink.textContent = 'Download drafts';
  1536. downloadLink.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(JSON.stringify(drafts[adventureID], null, 4)));
  1537. downloadLink.setAttribute('download', `${adventureID}.json`);
  1538.  
  1539. draftInputTextarea.rows = 8;
  1540. draftDialog.appendChild(newBr());
  1541. draftDialog.appendChild(newBr());
  1542. draftDialog.appendChild(draftInputTextarea);
  1543. draftDialog.appendChild(newBr());
  1544. draftDialog.appendChild(newBr());
  1545. draftDialog.appendChild(downloadLink);
  1546. setTimeout(() => {
  1547. draftInputTextarea.focus();
  1548. draftInputTextarea.selectionStart = 0;
  1549. draftInputTextarea.selectionEnd = 0;
  1550. draftInputTextarea.scrollTop = 0;
  1551. }, 1);
  1552.  
  1553. draftInputTextarea.value = JSON.stringify(drafts[adventureID], null, 4);
  1554.  
  1555. window.MSPFA.dialog('Saved Drafts', draftDialog, ["Load Draft", "Cancel"], (output, form) => {
  1556. if (output === "Load Draft") {
  1557. if (draftInputTextarea.value === '') {
  1558. setTimeout(() => {
  1559. 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) => {
  1560. if (output === "Delete") {
  1561. loadDrafts();
  1562. drafts[adventureID] = {};
  1563.  
  1564. if (settings.drafts && settings.drafts[adventureID]) {
  1565. delete settings.drafts[adventureID];
  1566. saveData(settings);
  1567. }
  1568.  
  1569. saveDrafts(drafts);
  1570. }
  1571. });
  1572. }, 1);
  1573. } else if (draftInputTextarea.value !== JSON.stringify(drafts[adventureID], null, 4)) {
  1574. setTimeout(() => {
  1575. 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) => {
  1576. if (output === "Load") {
  1577. let newData = {};
  1578. try { // Just in case the data given is invalid.
  1579. newData = JSON.parse(draftInputTextarea.value);
  1580. } catch (err) {
  1581. console.error(err);
  1582. setTimeout(() => {
  1583. window.MSPFA.dialog('Error', window.MSPFA.parseBBCode('The entered data is invalid.'), ["Okay"]);
  1584. }, 1);
  1585. return;
  1586. }
  1587.  
  1588. loadDrafts();
  1589. drafts[adventureID] = newData;
  1590. saveDrafts(drafts);
  1591. }
  1592. });
  1593. }, 1);
  1594. }
  1595. }
  1596. });
  1597. });
  1598.  
  1599. const addDraftPagesButton = document.createElement('input');
  1600. replaceButton.parentNode.insertBefore(addDraftPagesButton, replaceButton);
  1601. addDraftPagesButton.parentNode.insertBefore(newBr(), replaceButton);
  1602. addDraftPagesButton.parentNode.insertBefore(newBr(), replaceButton);
  1603.  
  1604. Object.assign(addDraftPagesButton, { className: 'major', value: 'Add Draft Pages', type: 'button' });
  1605. addDraftPagesButton.addEventListener('click', () => {
  1606. const newPageForm = document.querySelector('#newpage');
  1607. let currentPage = 1;
  1608. let pageAmount = 0;
  1609. if (newPageForm.nextSibling.nodeName !== '#text') {
  1610. currentPage = parseInt(newPageForm.nextSibling.id.replace('p', ''))+1;
  1611. }
  1612. for (let i = currentPage; drafts[adventureID][i]; i++) {
  1613. pageAmount++;
  1614. }
  1615.  
  1616. window.MSPFA.dialog("Drafts: Add New Pages", document.createTextNode(`This will add the pages with draft data from pages ${currentPage}-${currentPage+pageAmount-1}`), ["Okay", "Cancel"], (output, form) => {
  1617. if (output === "Okay") {
  1618. for (let i = currentPage; drafts[adventureID][i]; i++) {
  1619. newPageForm.querySelector('input[name=cmd]').value = drafts[adventureID][i].command;
  1620. newPageForm.querySelector('textarea[name=body]').value = drafts[adventureID][i].pageContent;
  1621. if (drafts[adventureID][i].next) newPageForm.querySelector('input[name=next]').value = drafts[adventureID][i].next;
  1622.  
  1623. newPageForm.querySelector('input[name=save]').click();
  1624. }
  1625. }
  1626. });
  1627. });
  1628.  
  1629. // Draft stuff
  1630. const showDraftDialog = (pageNum) => {
  1631. loadDrafts();
  1632.  
  1633. const msg = document.createElement('span');
  1634. msg.appendChild(document.createTextNode('Command:'));
  1635. msg.appendChild(document.createElement('br'));
  1636.  
  1637. const commandInput = document.createElement('input');
  1638. Object.assign(commandInput, { style: 'width: 100%; box-sizing: border-box;', readOnly: true, });
  1639.  
  1640. msg.appendChild(commandInput);
  1641. msg.appendChild(document.createElement('br'));
  1642. msg.appendChild(document.createElement('br'));
  1643.  
  1644. msg.appendChild(document.createTextNode('Body:'));
  1645.  
  1646. const bodyInput = document.createElement('textarea');
  1647. Object.assign(bodyInput, { style: 'width: 100%; box-sizing: border-box; resize: vertical;', readOnly: true, rows: 8 });
  1648.  
  1649. msg.appendChild(bodyInput);
  1650.  
  1651. const pageElement = document.querySelector(`#p${pageNum}`);
  1652.  
  1653. let shownMessage = msg;
  1654. let optionButtons = [];
  1655.  
  1656. const commandElement = pageElement.querySelector('input[name="cmd"]');
  1657. const pageContentElement = pageElement.querySelector('textarea[name="body"]');
  1658.  
  1659. if (typeof drafts[adventureID][pageNum] === "undefined") {
  1660. shownMessage = document.createTextNode('There is no draft saved for this page.');
  1661. optionButtons = ["Save New", "Close"];
  1662. } else {
  1663. commandInput.value = drafts[adventureID][pageNum].command;
  1664. bodyInput.textContent = drafts[adventureID][pageNum].pageContent;
  1665. optionButtons = ["Save New", "Load", "Delete", "Close"];
  1666. }
  1667.  
  1668. window.MSPFA.dialog(`Page ${pageNum} Draft`, shownMessage, optionButtons, (output, form) => {
  1669. if (output === "Save New") {
  1670. if (typeof drafts[adventureID][pageNum] === "undefined") {
  1671. loadDrafts();
  1672. drafts[adventureID][pageNum] = {
  1673. command: commandElement.value,
  1674. pageContent: pageContentElement.value
  1675. }
  1676. saveDrafts(drafts);
  1677. } else {
  1678. setTimeout(() => {
  1679. 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) => {
  1680. if (output === "Yes") {
  1681. loadDrafts();
  1682. drafts[adventureID][pageNum] = {
  1683. command: commandElement.value,
  1684. pageContent: pageContentElement.value
  1685. }
  1686. saveDrafts(drafts);
  1687. }
  1688. });
  1689. }, 1);
  1690. }
  1691. } else if (output === "Load") {
  1692. if (pageContentElement.value === '' && (commandElement.value === '' || commandElement.value === document.querySelector('#defaultcmd').value)) {
  1693. commandElement.value = drafts[adventureID][pageNum].command;
  1694. pageContentElement.value = drafts[adventureID][pageNum].pageContent;
  1695. pageElement.querySelector('input[value="Save"]').disabled = false;
  1696. } else {
  1697. setTimeout(() => {
  1698. 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) => {
  1699. if (output === "Yes") {
  1700. commandElement.value = drafts[adventureID][pageNum].command;
  1701. pageContentElement.value = drafts[adventureID][pageNum].pageContent;
  1702. pageElement.querySelector('input[value="Save"]').disabled = false;
  1703. }
  1704. });
  1705. }, 1);
  1706. }
  1707. } else if (output === "Delete") {
  1708. setTimeout(() => {
  1709. window.MSPFA.dialog('Delete this draft?', document.createTextNode('This action is irreversable! Are you sure?'), ["Yes", "No"], (output, form) => {
  1710. if (output === "Yes") {
  1711. loadDrafts();
  1712. delete drafts[adventureID][pageNum];
  1713.  
  1714. if (settings.drafts && settings.drafts[adventureID] && settings.drafts[adventureID][pageNum]) {
  1715. delete settings.drafts[adventureID][pageNum];
  1716. saveData(settings);
  1717. }
  1718.  
  1719. saveDrafts(drafts);
  1720. }
  1721. });
  1722. }, 1);
  1723. }
  1724. });
  1725. }
  1726.  
  1727. const createDraftButton = (form) => {
  1728. const draftButton = document.createElement('input');
  1729. Object.assign(draftButton, { className: 'major draft', type: 'button', value: 'Draft' });
  1730. draftButton.addEventListener('click', () => {
  1731. showDraftDialog(form.id.replace('p', ''));
  1732. });
  1733. return draftButton;
  1734. }
  1735.  
  1736. pageLoad(() => {
  1737. let allPages = document.querySelectorAll('#storypages form:not(#newpage)');
  1738. if (allPages.length !== 0) {
  1739. allPages.forEach(form => {
  1740. const prevButton = form.querySelector('input[name="preview"]');
  1741. prevButton.parentNode.insertBefore(createDraftButton(form), prevButton);
  1742. prevButton.parentNode.insertBefore(document.createTextNode(' '), prevButton);
  1743.  
  1744. // Preview
  1745. const previewButton = form.querySelector('input[value=Preview]');
  1746. const previewLink = addLink(previewButton, getPreviewLink(form), '_blank');
  1747. previewButton.addEventListener('mousedown', () => {
  1748. previewLink.href = getPreviewLink(form);
  1749. });
  1750.  
  1751. // "Enable keyboard shortcuts" label
  1752. const shortcutCheck = form.querySelector('input[type="checkbox"]');
  1753. shortcutCheck.previousSibling.textContent = '';
  1754. shortcutCheck.id = `key-${form.id}`;
  1755. shortcutCheck.parentNode.insertBefore(createLabel('Enable keyboard shortcuts: ', shortcutCheck.id), shortcutCheck);
  1756. });
  1757. document.querySelector('input[value="Add"]').addEventListener('click', () => {
  1758. allPages = document.querySelectorAll('#storypages form:not(#newpage)');
  1759. const form = document.querySelector(`#p${allPages.length}`);
  1760. const prevButton = form.querySelector('input[name="preview"]');
  1761. prevButton.parentNode.insertBefore(createDraftButton(form), prevButton);
  1762. prevButton.parentNode.insertBefore(document.createTextNode(' '), prevButton);
  1763.  
  1764. // Preview link
  1765. const previewButton = form.querySelector('input[value=Preview]');
  1766. const previewLink = addLink(previewButton, getPreviewLink(form), '_blank');
  1767. previewButton.addEventListener('mousedown', () => {
  1768. previewLink.href = getPreviewLink(form);
  1769. });
  1770.  
  1771. // "Enable keyboard shortcuts" label
  1772. const shortcutCheck = form.querySelector('input[type="checkbox"]');
  1773. shortcutCheck.previousSibling.textContent = '';
  1774. shortcutCheck.id = `key-${form.id}`;
  1775. shortcutCheck.parentNode.insertBefore(createLabel('Enable keyboard shortcuts: ', shortcutCheck.id), shortcutCheck);
  1776. });
  1777. const newForm = document.querySelector('#newpage');
  1778. {
  1779. // "Enable keyboard shortcuts" label
  1780. const shortcutCheck = newForm.querySelector('input[type="checkbox"]');
  1781. shortcutCheck.previousSibling.textContent = '';
  1782. shortcutCheck.id = `key-${newForm.id}`;
  1783. shortcutCheck.parentNode.insertBefore(createLabel('Enable keyboard shortcuts: ', shortcutCheck.id), shortcutCheck);
  1784. }
  1785. const newPreviewButton = newForm.querySelector('input[value=Preview]');
  1786. const newPreviewLink = addLink(newPreviewButton, getPreviewLink(newForm), '_blank');
  1787. newPreviewButton.addEventListener('mousedown', () => {
  1788. newPreviewLink.href = getPreviewLink(newForm);
  1789. });
  1790. return true;
  1791. }
  1792. });
  1793.  
  1794. if (params.click) {
  1795. if (params.click === 's') {
  1796. spoilerButton.click();
  1797. } else if (params.click === 'd') {
  1798. accessDraftsButton.click();
  1799. }
  1800. }
  1801.  
  1802. // Don't scroll after pressing a BBToolbar button (awesome)
  1803. let lastScroll = window.scrollY;
  1804. pageLoad(() => {
  1805. if (document.querySelectorAll('#storypages textarea').length > 1) {
  1806. document.querySelectorAll('#storypages textarea').forEach(textarea => {
  1807. textarea.addEventListener('focus', () => {
  1808. window.scrollTo(window.scrollX, lastScroll);
  1809. });
  1810. });
  1811.  
  1812. document.addEventListener('scroll', evt => {
  1813. lastScroll = window.scrollY;
  1814. });
  1815. return true;
  1816. }
  1817. });
  1818.  
  1819. // Focus on the text input when clicking on the Color or Background Color BBToolbar buttons
  1820. const colourButtons = [document.querySelector('#bbtoolbar input[data-tag=color]'), document.querySelector('#bbtoolbar input[data-tag=background]')];
  1821.  
  1822. let prevColour;
  1823.  
  1824. colourButtons[0].addEventListener('click', () => {
  1825. const inputs = document.querySelectorAll('#dialog input');
  1826. document.querySelector('button[data-value="Okay"]').addEventListener('click', () => {
  1827. prevColour = inputs[0].value;
  1828. });
  1829. if (prevColour) {
  1830. inputs[0].value = prevColour;
  1831. inputs[1].value = prevColour;
  1832. }
  1833. });
  1834.  
  1835. colourButtons.forEach(button => {
  1836. button.addEventListener('click', () => {
  1837. document.querySelector('#dialog input[type=text]').select();
  1838. });
  1839. });
  1840. }
  1841. else if (location.pathname === "/my/profile/") {
  1842. // Nothing
  1843. }
  1844. else if (location.pathname === "/user/") {
  1845. // Button links
  1846. pageLoad(() => {
  1847. const msgButton = document.querySelector('#sendmsg');
  1848. if (msgButton) {
  1849. addLink(msgButton, `/my/messages/new/?u=${params.u}`);
  1850. addLink(document.querySelector('#favstories'), `/favs/?u=${params.u}`);
  1851. return true;
  1852. }
  1853. });
  1854.  
  1855. // Add extra user stats
  1856. pageLoad(() => {
  1857. if (window.MSPFA) {
  1858. const stats = document.querySelector('#userinfo table');
  1859.  
  1860. const joinTr = stats.insertRow(1);
  1861. const joinTextTd = joinTr.insertCell(0);
  1862. joinTextTd.appendChild(document.createTextNode("Account created:"));
  1863. const joinDate = joinTr.insertCell(1);
  1864. const joinTime = document.createElement('b');
  1865. joinTime.textContent = "Loading...";
  1866. joinDate.appendChild(joinTime);
  1867.  
  1868. const advCountTr = stats.insertRow(2);
  1869. const advTextTd = advCountTr.insertCell(0);
  1870. advTextTd.appendChild(document.createTextNode("Adventures created:"));
  1871. const advCount = advCountTr.insertCell(1);
  1872. const advCountText = document.createElement('b');
  1873. advCountText.textContent = "Loading...";
  1874. advCount.appendChild(advCountText);
  1875.  
  1876. // Show user creation date
  1877. window.MSPFA.request(0, {
  1878. do: "user",
  1879. u: params.u
  1880. }, user => {
  1881. if (typeof user !== "undefined") {
  1882. joinTime.textContent = new Date(user.d).toString().split(' ').splice(1, 4).join(' ');
  1883. }
  1884.  
  1885. // Show created adventures
  1886. window.MSPFA.request(0, {
  1887. do: "editor",
  1888. u: params.u
  1889. }, s => {
  1890. if (typeof s !== "undefined") {
  1891. advCountText.textContent = s.length;
  1892. }
  1893.  
  1894. // Show favourites
  1895. if (document.querySelector('#favstories').style.display !== 'none') {
  1896. const favCountTr = stats.insertRow(3);
  1897. const favTextTd = favCountTr.insertCell(0);
  1898. favTextTd.appendChild(document.createTextNode("Adventures favorited:"));
  1899. const favCount = favCountTr.insertCell(1);
  1900. const favCountText = document.createElement('b');
  1901. favCountText.textContent = "Loading...";
  1902. window.MSPFA.request(0, {
  1903. do: "favs",
  1904. u: params.u
  1905. }, s => {
  1906. if (typeof s !== "undefined") {
  1907. favCountText.textContent = s.length;
  1908. }
  1909. });
  1910. favCount.appendChild(favCountText);
  1911. }
  1912. });
  1913. });
  1914.  
  1915. return true;
  1916. }
  1917. });
  1918. }
  1919. else if (location.pathname === "/favs/" && location.search) {
  1920. const toggleButton = document.createElement('input');
  1921. Object.assign(toggleButton, { className: "major", type: "button", value: "Hide Muted Adventures", title: 'Cycle through muted/unmuted adventures' });
  1922. const buttonRow = document.querySelector('table.container.alt').insertRow(2);
  1923. const actionSpan = document.createElement('span');
  1924.  
  1925. let stories = [];
  1926. // Button links
  1927. pageLoad(() => {
  1928. stories = document.querySelectorAll('#stories tr');
  1929. let favCount = 0;
  1930.  
  1931. if (stories.length > 0) {
  1932. stories.forEach(story => {
  1933. favCount++;
  1934. const id = story.querySelector('a').href.replace('https://mspfa.com/', '');
  1935. pageLoad(() => {
  1936. if (window.MSPFA.me.i) {
  1937. addLink(story.querySelector('.edit.major'), `/my/stories/info/${id}`);
  1938. return true;
  1939. }
  1940. if (pageLoaded) return true;
  1941. });
  1942. addLink(story.querySelector('.rss.major'), `/rss/${id}`);
  1943. });
  1944.  
  1945. // Fav count
  1946. const username = document.querySelector('#username');
  1947. username.parentNode.appendChild(newBr());
  1948. username.parentNode.appendChild(newBr());
  1949. username.parentNode.appendChild(document.createTextNode(`Favorited adventures: ${favCount}`));
  1950.  
  1951. return true;
  1952. }
  1953. if (pageLoaded) return true;
  1954. });
  1955.  
  1956. pageLoad(() => {
  1957. if (window.MSPFA && window.MSPFA.me) {
  1958. if (window.MSPFA.me.i === params.u) {
  1959. const cell = buttonRow.insertCell(0);
  1960. cell.appendChild(toggleButton);
  1961. cell.appendChild(newBr());
  1962. cell.appendChild(actionSpan);
  1963. return true;
  1964. }
  1965. }
  1966. if (pageLoaded) return true;
  1967. });
  1968.  
  1969. let type = 0;
  1970.  
  1971. toggleButton.addEventListener('click', () => {
  1972. type++;
  1973. if (type > 2) type = 0;
  1974.  
  1975. stories.forEach(story => {
  1976. const unmuted = story.querySelector('.notify').className.includes(' lit');
  1977. story.style.display = '';
  1978. if (type === 2 && unmuted || type === 1 && !unmuted) {
  1979. story.style.display = 'none';
  1980. }
  1981. });
  1982.  
  1983. if (type === 0) {
  1984. // show all
  1985. actionSpan.textContent = '';
  1986. }
  1987. else if (type === 1) {
  1988. // hide muted
  1989. toggleButton.value = 'Hide Unmuted Adventures';
  1990. }
  1991. else {
  1992. // only muted
  1993. toggleButton.value = 'Show All Adventures';
  1994. }
  1995. });
  1996. }
  1997. else if (location.pathname === "/search/" && location.search) {
  1998. // Character and word statistics
  1999. const statTable = document.createElement('table');
  2000. const statTbody = document.createElement('tbody');
  2001. const statTr = statTbody.insertRow(0);
  2002. const charCount = statTr.insertCell(0);
  2003. const wordCount = statTr.insertCell(0);
  2004. const statParentTr = document.querySelector('#pages').parentNode.parentNode.insertRow(2);
  2005. const statParentTd = statParentTr.insertCell(0);
  2006.  
  2007. const statHeaderTr = statTbody.insertRow(0);
  2008. const statHeader = document.createElement('th');
  2009. statHeader.colSpan = '2';
  2010.  
  2011. statHeaderTr.appendChild(statHeader);
  2012. statHeader.textContent = 'Statistics may not be entirely accurate.';
  2013.  
  2014. statTable.style.width = "100%";
  2015.  
  2016. charCount.textContent = "Character count: loading...";
  2017. wordCount.textContent = "Word count: loading...";
  2018.  
  2019. statTable.appendChild(statTbody);
  2020. statParentTd.appendChild(statTable);
  2021.  
  2022. pageLoad(() => {
  2023. if (document.querySelector('#pages br')) {
  2024. const bbc = window.MSPFA.BBC.slice();
  2025. bbc.splice(0, 3);
  2026.  
  2027. window.MSPFA.request(0, {
  2028. do: "story",
  2029. s: params.s
  2030. }, story => {
  2031. if (typeof story !== "undefined") {
  2032. const pageContent = [];
  2033. story.p.forEach(p => {
  2034. pageContent.push(p.c);
  2035. pageContent.push(p.b);
  2036. });
  2037.  
  2038. const storyText = pageContent.join(' ')
  2039. .replace(/\n/g, ' ')
  2040. .replace(bbc[0][0], '$1')
  2041. .replace(bbc[1][0], '$1')
  2042. .replace(bbc[2][0], '$1')
  2043. .replace(bbc[3][0], '$1')
  2044. .replace(bbc[4][0], '$2')
  2045. .replace(bbc[5][0], '$3')
  2046. .replace(bbc[6][0], '$3')
  2047. .replace(bbc[7][0], '$3')
  2048. .replace(bbc[8][0], '$3')
  2049. .replace(bbc[9][0], '$3')
  2050. .replace(bbc[10][0], '$2')
  2051. .replace(bbc[11][0], '$1')
  2052. .replace(bbc[12][0], '$3')
  2053. .replace(bbc[13][0], '$3')
  2054. .replace(bbc[14][0], '')
  2055. .replace(bbc[16][0], '$1')
  2056. .replace(bbc[17][0], '$2 $4 $5')
  2057. .replace(bbc[18][0], '$2 $4 $5')
  2058. .replace(bbc[19][0], '')
  2059. .replace(bbc[20][0], '')
  2060. .replace(/<(.*?)>/g, '');
  2061.  
  2062. wordCount.textContent = `Word count: ${storyText.split(/ +/g).length}`;
  2063. charCount.textContent = `Character count: ${storyText.replace(/ +/g, '').length}`;
  2064. }
  2065. });
  2066. return true;
  2067. }
  2068. });
  2069. }
  2070. else if (location.pathname === "/stories/" && location.search) {
  2071.  
  2072. // Add a button to hide adventures with a tag
  2073. const tagHide = document.createElement('input');
  2074. Object.assign(tagHide, { id: 'taghide', value: '-', className: 'major', type: 'button', style: 'padding: 0;', title: 'Hide Tag' });
  2075.  
  2076. const tagAdd = document.querySelector('#tagadd');
  2077. tagAdd.parentNode.insertBefore(tagHide, tagAdd.nextSibling);
  2078. tagAdd.parentNode.insertBefore(document.createTextNode(' '), tagAdd.nextSibling);
  2079.  
  2080. const tagselect = document.querySelector('#tagselect');
  2081. const taglist = document.querySelector('textarea[name=tags]');
  2082. tagHide.addEventListener('click', () => {
  2083. let tags = [];
  2084. if(taglist.value) {
  2085. tags = taglist.value.split(",");
  2086. }
  2087. if(tagselect.value && tags.indexOf('-'+tagselect.value) == -1) {
  2088. tags.push('-'+tagselect.value);
  2089. }
  2090. taglist.value = tags.join(",");
  2091. tagselect.options[0].selected = true;
  2092. });
  2093.  
  2094. // Add titles on hover to the [?] and [+] buttons
  2095. document.querySelector('#taghelp').title = 'Tip';
  2096. tagAdd.title = 'Add Tag';
  2097.  
  2098. // Click text to check/uncheck boxes
  2099. ['Ongoing', 'Complete', 'Inactive'].forEach(t => {
  2100. const check = document.querySelector(`input[name="${t.toLowerCase()}"]`);
  2101. check.id = `check_${t.toLowerCase()}`;
  2102. const label = createLabel(' ' + t + ' ', check.id);
  2103. check.nextSibling.textContent = '';
  2104. check.parentNode.insertBefore(label, check.nextSibling);
  2105. });
  2106.  
  2107. const adventureList = document.querySelector('#doit');
  2108. const resultAmount = document.createElement('span');
  2109. adventureList.parentNode.appendChild(resultAmount);
  2110.  
  2111. pageLoad(() => {
  2112. if (window.MSPFA) {
  2113. window.MSPFA.request(0, {
  2114. do: "stories",
  2115. n: params.n,
  2116. t: params.t,
  2117. h: params.h,
  2118. o: params.o,
  2119. p: params.p,
  2120. m: 20000
  2121. }, (s) => {
  2122. resultAmount.textContent = `Number of results: ${s.length}`;
  2123. return true;
  2124. });
  2125. return true;
  2126. }
  2127. },1);
  2128.  
  2129. pageLoad(() => {
  2130. const stories = document.querySelector('#stories');
  2131. if (stories.childNodes.length > 0) {
  2132. if (params.load && stories.childNodes.length === 1) {
  2133. stories.querySelector('a').click();
  2134. }
  2135.  
  2136. stories.querySelectorAll('tr').forEach(story => {
  2137. const storyID = story.querySelector('a.major').href.split('&')[0].replace(/\D/g, '');
  2138. addLink(story.querySelector('.rss'), `/rss/?s=${storyID}`);
  2139.  
  2140. pageLoad(() => {
  2141. if (window.MSPFA.me.i) {
  2142. addLink(story.querySelector('.edit.major'), `/my/stories/info/?s=${storyID}`);
  2143. return true;
  2144. }
  2145. if (pageLoaded) return true;
  2146. });
  2147. });
  2148. return true;
  2149. }
  2150. if (pageLoaded) return true;
  2151. });
  2152. }
  2153. })();