Guest User

Chyoa Userscript 6/30/2026

a guest
Jun 30th, 2026
67
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 45.82 KB | None | 0 0
  1. // ==UserScript==
  2. // @name Chapter counter for Map and Next Chapter choice (With Path Isolation & Highlight)
  3. // @version 5.0.0
  4. // @description Counts branches, isolates path to target chapter, and highlights choices in pink. Includes drop-down filtering and quick-select buttons next to map chapters and other things i forgot i added. Original Script by sllypper.
  5. // @author The Community
  6. // @match *://chyoa.com/story/*
  7. // @match *://chyoa.com/chapter/*
  8. // @icon https://chyoa.com/favicon.png
  9. // @grant GM.getValue
  10. // @grant GM.setValue
  11. // @grant GM.deleteValue
  12. // @grant GM.listValues
  13. // @grant GM.registerMenuCommand
  14. // ==/UserScript==
  15.  
  16. /* Load the entire map by moving to the bottom, then click the Regenerate button. */
  17.  
  18. // runs automatically when opening a new map
  19. let runAutomatically = true
  20.  
  21. // shows the floating buttons on the bottom-right of the page
  22. let showFloatingButton = true
  23. let showTogglerButton = true
  24. let showPathTracker = true // Added parameter for target feature
  25.  
  26. // Style for the counter on the chapter pages. Use one of the below between ""
  27. // alternative | simple | crazy | default
  28. const cssStyle = "alternative"
  29.  
  30. /************************/
  31.  
  32. let chapterDataArray;
  33. let chapterData;
  34. let storyStorageId;
  35. let pageType = getPageType()
  36. console.log('The current page is a ' + pageType);
  37.  
  38. // Global settings for visibility persistence
  39. let showPathTrackerOnStoryPages = true;
  40. let showStoryNumberButtons = true; // Added toggle setting variable
  41.  
  42. /************************/
  43. (async() => {
  44. // Fetch user profile configurations or initialize default toggles
  45. showPathTrackerOnStoryPages = await GM.getValue("config_show_tracker_story_page", true);
  46. showStoryNumberButtons = await GM.getValue("config_show_story_number_buttons", true);
  47.  
  48. GM.registerMenuCommand("Get map & regenerate (experimental)", getMapAndRegenerateViaAjax, "k" )
  49.  
  50. // Menu toggle to control if tracker box shows on story/chapter reading lanes
  51. GM.registerMenuCommand(
  52. (showPathTrackerOnStoryPages ? "โœ“ " : "โœ— ") + "Show Tracker Panel on Story Pages",
  53. async () => {
  54. showPathTrackerOnStoryPages = !showPathTrackerOnStoryPages;
  55. await GM.setValue("config_show_tracker_story_page", showPathTrackerOnStoryPages);
  56. window.location.reload();
  57. }
  58. );
  59.  
  60. // Menu toggle to control if number counts next to buttons appear
  61. GM.registerMenuCommand(
  62. (showStoryNumberButtons ? "โœ“ " : "โœ— ") + "Show Chapter Counter Badges",
  63. async () => {
  64. showStoryNumberButtons = !showStoryNumberButtons;
  65. await GM.setValue("config_show_story_number_buttons", showStoryNumberButtons);
  66. window.location.reload();
  67. }
  68. );
  69.  
  70. // Load common style configurations for path highlighting
  71. injectGlobalStyles();
  72.  
  73. if (pageType == "map") {
  74. GM.registerMenuCommand("Regenerate map data", generateDraw, "r");
  75. GM.registerMenuCommand("Redraw map counters", drawChildrenCounter, "d");
  76. GM.registerMenuCommand("Fold read chapters", foldTopmostReadChapters, "f");
  77. GM.registerMenuCommand("Fold unread chapters", foldTopmostUnreadChapters, "u");
  78.  
  79. await loadMapData()
  80.  
  81. if (showFloatingButton) { document.body.appendChild(btnLoad()) }
  82.  
  83. addStyleHead(".rd-story-map-row__body > .children { padding-right: 8px; }")
  84.  
  85. if (showTogglerButton) { collapsibator() }
  86. if (showPathTracker) { setTimeout(initPathTracker, 500); }
  87.  
  88. // Setup author filter event listeners on map load
  89. setTimeout(initAuthorFilters, 600);
  90. }
  91.  
  92. // /chapter/* and /story/* pages
  93. if (pageType == "chapter") {
  94. await loadMapData() // Ensure map arrays are in memory for cross-referencing paths
  95. if (showStoryNumberButtons) {
  96. await showChapterCountOnChapterChoices()
  97. }
  98. showChapterDate()
  99.  
  100. // previous chapters map tree on clicking the link
  101. await prevMapInit()
  102.  
  103. // Evaluates visibility filters (Forces override rendering on Maps)
  104. if (showPathTracker && showPathTrackerOnStoryPages) {
  105. setTimeout(initPathTracker, 500);
  106. } else {
  107. // Apply highlighting silently if container panel is hidden entirely by configuration settings
  108. let savedTarget = sessionStorage.getItem('chyoa_path_target_idx') || '-1';
  109. if (savedTarget !== '-1') {
  110. executePathHighlighting(parseInt(savedTarget));
  111. }
  112. }
  113. }
  114.  
  115. GM.registerMenuCommand("Delete this from cache", deleteThisFromCache, "c");
  116. GM.registerMenuCommand("Delete entire cache", deleteEntireCache, "a");
  117.  
  118. GM.registerMenuCommand("Get superParent Group sorted by New", () => console.log(getSuperParentGroupSortedByNew()) )
  119.  
  120. unsafeWindow.computeParents = getSuperParentGroupSortedByNew
  121. })();
  122. return
  123. /************************/
  124.  
  125. async function loadMapData() {
  126. if (! await attemptLoadChapterArrayFromCache() && runAutomatically) {
  127. // no map data on cache
  128. generateMap()
  129. drawChildrenCounter()
  130. } else {
  131. // got map data from cache
  132. drawChildrenCounter()
  133. }
  134. }
  135.  
  136. function generateDraw() {
  137. generateMap()
  138. drawChildrenCounter()
  139. if (showPathTracker) { updatePathTrackerDropdown(); }
  140. }
  141.  
  142. function createChildrenElement(chapterCount) {
  143. var child = document.createElement('span')
  144. child.setAttribute('class', 'control-item children')
  145. let icon = document.createElement('i')
  146. icon.setAttribute('class', 'btb bt-folder')
  147. child.appendChild(icon);
  148. let count = document.createTextNode(" "+chapterCount)
  149. child.appendChild(count)
  150. return child
  151. }
  152.  
  153. function addStyleHead(css) {
  154. var style = document.createElement('style');
  155. document.head.appendChild(style);
  156. style.textContent = css;
  157. };
  158.  
  159. async function attemptLoadChapterArrayFromCache() {
  160. if (!storyStorageId) loadStoryPathName()
  161. if (! await storyIsInTheCache(storyStorageId)) {
  162. console.log('story not found in cache', GM.listValues(), storyStorageId);
  163. return false
  164. }
  165.  
  166. let chapterDataJson = await GM.getValue(storyStorageId)
  167.  
  168. chapterDataArray = JSON.parse(chapterDataJson);
  169. console.log("!! got map from cache");
  170. unsafeWindow.chapterDataArray = chapterDataArray
  171. console.log("Map available as a javascript array on `chapterDataArray`");
  172.  
  173. return true
  174. }
  175.  
  176. function deleteThisFromCache() {
  177. if (pageType == "map") {
  178. GM.deleteValue(getStoryStorageId())
  179. }
  180. let storyStorageId = getStoryStorageId()
  181. GM.deleteValue(storyStorageId)
  182. }
  183.  
  184. function deleteEntireCache() {
  185. (GM.listValues()).forEach((value) => {
  186. GM.deleteValue(value)
  187. })
  188. }
  189.  
  190. async function readOrGenerateMap() {
  191. if (! await attemptLoadChapterArrayFromCache()) generateMap()
  192. }
  193.  
  194. function generateMap() {
  195. console.log("!! generating map");
  196. chapterDataArray = createStoryMap()
  197. console.log("Chapter Count = ", chapterDataArray.length);
  198.  
  199. unsafeWindow.chapterDataArray = chapterDataArray
  200. console.log("Map available as a javascript array on `chapterDataArray`");
  201.  
  202. console.log("!! assembling hierarchy tree indices and unique structural codes");
  203. buildPathNumericalCodes();
  204.  
  205. console.log("!! assembling hierarchy tree");
  206. countMapChapters();
  207. console.log("!! done counting the children");
  208.  
  209. // cache it
  210. GM.setValue(getStoryStorageId(), JSON.stringify(chapterDataArray))
  211. }
  212.  
  213. function buildPathNumericalCodes() {
  214. if (!chapterDataArray || !chapterDataArray.length) return;
  215.  
  216. // First assign sibling sequential indexing to build path codes (e.g., 1.2.1)
  217. let siblingCounters = {};
  218.  
  219. chapterDataArray.forEach((chap, idx) => {
  220. let key = chap.parent === null ? "root" : chap.parent.toString();
  221. if (siblingCounters[key] === undefined) {
  222. siblingCounters[key] = 1;
  223. } else {
  224. siblingCounters[key]++;
  225. }
  226. chap.siblingIdx = siblingCounters[key];
  227. });
  228.  
  229. chapterDataArray.forEach((chap, idx) => {
  230. let codeParts = [];
  231. let current = chap;
  232. while (current) {
  233. codeParts.unshift(current.siblingIdx);
  234. current = current.parent !== null ? chapterDataArray[current.parent] : null;
  235. }
  236. chap.pathCode = codeParts.join(".");
  237. });
  238. }
  239.  
  240. function countMapChapters() {
  241. for (const chapter of chapterDataArray) {
  242. if (chapter.children.length !== 0) continue;
  243. chapter.chapterCount = 0;
  244. let next = chapter.parent != null ? chapter.parent : -1;
  245. while (next != -1) {
  246. next = countChildChaptersOf(next);
  247. }
  248. }
  249. }
  250.  
  251. function countChildChaptersOf(currIndex) {
  252. let nextIndex = -1;
  253. const currentNode = chapterDataArray[currIndex];
  254. if (currentNode.chapterCount != undefined) {
  255. return nextIndex;
  256. }
  257. const chapterCountSum = sumChildrenChaptersCount(currentNode);
  258. if (chapterCountSum === -1) {
  259. return nextIndex;
  260. }
  261.  
  262. currentNode.chapterCount = chapterCountSum + currentNode.children.length;
  263. nextIndex = currentNode.parent !== null ? currentNode.parent : -1;
  264. return nextIndex;
  265. }
  266.  
  267. function sumChildrenChaptersCount(chapterObj) {
  268. return chapterObj.children.reduce((acc, curr) => {
  269. const currentNode = chapterDataArray[curr];
  270. if (currentNode.chapterCount == undefined) {
  271. return -1;
  272. }
  273. return acc !== -1 ? acc + currentNode.chapterCount : acc;
  274. }, 0);
  275. }
  276.  
  277. function drawChildrenCounter() {
  278. let list = document.querySelectorAll(".rd-story-map-row__body");
  279. if (list.length !== chapterDataArray.length) {
  280. console.log('Outdated data. Please regenerate the map');
  281. if (list.length >= chapterDataArray.length) { return }
  282. }
  283.  
  284. list.forEach((elem, i) => {
  285. let existingChildren = elem.querySelector('.children')
  286. if (existingChildren) {
  287. existingChildren.remove()
  288. }
  289.  
  290. let existingQuickBtn = elem.querySelector('.quick-path-btn')
  291. if (existingQuickBtn) {
  292. existingQuickBtn.remove()
  293. }
  294.  
  295. let labelText = chapterDataArray[i].chapterCount;
  296. if (chapterDataArray[i].isLinkChapter) {
  297. labelText = labelText + " ๐Ÿ”—";
  298. }
  299.  
  300. let child = createChildrenElement(labelText)
  301. let titleEl = elem.querySelector('.rd-story-map-row__title')
  302. elem.insertBefore(child, titleEl)
  303.  
  304. // Add quick-select target button next to the chapter listing
  305. let quickBtn = document.createElement('button');
  306. quickBtn.className = 'quick-path-btn';
  307. quickBtn.innerHTML = '๐ŸŽฏ';
  308. quickBtn.title = 'Follow path to this chapter';
  309. quickBtn.setAttribute('style', 'background:none; border:none; cursor:pointer; font-size:11px; padding:0 4px; vertical-align:middle; display:inline-block;');
  310. quickBtn.onclick = (e) => {
  311. e.preventDefault();
  312. e.stopPropagation();
  313. let select = document.getElementById('path-tracker-select');
  314. if (select) {
  315. select.value = i;
  316. }
  317. sessionStorage.setItem('chyoa_path_target_idx', i);
  318. executePathHighlighting(i);
  319. };
  320. elem.insertBefore(quickBtn, child.nextSibling);
  321. })
  322. }
  323.  
  324. function createChapterObj(el) {
  325. const pel = {};
  326. let tempEl = el.querySelector(".rd-story-map-row__title");
  327. pel.title = tempEl ? tempEl.textContent.trim() : "";
  328. pel.url = tempEl ? tempEl.href : "";
  329.  
  330. tempEl = el.querySelector(".rd-story-map-row__author a");
  331. pel.author = tempEl ? tempEl.textContent.trim() : "";
  332.  
  333. let dateEl = el.querySelector(".rd-story-map-row__date");
  334. pel.date = dateEl ? dateEl.textContent.trim() : "";
  335.  
  336. pel.parent = null;
  337. pel.children = [];
  338.  
  339. let indentStr = el.style.getPropertyValue('--rd-story-map-indent');
  340. if (indentStr !== null && indentStr !== "") {
  341. pel.margin = parseInt(indentStr);
  342. } else {
  343. let depthAttr = el.getAttribute('data-depth');
  344. pel.margin = depthAttr ? (parseInt(depthAttr) * 30) : 0;
  345. }
  346.  
  347. // Identify structural Link nodes explicitly via meta layouts
  348. let isLinkMarker = el.querySelector(".rd-story-map-row__meta-item--link");
  349. if (isLinkMarker) {
  350. pel.views = null;
  351. pel.likes = null;
  352. pel.comments = null;
  353. pel.isLinkChapter = 1;
  354. return pel;
  355. }
  356.  
  357. let metaItems = el.querySelectorAll(".rd-story-map-row__meta-item");
  358. if (metaItems.length === 0) {
  359. pel.views = null;
  360. pel.likes = null;
  361. pel.comments = null;
  362. pel.isLinkChapter = 1;
  363. return pel;
  364. }
  365.  
  366. pel.views = 0;
  367. pel.likes = 0;
  368. pel.comments = 0;
  369. pel.isLinkChapter = 0;
  370.  
  371. metaItems.forEach(item => {
  372. let svg = item.querySelector("svg");
  373. if (!svg) return;
  374. let type = svg.getAttribute("data-lucide");
  375. let val = item.textContent.replace(/,/g, "").trim();
  376. if (val === "-") val = "0";
  377. if (type === "eye") pel.views = parseInt(val) || 0;
  378. if (type === "heart") pel.likes = parseInt(val) || 0;
  379. if (type === "message-circle") pel.comments = parseInt(val) || 0;
  380. });
  381.  
  382. return pel;
  383. }
  384.  
  385. function createStoryMap() {
  386. let chapterElementArray = Array.from(document.querySelectorAll(".story-map-chapter"))
  387. let prevParentI = -1;
  388. const finalList = [];
  389.  
  390. chapterElementArray.forEach((el, i) => {
  391. const chapterObj = createChapterObj(el);
  392. finalList[i] = chapterObj;
  393.  
  394. if (i == 0) {
  395. prevParentI = 0;
  396. return;
  397. }
  398.  
  399. const currElMargin = chapterObj.margin
  400. const prevElMargin = finalList[i-1].margin
  401.  
  402. if (prevElMargin < currElMargin) {
  403. chapterObj.parent = parseInt(i - 1);
  404. finalList[i - 1].children.push(parseInt(i));
  405. prevParentI = i - 1;
  406. return;
  407. }
  408.  
  409. if (prevElMargin == currElMargin) {
  410. chapterObj.parent = prevParentI;
  411. finalList[prevParentI].children.push(i);
  412. return;
  413. }
  414.  
  415. const selfMargin = chapterObj.margin;
  416. for (let j = i - 1; j >= 0 && j !== null; j = finalList[j].parent) {
  417. if (finalList[j].margin < selfMargin) {
  418. const actualParentI = j;
  419. chapterObj.parent = actualParentI;
  420. finalList[actualParentI].children.push(i);
  421. prevParentI = actualParentI;
  422. break;
  423. }
  424. }
  425. })
  426. return finalList;
  427. }
  428.  
  429. function createButton(text, action, styleStr) {
  430. let button = document.createElement('button');
  431. button.textContent = text;
  432. button.onclick = action;
  433. button.setAttribute('id', 'regenerate-map-btn');
  434. button.setAttribute('style', styleStr || '');
  435. return button;
  436. };
  437.  
  438. // Map View logic handler
  439. function btnLoadCss() {
  440. return toStyleStr({
  441. 'position': 'fixed',
  442. 'bottom': '10px',
  443. 'right': '10px',
  444. 'padding': '6px 12px',
  445. 'color': '#333',
  446. 'background-color': 'rgb(246, 245, 244)',
  447. 'border': '1px solid #ccc',
  448. 'border-radius': '4px',
  449. 'z-index': '9999999999',
  450. 'font-size': '12px',
  451. 'max-width': 'calc(100% - 20px)'
  452. })
  453. }
  454.  
  455. function btnLoad() {
  456. return createButton('Regenerate Map', function() {
  457. generateDraw()
  458. }, btnLoadCss());
  459. }
  460.  
  461. function collapsibator() {
  462. const input = document.createElement('input')
  463. input.defaultValue = 1
  464. input.setAttribute('id', 'toggler-input')
  465. input.setAttribute('type', 'number')
  466. // Forced background configuration to white for dark themes
  467. input.setAttribute('style', toStyleStr({
  468. 'color': '#111 !important',
  469. 'background-color': '#ffffff !important',
  470. 'width': '40px',
  471. 'padding': '4px',
  472. 'border': '1px solid #999',
  473. 'border-radius': '4px',
  474. 'display': 'inline-block'
  475. }))
  476.  
  477. const button = document.createElement('button')
  478. button.setAttribute('id', 'toggler-btn')
  479. button.textContent = 'Toggle Depth'
  480. button.setAttribute('style', toStyleStr({
  481. 'padding': '4px 8px',
  482. 'color': '#333',
  483. 'background-color': 'rgb(246, 245, 244)',
  484. 'border': '1px solid #ccc',
  485. 'border-radius': '4px',
  486. 'display': 'inline-block',
  487. 'font-size': '12px',
  488. 'margin-left': '4px',
  489. }))
  490. button.onclick = () => {
  491. const level = document.getElementById('toggler-input').value
  492. toggleCollapsibleLevel(level)
  493. }
  494.  
  495. const cont = document.createElement('div')
  496. cont.setAttribute('id', 'toggler-container')
  497. cont.setAttribute('style', toStyleStr({
  498. 'position': 'fixed',
  499. 'bottom': '50px',
  500. 'right': '10px',
  501. 'padding': '4px',
  502. 'background': 'rgba(255,255,255,0.95)',
  503. 'border-radius': '4px',
  504. 'box-shadow': '0 2px 5px rgba(0,0,0,0.2)',
  505. 'z-index': '9999999998',
  506. 'display': 'flex',
  507. 'align-items': 'center'
  508. }))
  509. cont.appendChild(input)
  510. cont.appendChild(button)
  511. document.body.appendChild(cont)
  512. }
  513.  
  514. function toggleCollapsibleLevel(level) {
  515. const chapters = Array.from(document.getElementsByClassName("story-map-chapter"));
  516. if (!chapters.length) return;
  517.  
  518. let targetIndent = parseInt(level) * 30;
  519.  
  520. for (let i = 0; i < chapters.length; i++) {
  521. let indentStr = chapters[i].style.getPropertyValue('--rd-story-map-indent') || "";
  522. let currentIndent = parseInt(indentStr) || (parseInt(chapters[i].getAttribute('data-depth')) * 30) || 0;
  523.  
  524. if (currentIndent === targetIndent) {
  525. const btn = chapters[i].querySelector('.rd-story-map-row__toggle.js-collapsable')
  526. if (btn) btn.click()
  527. }
  528. }
  529. }
  530.  
  531. function initAuthorFilters() {
  532. let authorContainers = document.querySelectorAll(".story-map-chapter .rd-story-map-row__author");
  533. authorContainers.forEach(container => {
  534. let existingEmojiBtn = container.querySelector(".block-author-emoji-btn");
  535. if (existingEmojiBtn) existingEmojiBtn.remove();
  536.  
  537. let authorAnchor = container.querySelector("a");
  538. if (!authorAnchor) return;
  539.  
  540. let targetAuthor = authorAnchor.textContent.trim();
  541. if (!targetAuthor) return;
  542.  
  543. // Generate custom dedicated โ›” button directly to the right of the author anchor link
  544. let blockBtn = document.createElement("button");
  545. blockBtn.className = "block-author-emoji-btn";
  546. blockBtn.innerHTML = "โ›”";
  547. blockBtn.title = "Filter out all chapters written by " + targetAuthor;
  548. blockBtn.setAttribute("style", "background:none; border:none; cursor:pointer; font-size:12px; margin-left:5px; padding:0 2px; vertical-align:middle; display:inline-block;");
  549.  
  550. blockBtn.onclick = (e) => {
  551. e.preventDefault();
  552. e.stopPropagation();
  553.  
  554. let confirmFilter = confirm("Hide all chapters written by: " + targetAuthor + "?");
  555. if (!confirmFilter) return;
  556.  
  557. filterMapByAuthor(targetAuthor);
  558. };
  559.  
  560. authorAnchor.insertAdjacentElement('afterend', blockBtn);
  561. });
  562. }
  563.  
  564. function filterMapByAuthor(authorName) {
  565. const domChapters = Array.from(document.getElementsByClassName("story-map-chapter"));
  566. if (!chapterDataArray || domChapters.length !== chapterDataArray.length) return;
  567.  
  568. let targetAuthorLower = authorName.toLowerCase();
  569.  
  570. chapterDataArray.forEach((chap, idx) => {
  571. if (chap.author && chap.author.toLowerCase() === targetAuthorLower) {
  572. if (domChapters[idx]) {
  573. domChapters[idx].style.setProperty('display', 'none', 'important');
  574. }
  575. }
  576. });
  577. }
  578.  
  579. /* --- TARGET PATH ISOLATION & HIGHLIGHT SYSTEM --- */
  580.  
  581. function injectGlobalStyles() {
  582. addStyleHead(`
  583. .path-highlighted-pink {
  584. background-color: rgba(255, 20, 147, 0.35) !important;
  585. border: 2px solid rgba(255, 20, 147, 0.8) !important;
  586. border-radius: 4px !important;
  587. transition: background 0.3s ease;
  588. }
  589. /* Style adjustments for minimized floating trigger badge */
  590. .path-tracker-minimized {
  591. width: 38px !important;
  592. height: 38px !important;
  593. border-radius: 50% !important;
  594. padding: 0 !important;
  595. justify-content: center !important;
  596. align-items: center !important;
  597. cursor: pointer !important;
  598. background: rgba(255, 255, 255, 0.95) !important;
  599. border: 2px solid rgba(255, 20, 147, 0.6) !important;
  600. box-shadow: 0 4px 10px rgba(0,0,0,0.25) !important;
  601. transition: transform 0.2s ease;
  602. }
  603. .path-tracker-minimized:hover {
  604. transform: scale(1.1);
  605. }
  606. .path-tracker-minimized * {
  607. display: none !important;
  608. }
  609. .path-tracker-minimized::before {
  610. content: "๐ŸŽฏ";
  611. font-size: 18px;
  612. display: block !important;
  613. }
  614. /* Mobile layout adjustments to clean up overlapping panels */
  615. @media (max-width: 767px) {
  616. #path-tracker-container, #toggler-container {
  617. right: 5px !important;
  618. left: auto !important;
  619. }
  620. #regenerate-map-btn {
  621. bottom: 5px !important;
  622. right: 5px !important;
  623. width: 170px !important;
  624. text-align: center;
  625. }
  626. #toggler-container {
  627. bottom: 45px !important;
  628. right: 5px !important;
  629. }
  630. #path-tracker-container:not(.path-tracker-minimized) {
  631. bottom: 85px !important;
  632. width: 170px !important;
  633. }
  634. }
  635. `);
  636. }
  637.  
  638. function initPathTracker() {
  639. if (document.getElementById('path-tracker-container')) return;
  640.  
  641. // Persist path selection values automatically across story page reads
  642. let savedTarget = sessionStorage.getItem('chyoa_path_target_idx') || '-1';
  643.  
  644. const cont = document.createElement('div');
  645. cont.setAttribute('id', 'path-tracker-container');
  646.  
  647. // Dynamic default positioning adjustment between map vs story viewports
  648. let bottomPos = (pageType === "map") ? '95px' : '15px';
  649.  
  650. cont.setAttribute('style', toStyleStr({
  651. 'position': 'fixed',
  652. 'bottom': bottomPos,
  653. 'right': '10px',
  654. 'padding': '6px',
  655. 'background': 'rgba(255,255,255,0.95)',
  656. 'border': '1px solid #bbb',
  657. 'border-radius': '4px',
  658. 'box-shadow': '0 2px 6px rgba(0,0,0,0.3)',
  659. 'z-index': '9999999997',
  660. 'display': 'flex',
  661. 'flex-direction': 'column',
  662. 'gap': '4px',
  663. 'width': '220px',
  664. 'max-width': 'calc(100vw - 20px)',
  665. 'font-family': 'sans-serif',
  666. 'box-sizing': 'border-box',
  667. 'cursor': 'default'
  668. }));
  669.  
  670. const label = document.createElement('label');
  671. label.textContent = 'Follow Story Path To:';
  672. label.setAttribute('style', 'font-size: 11px; font-weight: bold; color: #333; pointer-events: none;');
  673.  
  674. // Add filter input search field
  675. const filterInput = document.createElement('input');
  676. filterInput.setAttribute('id', 'path-tracker-search');
  677. filterInput.setAttribute('type', 'text');
  678. filterInput.setAttribute('placeholder', 'Filter chapters...');
  679. filterInput.setAttribute('style', toStyleStr({
  680. 'color': '#111 !important',
  681. 'background-color': '#ffffff !important',
  682. 'padding': '3px 6px',
  683. 'font-size': '11px',
  684. 'width': '100%',
  685. 'border': '1px solid #ccc',
  686. 'border-radius': '3px',
  687. 'box-sizing': 'border-box'
  688. }));
  689.  
  690. filterInput.oninput = (e) => {
  691. let text = e.target.value.toLowerCase();
  692. let select = document.getElementById('path-tracker-select');
  693. if (!select) return;
  694. Array.from(select.options).forEach(opt => {
  695. if (opt.value === '-1') return; // Always keep "Show All Paths"
  696. let match = opt.textContent.toLowerCase().includes(text);
  697. opt.style.display = match ? '' : 'none';
  698. });
  699. };
  700.  
  701. const select = document.createElement('select');
  702. select.setAttribute('id', 'path-tracker-select');
  703. select.setAttribute('style', toStyleStr({
  704. 'color': '#111 !important',
  705. 'background-color': '#ffffff !important',
  706. 'padding': '4px',
  707. 'font-size': '12px',
  708. 'width': '100%',
  709. 'border': '1px solid #ccc',
  710. 'border-radius': '3px'
  711. }));
  712.  
  713. select.onchange = (e) => {
  714. let val = e.target.value;
  715. sessionStorage.setItem('chyoa_path_target_idx', val);
  716. executePathHighlighting(parseInt(val));
  717. };
  718.  
  719. // Prevent component selection dropdown & search box from collapsing container on click
  720. filterInput.onclick = (e) => e.stopPropagation();
  721. select.onclick = (e) => e.stopPropagation();
  722.  
  723. // Toggle box click logic to safely minimize/expand via empty spacing triggers
  724. cont.onclick = function(e) {
  725. e.preventDefault();
  726. this.classList.toggle('path-tracker-minimized');
  727.  
  728. // Save state locally to persist structural minimized views across updates
  729. if(this.classList.contains('path-tracker-minimized')) {
  730. sessionStorage.setItem('chyoa_tracker_collapsed', 'true');
  731. this.title = "Click to restore tracker path box";
  732. } else {
  733. sessionStorage.setItem('chyoa_tracker_collapsed', 'false');
  734. this.title = "";
  735. }
  736. };
  737.  
  738. cont.appendChild(label);
  739. cont.appendChild(filterInput);
  740. cont.appendChild(select);
  741. document.body.appendChild(cont);
  742.  
  743. updatePathTrackerDropdown();
  744.  
  745. // Reapply minimized states automatically based on layout preferences
  746. if (sessionStorage.getItem('chyoa_tracker_collapsed') === 'true') {
  747. cont.classList.add('path-tracker-minimized');
  748. cont.title = "Click to restore tracker path box";
  749. }
  750.  
  751. // Auto-fire saved tracking paths instantly when navigating across page text links
  752. if (savedTarget !== '-1') {
  753. select.value = savedTarget;
  754. executePathHighlighting(parseInt(savedTarget));
  755. }
  756. }
  757.  
  758. function updatePathTrackerDropdown() {
  759. const select = document.getElementById('path-tracker-select');
  760. if (!select || !chapterDataArray) return;
  761.  
  762. select.innerHTML = '';
  763.  
  764. const defaultOpt = document.createElement('option');
  765. defaultOpt.value = '-1';
  766. defaultOpt.textContent = '-- Show All Paths --';
  767. select.appendChild(defaultOpt);
  768.  
  769. // Regenerate unique hierarchical structural index arrays if missing
  770. if (chapterDataArray.length && !chapterDataArray[0].pathCode) {
  771. buildPathNumericalCodes();
  772. }
  773.  
  774. chapterDataArray.forEach((chap, idx) => {
  775. const opt = document.createElement('option');
  776. opt.value = idx;
  777.  
  778. // Strip nested loop margins. Prepend clean path depth tags [1.1], [1.2.1] directly on the left hand side
  779. let pathTag = "";
  780. let linkIndicator = chap.isLinkChapter ? "๐Ÿ”— " : "";
  781.  
  782. opt.textContent = pathTag + linkIndicator + chap.title;
  783. select.appendChild(opt);
  784. });
  785. }
  786.  
  787. function executePathHighlighting(targetIdx) {
  788. if (pageType === "map") {
  789. isolateAndHighlightMapPath(targetIdx);
  790. } else if (pageType === "chapter") {
  791. highlightStoryChoicePath(targetIdx);
  792. }
  793. }
  794.  
  795. function isolateAndHighlightMapPath(targetIdx) {
  796. const domChapters = Array.from(document.getElementsByClassName("story-map-chapter"));
  797. if (!domChapters.length || !chapterDataArray) return;
  798.  
  799. domChapters.forEach(el => {
  800. el.classList.remove('path-highlighted-pink');
  801. el.style.display = '';
  802. });
  803.  
  804. if (targetIdx === -1) return;
  805.  
  806. let pathIndices = getPathIndicesToRoot(targetIdx);
  807.  
  808. for (let i = 0; i < chapterDataArray.length; i++) {
  809. if (!domChapters[i]) continue;
  810.  
  811. if (pathIndices.has(i)) {
  812. domChapters[i].classList.add('path-highlighted-pink');
  813. domChapters[i].style.setProperty('display', 'block', 'important');
  814.  
  815. const toggleBtn = domChapters[i].querySelector('.rd-story-map-row__toggle.js-collapsable');
  816. const svgIcon = toggleBtn ? toggleBtn.querySelector('svg') : null;
  817. if (svgIcon && svgIcon.getAttribute('data-lucide') === 'plus') {
  818. toggleBtn.click();
  819. }
  820. } else {
  821. let parentId = chapterDataArray[i].parent;
  822. if (parentId !== null && pathIndices.has(parentId)) {
  823. domChapters[i].style.setProperty('display', 'block', 'important');
  824. } else {
  825. domChapters[i].style.setProperty('display', 'none', 'important');
  826. }
  827. }
  828. }
  829.  
  830. if(domChapters[targetIdx]) {
  831. domChapters[targetIdx].scrollIntoView({ behavior: 'smooth', block: 'center' });
  832. }
  833. }
  834.  
  835. // Story Reading View logic handler
  836. function highlightStoryChoicePath(targetIdx) {
  837. let chapterChoices = Array.from(document.querySelectorAll(".rd-story-branch-link"));
  838. chapterChoices.forEach(el => el.classList.remove('path-highlighted-pink'));
  839.  
  840. if (targetIdx === -1 || !chapterDataArray) return;
  841.  
  842. let pathIndices = getPathIndicesToRoot(targetIdx);
  843.  
  844. chapterChoices.forEach(el => {
  845. let normalizedChoiceUrl = new URL(el.href).pathname;
  846.  
  847. let matchedIndex = chapterDataArray.findIndex(c => {
  848. try { return new URL(c.url).pathname === normalizedChoiceUrl; } catch(err) { return false; }
  849. });
  850.  
  851. if (matchedIndex !== -1 && pathIndices.has(matchedIndex)) {
  852. el.classList.add('path-highlighted-pink');
  853. }
  854. });
  855. }
  856.  
  857. // Utility tree pathing tracker helper
  858. function getPathIndicesToRoot(targetIdx) {
  859. let pathIndices = new Set();
  860. let current = targetIdx;
  861. while (current !== null && current >= 0 && current < chapterDataArray.length) {
  862. pathIndices.add(current);
  863. current = chapterDataArray[current].parent;
  864. }
  865. return pathIndices;
  866. }
  867.  
  868. /* --- END OF TARGET PATH ISOLATION SYSTEM --- */
  869.  
  870. function getPageType() {
  871. let url = window.location.pathname
  872. if (url.search(/\/story\/.*\/map/) >= 0) {
  873. return "map"
  874. }
  875. if (url.search(/\/chapter\//) >= 0) {
  876. return "chapter"
  877. }
  878. if (url.search(/\/story\/.*\.[0-9]+$\/?/) >= 0) {
  879. return "chapter"
  880. }
  881. }
  882.  
  883. async function storyIsInTheCache(storyStorageId) {
  884. return ((await GM.listValues()).indexOf(storyStorageId) >= 0)
  885. }
  886.  
  887. async function showChapterCountOnChapterChoices() {
  888. let storyStorageId = getStoryStorageId()
  889. if (!chapterData) {
  890. if (!chapterDataArray) if (! await attemptLoadChapterArrayFromCache()) return false;
  891. chapterData = await getChapterData(storyStorageId)
  892. if (!chapterData) return console.error('Story found but Chapter not found. Please regenerate the map.');
  893. }
  894.  
  895. let nextChapterIndexList = chapterData.children
  896. let chapterChoices = Array.from(document.querySelectorAll(".rd-story-branch-link"))
  897.  
  898. if (cssStyle == "crazy") { applyCrazyCSS() } else
  899. if (cssStyle == "alternative") { applyAlternativeCSS() }
  900.  
  901. chapterChoices.forEach((el, i) => {
  902. let mapIndex = nextChapterIndexList[i]
  903. if (mapIndex == undefined) { return console.log('Chapter "'+el.textContent+'" not found. Please regenerate the story map.'); }
  904. drawLinkChapterCount(el, chapterDataArray[mapIndex], cssStyle)
  905. })
  906. }
  907.  
  908. async function getChapterData() {
  909. if (chapterData) return chapterData;
  910. if (!chapterDataArray) chapterDataArray = await attemptLoadChapterArrayFromCache()
  911. if (!chapterDataArray) {
  912. throw new Error("Failed to load chapterDataArray from cache")
  913. }
  914.  
  915. let chapterUrl = window.location.href
  916. if (chapterUrl.search(/\/story\//) > -1) {
  917. let els = document.querySelectorAll('.rd-story-navigation__links a')
  918. let mapLink = [...els].find(item => item.href.includes('/map'));
  919. let chapterNum = mapLink ? mapLink.href.match(/chapter=(\d+)/)[1] : "1760074";
  920. chapterUrl = "https://chyoa.com/chapter/Introduction."+chapterNum;
  921. }
  922. try {
  923. let currentPath = new URL(chapterUrl).pathname;
  924. return chapterDataArray.find(c => new URL(c.url).pathname === currentPath);
  925. } catch(e) {
  926. return chapterDataArray.find(c=>c.url==chapterUrl);
  927. }
  928. }
  929.  
  930. function showChapterDate() {
  931. getChapterData().then(data => {
  932. if (!data) return;
  933. let date = document.createElement('div')
  934. date.className = "tw-text-sm tw-opacity-70 tw-mt-1";
  935. date.textContent = data.date
  936. let header = document.querySelector('.rd-story-question-header');
  937. if (header) header.append(date)
  938. }).catch(() => {});
  939. }
  940.  
  941. function applyCrazyCSS() {
  942. let style = toStyleStr({
  943. 'display': 'flex',
  944. 'width': '55px',
  945. 'justify-content': 'space-between',
  946. 'align-items': 'center',
  947. 'margin': '0 8px 0 -55px !important',
  948. 'position': 'inherit !important',
  949. 'float': 'left',
  950. 'border-right': '1px solid',
  951. 'padding': '0 8px 0 0',
  952. 'font-family': 'monospace',
  953. 'font-size': '14px',
  954. 'line-height': '27px',
  955. }, '.rd-story-branches .chapterCount')
  956. addStyleHead(style)
  957. }
  958.  
  959. function applyAlternativeCSS() {
  960. let style = toStyleStr({
  961. 'position': 'absolute',
  962. 'left': '-45px',
  963. 'text-align': 'right',
  964. 'width': '40px',
  965. 'padding': '11px 0',
  966. 'top': '0',
  967. }, '.rd-story-branches .chapterCount')
  968. + toStyleStr({
  969. 'position': 'relative',
  970. }, '.rd-story-branches a')
  971. addStyleHead(style)
  972. }
  973.  
  974. function drawLinkChapterCount(el, chapterObj, cssStyle = 'default') {
  975. const chapterCount = chapterObj.chapterCount
  976. const isLinkChapter = chapterObj.isLinkChapter
  977. let span = document.createElement('SPAN')
  978.  
  979. // Append link emoji in story chapter view for true reference pointers
  980. if (isLinkChapter) {
  981. let linkIcon = document.createElement('span');
  982. linkIcon.className = "story-link-chapter-emoji";
  983. linkIcon.innerHTML = "๐Ÿ”— ";
  984. linkIcon.setAttribute('style', 'margin-right: 4px; font-size: 13px; vertical-align: middle;');
  985.  
  986. let titleSpan = el.querySelector(".rd-story-branch-link__text");
  987. if (titleSpan) {
  988. titleSpan.insertAdjacentElement('beforebegin', linkIcon);
  989. } else {
  990. el.insertAdjacentElement('afterbegin', linkIcon);
  991. }
  992. return;
  993. }
  994.  
  995. span.setAttribute('class', 'chapterCount')
  996.  
  997. if (cssStyle == 'simple') {
  998. span.textContent = " ("+chapterCount+")"
  999. el.append(span)
  1000. return
  1001. }
  1002.  
  1003. if (cssStyle == 'alternative') {
  1004. let icon = document.createElement('i')
  1005. icon.setAttribute('class', 'btb bt-folder')
  1006. span.textContent = Number(chapterCount).toString()
  1007. el.append(span)
  1008. el.insertAdjacentElement('afterbegin', icon)
  1009. return
  1010. }
  1011.  
  1012. let icon = document.createElement('i')
  1013. icon.setAttribute('class', 'btb bt-folder')
  1014. span.appendChild(icon);
  1015. let count = document.createTextNode(" "+chapterCount)
  1016. span.append(count)
  1017. el.insertAdjacentElement('afterbegin',span)
  1018.  
  1019. if (cssStyle != 'crazy') {
  1020. let color = window.getComputedStyle(el).color
  1021. span.setAttribute('style', 'position: absolute; margin: 0 0 0 -60px; color: '+color+' !important;')
  1022. icon.setAttribute('style', 'margin: 0 2px 0 0;')
  1023. }
  1024. }
  1025.  
  1026. function toStyleStr(obj, selector) {
  1027. let stack = [], key;
  1028. for (key in obj) {
  1029. if (obj.hasOwnProperty(key)) {
  1030. stack.push(key + ':' + obj[key]);
  1031. }
  1032. }
  1033. if (selector) {
  1034. return selector + '{' + stack.join(';') + '}';
  1035. }
  1036. return stack.join(';');
  1037. }
  1038.  
  1039. function loadStoryPathName() {
  1040. storyStorageId = getStoryStorageId()
  1041. }
  1042.  
  1043. function getStoryStorageId() {
  1044. if (storyStorageId) return storyStorageId;
  1045. try {
  1046. return getMapUrl();
  1047. } catch(e) {
  1048. return window.location.pathname;
  1049. }
  1050. }
  1051.  
  1052. function getMapUrl() {
  1053. if (pageType == "map") return window.location.pathname;
  1054. if (pageType != "chapter") {
  1055. const errorMsg = "Unrecognizable page type"
  1056. console.error(errorMsg);
  1057. throw new Error(errorMsg);
  1058. }
  1059. const mapElement = [...document.querySelectorAll('a')].find((item) => item.text.includes('Story Map') || item.href.includes('/map'))
  1060. const location = new URL(mapElement.href)
  1061. return location.pathname
  1062. }
  1063.  
  1064. function foldTopmostReadChapters() {
  1065. let ignoreList = []
  1066. const els = Array.from(document.querySelectorAll(".story-map-chapter"))
  1067. if (chapterDataArray.length != els.length) return false;
  1068.  
  1069. for (const i in chapterDataArray) {
  1070. if (i == 0) continue
  1071. if (els[i].classList.contains('unread') === false) {
  1072. if (binarySearch(i, ignoreList) == -1) {
  1073. console.log('success at i = ', i);
  1074. let toggle = els[i].querySelector('.js-collapsable');
  1075. if (toggle) toggle.click();
  1076. }
  1077. ignoreList = ignoreList.concat(chapterDataArray[i].children)
  1078. }
  1079. }
  1080. }
  1081.  
  1082. function foldTopmostUnreadChapters() {
  1083. let ignoreList = []
  1084. const els = Array.from(document.querySelectorAll(".story-map-chapter"))
  1085. if (chapterDataArray.length != els.length) return false;
  1086.  
  1087. for (const i in chapterDataArray) {
  1088. if (i == 0) continue
  1089. if (els[i].classList.contains('unread')) {
  1090. if (binarySearch(i, ignoreList) == -1) {
  1091. console.log('success at i = ', i);
  1092. let toggle = els[i].querySelector('.js-collapsable');
  1093. if (toggle) toggle.click();
  1094. }
  1095. ignoreList = ignoreList.concat(chapterDataArray[i].children)
  1096. }
  1097. }
  1098. console.log('ignoreList leng',ignoreList.length);
  1099. }
  1100.  
  1101. function binarySearch(value, list) {
  1102. return list.find(a=>a==value) ? 1 : -1;
  1103. }
  1104.  
  1105. function prevMapShow() {
  1106. let container = document.querySelector('.prevMapContainer')
  1107. if (container) container.classList.toggle('tw-hidden');
  1108. }
  1109.  
  1110. async function prevMapRender() {
  1111. let storyStorageId = getStoryStorageId()
  1112.  
  1113. if (!chapterDataArray) {
  1114. await attemptLoadChapterArrayFromCache();
  1115. }
  1116. if (!chapterData && chapterDataArray) {
  1117. chapterData = await getChapterData(storyStorageId);
  1118. }
  1119.  
  1120. if (!chapterData) {
  1121. let navContainer = document.querySelector('.rd-story-navigation');
  1122. if (navContainer) {
  1123. navContainer.insertAdjacentHTML('afterend', '<div class="prevMapContainer tw-hidden tw-mt-3 tw-p-3 tw-bg-[rgba(var(--chyoa-home-box),0.05)] tw-rounded-md tw-text-sm">Unable to find current chapter mapping. Regenerate map view data.</div>');
  1124. }
  1125. return false;
  1126. }
  1127.  
  1128. if (document.querySelector('.prevMapContainer') === null) {
  1129. let navContainer = document.querySelector('.rd-story-navigation');
  1130. if (navContainer) {
  1131. navContainer.insertAdjacentHTML('afterend', '<div class="prevMapContainer tw-hidden tw-mt-3 tw-p-3 tw-bg-[rgba(0,0,0,0.04)] dark:tw-bg-[rgba(255,255,255,0.04)] tw-rounded-md"></div>');
  1132. }
  1133. }
  1134.  
  1135. drawParentTree()
  1136. }
  1137.  
  1138. function drawThisChapter() {
  1139. let container = document.querySelector('.prevMapContainer')
  1140. if (container && chapterData) container.insertAdjacentHTML('beforeend', '<div class="prevMap_chapter prevMap_currentChapter"><a href="'+chapterData.url+'">'+chapterData.title+'</a></div>')
  1141. }
  1142.  
  1143. function drawParentTree() {
  1144. let container = document.querySelector('.prevMapContainer')
  1145. if (!container) return;
  1146. container.innerHTML = '';
  1147.  
  1148. let chapter = chapterData
  1149. while (chapter && chapter.parent !== null && chapter.parent !== undefined) {
  1150. container.insertAdjacentHTML('afterbegin', '<div class="prevMap_chapter tw-mb-1 tw-flex tw-items-center"><a class="tw-text-14 tw-font-medium tw-text-[rgb(var(--chyoa-link-color))] hover:tw-underline" href="'+chapter.url+'">โ† '+chapter.title+'</a></div>')
  1151. chapter = chapterDataArray[chapter.parent]
  1152. }
  1153. }
  1154.  
  1155. async function prevMapInit() {
  1156. let targetNav = document.querySelector('.rd-story-navigation__links');
  1157. if (targetNav) {
  1158. targetNav.insertAdjacentHTML('beforeend', '<a href="#" class="prevMap_button"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-git-commit tw-size-4" aria-hidden="true"><circle cx="12" cy="12" r="3"></circle><line x1="3" x2="9" y1="12" y2="12"></line><line x1="15" x2="21" y1="12" y2="12"></line></svg>Prev Chapters</a>');
  1159. let prevMapButton = document.querySelector('.prevMap_button')
  1160. prevMapButton.addEventListener('click', function(e) {
  1161. e.preventDefault();
  1162. prevMapShow()
  1163. });
  1164. await prevMapRender()
  1165. }
  1166. }
  1167.  
  1168. function getSuperParentGroupSortedByNew() {
  1169. if (unsafeWindow.parentChapters) return unsafeWindow.parentChapters;
  1170. chapterDataArray.forEach((c, i) => { c.id = i; })
  1171. let parents = chapterDataArray.filter(c => c.parent == 0)
  1172. let parentIds = parents.map(c => c.id)
  1173.  
  1174. function computeSuperParent(chapter) {
  1175. if (chapter.superParent) return
  1176. if (parentIds.includes(chapter.id)) {
  1177. chapter.superParent = chapter.id;
  1178. return
  1179. }
  1180. if (parentIds.includes(chapter.parent)) {
  1181. chapter.superParent = chapter.parent
  1182. return chapter.superParent
  1183. }
  1184. chapter.superParent = getSuperParent(chapterDataArray[chapter.parent])
  1185. }
  1186.  
  1187. function getSuperParent(chapter) {
  1188. if (!chapter.superParent) computeSuperParent(chapter)
  1189. return chapter.superParent
  1190. }
  1191.  
  1192. chapterDataArray.slice(1).forEach(computeSuperParent)
  1193. unsafeWindow.parentChapters = parents.map(p => {
  1194. return {
  1195. p: p,
  1196. c: chapterDataArray.filter(c => c.superParent === p.id)
  1197. }
  1198. })
  1199.  
  1200. function dateComparison(a, b) {
  1201. let aDate = Date.parse(a.date)
  1202. if (! aDate) aDate = 1;
  1203. let bDate = Date.parse(b.date)
  1204. if (! bDate) bDate = 1;
  1205. return bDate - aDate;
  1206. }
  1207.  
  1208. unsafeWindow.parentChapters.forEach(g => g.c.sort((a, b) => { return dateComparison(a, b); }))
  1209. unsafeWindow.parentChapters.sort((a, b) => { return dateComparison(a.c[0], b.c[0]); })
  1210. return unsafeWindow.parentChapters
  1211. }
  1212.  
  1213. function getMapAndRegenerateViaAjax() {
  1214. console.log("!! generating map");
  1215. const url = getMapUrl() + '.json'
  1216. let ajaxChapters = []
  1217.  
  1218. function recursiveGetChaptersFromAjax(page) {
  1219. let currentUrl = url + (page > 1 ? `?page=${page}` : '');
  1220. unsafeWindow.$.get({
  1221. url: currentUrl,
  1222. dataType: "json",
  1223. headers: {
  1224. "Accept": "application/json, text/javascript, */*; q=0.01",
  1225. "X-CSRF-TOKEN": unsafeWindow.Chyoa.csrf_token,
  1226. }
  1227. }).done(function (response) {
  1228. ajaxChapters = ajaxChapters.concat(Object.values(response.data.chapters))
  1229.  
  1230. if (response.data.hasMorePages) {
  1231. recursiveGetChaptersFromAjax(page + 1)
  1232. } else {
  1233. chapterDataArray = createStoryMapAjax(ajaxChapters)
  1234. unsafeWindow.chapterDataArray = chapterDataArray
  1235. buildPathNumericalCodes();
  1236. countMapChapters();
  1237. GM.setValue(getStoryStorageId(), JSON.stringify(chapterDataArray))
  1238. if (showPathTracker) { updatePathTrackerDropdown(); }
  1239. }
  1240. }
  1241. )
  1242. }
  1243.  
  1244. recursiveGetChaptersFromAjax(1);
  1245.  
  1246. function createChapterObjFromAjax(obj) {
  1247. const chapter = { parent: null, children: [] }
  1248. chapter.title = obj.title
  1249. chapter.url = obj.url
  1250. chapter.author = obj.author
  1251. chapter.date = obj.created_at
  1252. chapter.margin = obj.indent
  1253.  
  1254. if (chapter.views) {
  1255. chapter.views = parseInt(obj.views.split(",").join("")) || 0;
  1256. chapter.likes = parseInt(obj.likes) || 0;
  1257. chapter.comments = parseInt(obj.comments) || 0;
  1258. chapter.isLinkChapter = 0;
  1259. } else {
  1260. chapter.views = null;
  1261. chapter.likes = null;
  1262. chapter.comments = null;
  1263. chapter.isLinkChapter = 1;
  1264. }
  1265. return chapter;
  1266. }
  1267.  
  1268. function createStoryMapAjax(chapters) {
  1269. let prevParentI = -1;
  1270. const finalList = [];
  1271.  
  1272. chapters.forEach((el, chapterIndex) => {
  1273. const chapterObj = createChapterObjFromAjax(el);
  1274. finalList[chapterIndex] = chapterObj;
  1275.  
  1276. if (chapterIndex == 0) {
  1277. prevParentI = 0;
  1278. return;
  1279. }
  1280.  
  1281. const currElMargin = chapterObj.margin
  1282. const prevElMargin = finalList[chapterIndex-1].margin
  1283.  
  1284. if (prevElMargin < currElMargin) {
  1285. chapterObj.parent = parseInt(chapterIndex - 1);
  1286. finalList[chapterIndex - 1].children.push(parseInt(chapterIndex));
  1287. prevParentI = chapterIndex - 1;
  1288. return;
  1289. }
  1290.  
  1291. if (prevElMargin == currElMargin) {
  1292. chapterObj.parent = prevParentI;
  1293. finalList[prevParentI].children.push(chapterIndex);
  1294. return;
  1295. }
  1296.  
  1297. const selfMargin = chapterObj.margin;
  1298. for (let parentIndex = chapterIndex - 1; parentIndex >= 0 && parentIndex !== null; parentIndex = finalList[parentIndex].parent) {
  1299. if (finalList[parentIndex].margin < selfMargin) {
  1300. const actualParentI = parentIndex;
  1301. chapterObj.parent = actualParentI;
  1302. finalList[actualParentI].children.push(chapterIndex);
  1303. prevParentI = actualParentI;
  1304. break;
  1305. }
  1306. }
  1307. })
  1308. return finalList;
  1309. }
  1310. }
Advertisement
Add Comment
Please, Sign In to add comment