MSPFA extras

Adds custom quality of life features to MSPFA.

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

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