MSPFA extras

Adds custom quality of life features to MSPFA.

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

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