MSPFA extras

Adds custom quality of life features to MSPFA.

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

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