MSPFA extras

Adds custom quality of life features to MSPFA.

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

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