Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // ==UserScript==
- // @name Chapter counter for Map and Next Chapter choice (With Path Isolation & Highlight)
- // @version 5.0.0
- // @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.
- // @author The Community
- // @match *://chyoa.com/story/*
- // @match *://chyoa.com/chapter/*
- // @icon https://chyoa.com/favicon.png
- // @grant GM.getValue
- // @grant GM.setValue
- // @grant GM.deleteValue
- // @grant GM.listValues
- // @grant GM.registerMenuCommand
- // ==/UserScript==
- /* Load the entire map by moving to the bottom, then click the Regenerate button. */
- // runs automatically when opening a new map
- let runAutomatically = true
- // shows the floating buttons on the bottom-right of the page
- let showFloatingButton = true
- let showTogglerButton = true
- let showPathTracker = true // Added parameter for target feature
- // Style for the counter on the chapter pages. Use one of the below between ""
- // alternative | simple | crazy | default
- const cssStyle = "alternative"
- /************************/
- let chapterDataArray;
- let chapterData;
- let storyStorageId;
- let pageType = getPageType()
- console.log('The current page is a ' + pageType);
- // Global settings for visibility persistence
- let showPathTrackerOnStoryPages = true;
- let showStoryNumberButtons = true; // Added toggle setting variable
- /************************/
- (async() => {
- // Fetch user profile configurations or initialize default toggles
- showPathTrackerOnStoryPages = await GM.getValue("config_show_tracker_story_page", true);
- showStoryNumberButtons = await GM.getValue("config_show_story_number_buttons", true);
- GM.registerMenuCommand("Get map & regenerate (experimental)", getMapAndRegenerateViaAjax, "k" )
- // Menu toggle to control if tracker box shows on story/chapter reading lanes
- GM.registerMenuCommand(
- (showPathTrackerOnStoryPages ? "โ " : "โ ") + "Show Tracker Panel on Story Pages",
- async () => {
- showPathTrackerOnStoryPages = !showPathTrackerOnStoryPages;
- await GM.setValue("config_show_tracker_story_page", showPathTrackerOnStoryPages);
- window.location.reload();
- }
- );
- // Menu toggle to control if number counts next to buttons appear
- GM.registerMenuCommand(
- (showStoryNumberButtons ? "โ " : "โ ") + "Show Chapter Counter Badges",
- async () => {
- showStoryNumberButtons = !showStoryNumberButtons;
- await GM.setValue("config_show_story_number_buttons", showStoryNumberButtons);
- window.location.reload();
- }
- );
- // Load common style configurations for path highlighting
- injectGlobalStyles();
- if (pageType == "map") {
- GM.registerMenuCommand("Regenerate map data", generateDraw, "r");
- GM.registerMenuCommand("Redraw map counters", drawChildrenCounter, "d");
- GM.registerMenuCommand("Fold read chapters", foldTopmostReadChapters, "f");
- GM.registerMenuCommand("Fold unread chapters", foldTopmostUnreadChapters, "u");
- await loadMapData()
- if (showFloatingButton) { document.body.appendChild(btnLoad()) }
- addStyleHead(".rd-story-map-row__body > .children { padding-right: 8px; }")
- if (showTogglerButton) { collapsibator() }
- if (showPathTracker) { setTimeout(initPathTracker, 500); }
- // Setup author filter event listeners on map load
- setTimeout(initAuthorFilters, 600);
- }
- // /chapter/* and /story/* pages
- if (pageType == "chapter") {
- await loadMapData() // Ensure map arrays are in memory for cross-referencing paths
- if (showStoryNumberButtons) {
- await showChapterCountOnChapterChoices()
- }
- showChapterDate()
- // previous chapters map tree on clicking the link
- await prevMapInit()
- // Evaluates visibility filters (Forces override rendering on Maps)
- if (showPathTracker && showPathTrackerOnStoryPages) {
- setTimeout(initPathTracker, 500);
- } else {
- // Apply highlighting silently if container panel is hidden entirely by configuration settings
- let savedTarget = sessionStorage.getItem('chyoa_path_target_idx') || '-1';
- if (savedTarget !== '-1') {
- executePathHighlighting(parseInt(savedTarget));
- }
- }
- }
- GM.registerMenuCommand("Delete this from cache", deleteThisFromCache, "c");
- GM.registerMenuCommand("Delete entire cache", deleteEntireCache, "a");
- GM.registerMenuCommand("Get superParent Group sorted by New", () => console.log(getSuperParentGroupSortedByNew()) )
- unsafeWindow.computeParents = getSuperParentGroupSortedByNew
- })();
- return
- /************************/
- async function loadMapData() {
- if (! await attemptLoadChapterArrayFromCache() && runAutomatically) {
- // no map data on cache
- generateMap()
- drawChildrenCounter()
- } else {
- // got map data from cache
- drawChildrenCounter()
- }
- }
- function generateDraw() {
- generateMap()
- drawChildrenCounter()
- if (showPathTracker) { updatePathTrackerDropdown(); }
- }
- function createChildrenElement(chapterCount) {
- var child = document.createElement('span')
- child.setAttribute('class', 'control-item children')
- let icon = document.createElement('i')
- icon.setAttribute('class', 'btb bt-folder')
- child.appendChild(icon);
- let count = document.createTextNode(" "+chapterCount)
- child.appendChild(count)
- return child
- }
- function addStyleHead(css) {
- var style = document.createElement('style');
- document.head.appendChild(style);
- style.textContent = css;
- };
- async function attemptLoadChapterArrayFromCache() {
- if (!storyStorageId) loadStoryPathName()
- if (! await storyIsInTheCache(storyStorageId)) {
- console.log('story not found in cache', GM.listValues(), storyStorageId);
- return false
- }
- let chapterDataJson = await GM.getValue(storyStorageId)
- chapterDataArray = JSON.parse(chapterDataJson);
- console.log("!! got map from cache");
- unsafeWindow.chapterDataArray = chapterDataArray
- console.log("Map available as a javascript array on `chapterDataArray`");
- return true
- }
- function deleteThisFromCache() {
- if (pageType == "map") {
- GM.deleteValue(getStoryStorageId())
- }
- let storyStorageId = getStoryStorageId()
- GM.deleteValue(storyStorageId)
- }
- function deleteEntireCache() {
- (GM.listValues()).forEach((value) => {
- GM.deleteValue(value)
- })
- }
- async function readOrGenerateMap() {
- if (! await attemptLoadChapterArrayFromCache()) generateMap()
- }
- function generateMap() {
- console.log("!! generating map");
- chapterDataArray = createStoryMap()
- console.log("Chapter Count = ", chapterDataArray.length);
- unsafeWindow.chapterDataArray = chapterDataArray
- console.log("Map available as a javascript array on `chapterDataArray`");
- console.log("!! assembling hierarchy tree indices and unique structural codes");
- buildPathNumericalCodes();
- console.log("!! assembling hierarchy tree");
- countMapChapters();
- console.log("!! done counting the children");
- // cache it
- GM.setValue(getStoryStorageId(), JSON.stringify(chapterDataArray))
- }
- function buildPathNumericalCodes() {
- if (!chapterDataArray || !chapterDataArray.length) return;
- // First assign sibling sequential indexing to build path codes (e.g., 1.2.1)
- let siblingCounters = {};
- chapterDataArray.forEach((chap, idx) => {
- let key = chap.parent === null ? "root" : chap.parent.toString();
- if (siblingCounters[key] === undefined) {
- siblingCounters[key] = 1;
- } else {
- siblingCounters[key]++;
- }
- chap.siblingIdx = siblingCounters[key];
- });
- chapterDataArray.forEach((chap, idx) => {
- let codeParts = [];
- let current = chap;
- while (current) {
- codeParts.unshift(current.siblingIdx);
- current = current.parent !== null ? chapterDataArray[current.parent] : null;
- }
- chap.pathCode = codeParts.join(".");
- });
- }
- function countMapChapters() {
- for (const chapter of chapterDataArray) {
- if (chapter.children.length !== 0) continue;
- chapter.chapterCount = 0;
- let next = chapter.parent != null ? chapter.parent : -1;
- while (next != -1) {
- next = countChildChaptersOf(next);
- }
- }
- }
- function countChildChaptersOf(currIndex) {
- let nextIndex = -1;
- const currentNode = chapterDataArray[currIndex];
- if (currentNode.chapterCount != undefined) {
- return nextIndex;
- }
- const chapterCountSum = sumChildrenChaptersCount(currentNode);
- if (chapterCountSum === -1) {
- return nextIndex;
- }
- currentNode.chapterCount = chapterCountSum + currentNode.children.length;
- nextIndex = currentNode.parent !== null ? currentNode.parent : -1;
- return nextIndex;
- }
- function sumChildrenChaptersCount(chapterObj) {
- return chapterObj.children.reduce((acc, curr) => {
- const currentNode = chapterDataArray[curr];
- if (currentNode.chapterCount == undefined) {
- return -1;
- }
- return acc !== -1 ? acc + currentNode.chapterCount : acc;
- }, 0);
- }
- function drawChildrenCounter() {
- let list = document.querySelectorAll(".rd-story-map-row__body");
- if (list.length !== chapterDataArray.length) {
- console.log('Outdated data. Please regenerate the map');
- if (list.length >= chapterDataArray.length) { return }
- }
- list.forEach((elem, i) => {
- let existingChildren = elem.querySelector('.children')
- if (existingChildren) {
- existingChildren.remove()
- }
- let existingQuickBtn = elem.querySelector('.quick-path-btn')
- if (existingQuickBtn) {
- existingQuickBtn.remove()
- }
- let labelText = chapterDataArray[i].chapterCount;
- if (chapterDataArray[i].isLinkChapter) {
- labelText = labelText + " ๐";
- }
- let child = createChildrenElement(labelText)
- let titleEl = elem.querySelector('.rd-story-map-row__title')
- elem.insertBefore(child, titleEl)
- // Add quick-select target button next to the chapter listing
- let quickBtn = document.createElement('button');
- quickBtn.className = 'quick-path-btn';
- quickBtn.innerHTML = '๐ฏ';
- quickBtn.title = 'Follow path to this chapter';
- quickBtn.setAttribute('style', 'background:none; border:none; cursor:pointer; font-size:11px; padding:0 4px; vertical-align:middle; display:inline-block;');
- quickBtn.onclick = (e) => {
- e.preventDefault();
- e.stopPropagation();
- let select = document.getElementById('path-tracker-select');
- if (select) {
- select.value = i;
- }
- sessionStorage.setItem('chyoa_path_target_idx', i);
- executePathHighlighting(i);
- };
- elem.insertBefore(quickBtn, child.nextSibling);
- })
- }
- function createChapterObj(el) {
- const pel = {};
- let tempEl = el.querySelector(".rd-story-map-row__title");
- pel.title = tempEl ? tempEl.textContent.trim() : "";
- pel.url = tempEl ? tempEl.href : "";
- tempEl = el.querySelector(".rd-story-map-row__author a");
- pel.author = tempEl ? tempEl.textContent.trim() : "";
- let dateEl = el.querySelector(".rd-story-map-row__date");
- pel.date = dateEl ? dateEl.textContent.trim() : "";
- pel.parent = null;
- pel.children = [];
- let indentStr = el.style.getPropertyValue('--rd-story-map-indent');
- if (indentStr !== null && indentStr !== "") {
- pel.margin = parseInt(indentStr);
- } else {
- let depthAttr = el.getAttribute('data-depth');
- pel.margin = depthAttr ? (parseInt(depthAttr) * 30) : 0;
- }
- // Identify structural Link nodes explicitly via meta layouts
- let isLinkMarker = el.querySelector(".rd-story-map-row__meta-item--link");
- if (isLinkMarker) {
- pel.views = null;
- pel.likes = null;
- pel.comments = null;
- pel.isLinkChapter = 1;
- return pel;
- }
- let metaItems = el.querySelectorAll(".rd-story-map-row__meta-item");
- if (metaItems.length === 0) {
- pel.views = null;
- pel.likes = null;
- pel.comments = null;
- pel.isLinkChapter = 1;
- return pel;
- }
- pel.views = 0;
- pel.likes = 0;
- pel.comments = 0;
- pel.isLinkChapter = 0;
- metaItems.forEach(item => {
- let svg = item.querySelector("svg");
- if (!svg) return;
- let type = svg.getAttribute("data-lucide");
- let val = item.textContent.replace(/,/g, "").trim();
- if (val === "-") val = "0";
- if (type === "eye") pel.views = parseInt(val) || 0;
- if (type === "heart") pel.likes = parseInt(val) || 0;
- if (type === "message-circle") pel.comments = parseInt(val) || 0;
- });
- return pel;
- }
- function createStoryMap() {
- let chapterElementArray = Array.from(document.querySelectorAll(".story-map-chapter"))
- let prevParentI = -1;
- const finalList = [];
- chapterElementArray.forEach((el, i) => {
- const chapterObj = createChapterObj(el);
- finalList[i] = chapterObj;
- if (i == 0) {
- prevParentI = 0;
- return;
- }
- const currElMargin = chapterObj.margin
- const prevElMargin = finalList[i-1].margin
- if (prevElMargin < currElMargin) {
- chapterObj.parent = parseInt(i - 1);
- finalList[i - 1].children.push(parseInt(i));
- prevParentI = i - 1;
- return;
- }
- if (prevElMargin == currElMargin) {
- chapterObj.parent = prevParentI;
- finalList[prevParentI].children.push(i);
- return;
- }
- const selfMargin = chapterObj.margin;
- for (let j = i - 1; j >= 0 && j !== null; j = finalList[j].parent) {
- if (finalList[j].margin < selfMargin) {
- const actualParentI = j;
- chapterObj.parent = actualParentI;
- finalList[actualParentI].children.push(i);
- prevParentI = actualParentI;
- break;
- }
- }
- })
- return finalList;
- }
- function createButton(text, action, styleStr) {
- let button = document.createElement('button');
- button.textContent = text;
- button.onclick = action;
- button.setAttribute('id', 'regenerate-map-btn');
- button.setAttribute('style', styleStr || '');
- return button;
- };
- // Map View logic handler
- function btnLoadCss() {
- return toStyleStr({
- 'position': 'fixed',
- 'bottom': '10px',
- 'right': '10px',
- 'padding': '6px 12px',
- 'color': '#333',
- 'background-color': 'rgb(246, 245, 244)',
- 'border': '1px solid #ccc',
- 'border-radius': '4px',
- 'z-index': '9999999999',
- 'font-size': '12px',
- 'max-width': 'calc(100% - 20px)'
- })
- }
- function btnLoad() {
- return createButton('Regenerate Map', function() {
- generateDraw()
- }, btnLoadCss());
- }
- function collapsibator() {
- const input = document.createElement('input')
- input.defaultValue = 1
- input.setAttribute('id', 'toggler-input')
- input.setAttribute('type', 'number')
- // Forced background configuration to white for dark themes
- input.setAttribute('style', toStyleStr({
- 'color': '#111 !important',
- 'background-color': '#ffffff !important',
- 'width': '40px',
- 'padding': '4px',
- 'border': '1px solid #999',
- 'border-radius': '4px',
- 'display': 'inline-block'
- }))
- const button = document.createElement('button')
- button.setAttribute('id', 'toggler-btn')
- button.textContent = 'Toggle Depth'
- button.setAttribute('style', toStyleStr({
- 'padding': '4px 8px',
- 'color': '#333',
- 'background-color': 'rgb(246, 245, 244)',
- 'border': '1px solid #ccc',
- 'border-radius': '4px',
- 'display': 'inline-block',
- 'font-size': '12px',
- 'margin-left': '4px',
- }))
- button.onclick = () => {
- const level = document.getElementById('toggler-input').value
- toggleCollapsibleLevel(level)
- }
- const cont = document.createElement('div')
- cont.setAttribute('id', 'toggler-container')
- cont.setAttribute('style', toStyleStr({
- 'position': 'fixed',
- 'bottom': '50px',
- 'right': '10px',
- 'padding': '4px',
- 'background': 'rgba(255,255,255,0.95)',
- 'border-radius': '4px',
- 'box-shadow': '0 2px 5px rgba(0,0,0,0.2)',
- 'z-index': '9999999998',
- 'display': 'flex',
- 'align-items': 'center'
- }))
- cont.appendChild(input)
- cont.appendChild(button)
- document.body.appendChild(cont)
- }
- function toggleCollapsibleLevel(level) {
- const chapters = Array.from(document.getElementsByClassName("story-map-chapter"));
- if (!chapters.length) return;
- let targetIndent = parseInt(level) * 30;
- for (let i = 0; i < chapters.length; i++) {
- let indentStr = chapters[i].style.getPropertyValue('--rd-story-map-indent') || "";
- let currentIndent = parseInt(indentStr) || (parseInt(chapters[i].getAttribute('data-depth')) * 30) || 0;
- if (currentIndent === targetIndent) {
- const btn = chapters[i].querySelector('.rd-story-map-row__toggle.js-collapsable')
- if (btn) btn.click()
- }
- }
- }
- function initAuthorFilters() {
- let authorContainers = document.querySelectorAll(".story-map-chapter .rd-story-map-row__author");
- authorContainers.forEach(container => {
- let existingEmojiBtn = container.querySelector(".block-author-emoji-btn");
- if (existingEmojiBtn) existingEmojiBtn.remove();
- let authorAnchor = container.querySelector("a");
- if (!authorAnchor) return;
- let targetAuthor = authorAnchor.textContent.trim();
- if (!targetAuthor) return;
- // Generate custom dedicated โ button directly to the right of the author anchor link
- let blockBtn = document.createElement("button");
- blockBtn.className = "block-author-emoji-btn";
- blockBtn.innerHTML = "โ";
- blockBtn.title = "Filter out all chapters written by " + targetAuthor;
- blockBtn.setAttribute("style", "background:none; border:none; cursor:pointer; font-size:12px; margin-left:5px; padding:0 2px; vertical-align:middle; display:inline-block;");
- blockBtn.onclick = (e) => {
- e.preventDefault();
- e.stopPropagation();
- let confirmFilter = confirm("Hide all chapters written by: " + targetAuthor + "?");
- if (!confirmFilter) return;
- filterMapByAuthor(targetAuthor);
- };
- authorAnchor.insertAdjacentElement('afterend', blockBtn);
- });
- }
- function filterMapByAuthor(authorName) {
- const domChapters = Array.from(document.getElementsByClassName("story-map-chapter"));
- if (!chapterDataArray || domChapters.length !== chapterDataArray.length) return;
- let targetAuthorLower = authorName.toLowerCase();
- chapterDataArray.forEach((chap, idx) => {
- if (chap.author && chap.author.toLowerCase() === targetAuthorLower) {
- if (domChapters[idx]) {
- domChapters[idx].style.setProperty('display', 'none', 'important');
- }
- }
- });
- }
- /* --- TARGET PATH ISOLATION & HIGHLIGHT SYSTEM --- */
- function injectGlobalStyles() {
- addStyleHead(`
- .path-highlighted-pink {
- background-color: rgba(255, 20, 147, 0.35) !important;
- border: 2px solid rgba(255, 20, 147, 0.8) !important;
- border-radius: 4px !important;
- transition: background 0.3s ease;
- }
- /* Style adjustments for minimized floating trigger badge */
- .path-tracker-minimized {
- width: 38px !important;
- height: 38px !important;
- border-radius: 50% !important;
- padding: 0 !important;
- justify-content: center !important;
- align-items: center !important;
- cursor: pointer !important;
- background: rgba(255, 255, 255, 0.95) !important;
- border: 2px solid rgba(255, 20, 147, 0.6) !important;
- box-shadow: 0 4px 10px rgba(0,0,0,0.25) !important;
- transition: transform 0.2s ease;
- }
- .path-tracker-minimized:hover {
- transform: scale(1.1);
- }
- .path-tracker-minimized * {
- display: none !important;
- }
- .path-tracker-minimized::before {
- content: "๐ฏ";
- font-size: 18px;
- display: block !important;
- }
- /* Mobile layout adjustments to clean up overlapping panels */
- @media (max-width: 767px) {
- #path-tracker-container, #toggler-container {
- right: 5px !important;
- left: auto !important;
- }
- #regenerate-map-btn {
- bottom: 5px !important;
- right: 5px !important;
- width: 170px !important;
- text-align: center;
- }
- #toggler-container {
- bottom: 45px !important;
- right: 5px !important;
- }
- #path-tracker-container:not(.path-tracker-minimized) {
- bottom: 85px !important;
- width: 170px !important;
- }
- }
- `);
- }
- function initPathTracker() {
- if (document.getElementById('path-tracker-container')) return;
- // Persist path selection values automatically across story page reads
- let savedTarget = sessionStorage.getItem('chyoa_path_target_idx') || '-1';
- const cont = document.createElement('div');
- cont.setAttribute('id', 'path-tracker-container');
- // Dynamic default positioning adjustment between map vs story viewports
- let bottomPos = (pageType === "map") ? '95px' : '15px';
- cont.setAttribute('style', toStyleStr({
- 'position': 'fixed',
- 'bottom': bottomPos,
- 'right': '10px',
- 'padding': '6px',
- 'background': 'rgba(255,255,255,0.95)',
- 'border': '1px solid #bbb',
- 'border-radius': '4px',
- 'box-shadow': '0 2px 6px rgba(0,0,0,0.3)',
- 'z-index': '9999999997',
- 'display': 'flex',
- 'flex-direction': 'column',
- 'gap': '4px',
- 'width': '220px',
- 'max-width': 'calc(100vw - 20px)',
- 'font-family': 'sans-serif',
- 'box-sizing': 'border-box',
- 'cursor': 'default'
- }));
- const label = document.createElement('label');
- label.textContent = 'Follow Story Path To:';
- label.setAttribute('style', 'font-size: 11px; font-weight: bold; color: #333; pointer-events: none;');
- // Add filter input search field
- const filterInput = document.createElement('input');
- filterInput.setAttribute('id', 'path-tracker-search');
- filterInput.setAttribute('type', 'text');
- filterInput.setAttribute('placeholder', 'Filter chapters...');
- filterInput.setAttribute('style', toStyleStr({
- 'color': '#111 !important',
- 'background-color': '#ffffff !important',
- 'padding': '3px 6px',
- 'font-size': '11px',
- 'width': '100%',
- 'border': '1px solid #ccc',
- 'border-radius': '3px',
- 'box-sizing': 'border-box'
- }));
- filterInput.oninput = (e) => {
- let text = e.target.value.toLowerCase();
- let select = document.getElementById('path-tracker-select');
- if (!select) return;
- Array.from(select.options).forEach(opt => {
- if (opt.value === '-1') return; // Always keep "Show All Paths"
- let match = opt.textContent.toLowerCase().includes(text);
- opt.style.display = match ? '' : 'none';
- });
- };
- const select = document.createElement('select');
- select.setAttribute('id', 'path-tracker-select');
- select.setAttribute('style', toStyleStr({
- 'color': '#111 !important',
- 'background-color': '#ffffff !important',
- 'padding': '4px',
- 'font-size': '12px',
- 'width': '100%',
- 'border': '1px solid #ccc',
- 'border-radius': '3px'
- }));
- select.onchange = (e) => {
- let val = e.target.value;
- sessionStorage.setItem('chyoa_path_target_idx', val);
- executePathHighlighting(parseInt(val));
- };
- // Prevent component selection dropdown & search box from collapsing container on click
- filterInput.onclick = (e) => e.stopPropagation();
- select.onclick = (e) => e.stopPropagation();
- // Toggle box click logic to safely minimize/expand via empty spacing triggers
- cont.onclick = function(e) {
- e.preventDefault();
- this.classList.toggle('path-tracker-minimized');
- // Save state locally to persist structural minimized views across updates
- if(this.classList.contains('path-tracker-minimized')) {
- sessionStorage.setItem('chyoa_tracker_collapsed', 'true');
- this.title = "Click to restore tracker path box";
- } else {
- sessionStorage.setItem('chyoa_tracker_collapsed', 'false');
- this.title = "";
- }
- };
- cont.appendChild(label);
- cont.appendChild(filterInput);
- cont.appendChild(select);
- document.body.appendChild(cont);
- updatePathTrackerDropdown();
- // Reapply minimized states automatically based on layout preferences
- if (sessionStorage.getItem('chyoa_tracker_collapsed') === 'true') {
- cont.classList.add('path-tracker-minimized');
- cont.title = "Click to restore tracker path box";
- }
- // Auto-fire saved tracking paths instantly when navigating across page text links
- if (savedTarget !== '-1') {
- select.value = savedTarget;
- executePathHighlighting(parseInt(savedTarget));
- }
- }
- function updatePathTrackerDropdown() {
- const select = document.getElementById('path-tracker-select');
- if (!select || !chapterDataArray) return;
- select.innerHTML = '';
- const defaultOpt = document.createElement('option');
- defaultOpt.value = '-1';
- defaultOpt.textContent = '-- Show All Paths --';
- select.appendChild(defaultOpt);
- // Regenerate unique hierarchical structural index arrays if missing
- if (chapterDataArray.length && !chapterDataArray[0].pathCode) {
- buildPathNumericalCodes();
- }
- chapterDataArray.forEach((chap, idx) => {
- const opt = document.createElement('option');
- opt.value = idx;
- // Strip nested loop margins. Prepend clean path depth tags [1.1], [1.2.1] directly on the left hand side
- let pathTag = "";
- let linkIndicator = chap.isLinkChapter ? "๐ " : "";
- opt.textContent = pathTag + linkIndicator + chap.title;
- select.appendChild(opt);
- });
- }
- function executePathHighlighting(targetIdx) {
- if (pageType === "map") {
- isolateAndHighlightMapPath(targetIdx);
- } else if (pageType === "chapter") {
- highlightStoryChoicePath(targetIdx);
- }
- }
- function isolateAndHighlightMapPath(targetIdx) {
- const domChapters = Array.from(document.getElementsByClassName("story-map-chapter"));
- if (!domChapters.length || !chapterDataArray) return;
- domChapters.forEach(el => {
- el.classList.remove('path-highlighted-pink');
- el.style.display = '';
- });
- if (targetIdx === -1) return;
- let pathIndices = getPathIndicesToRoot(targetIdx);
- for (let i = 0; i < chapterDataArray.length; i++) {
- if (!domChapters[i]) continue;
- if (pathIndices.has(i)) {
- domChapters[i].classList.add('path-highlighted-pink');
- domChapters[i].style.setProperty('display', 'block', 'important');
- const toggleBtn = domChapters[i].querySelector('.rd-story-map-row__toggle.js-collapsable');
- const svgIcon = toggleBtn ? toggleBtn.querySelector('svg') : null;
- if (svgIcon && svgIcon.getAttribute('data-lucide') === 'plus') {
- toggleBtn.click();
- }
- } else {
- let parentId = chapterDataArray[i].parent;
- if (parentId !== null && pathIndices.has(parentId)) {
- domChapters[i].style.setProperty('display', 'block', 'important');
- } else {
- domChapters[i].style.setProperty('display', 'none', 'important');
- }
- }
- }
- if(domChapters[targetIdx]) {
- domChapters[targetIdx].scrollIntoView({ behavior: 'smooth', block: 'center' });
- }
- }
- // Story Reading View logic handler
- function highlightStoryChoicePath(targetIdx) {
- let chapterChoices = Array.from(document.querySelectorAll(".rd-story-branch-link"));
- chapterChoices.forEach(el => el.classList.remove('path-highlighted-pink'));
- if (targetIdx === -1 || !chapterDataArray) return;
- let pathIndices = getPathIndicesToRoot(targetIdx);
- chapterChoices.forEach(el => {
- let normalizedChoiceUrl = new URL(el.href).pathname;
- let matchedIndex = chapterDataArray.findIndex(c => {
- try { return new URL(c.url).pathname === normalizedChoiceUrl; } catch(err) { return false; }
- });
- if (matchedIndex !== -1 && pathIndices.has(matchedIndex)) {
- el.classList.add('path-highlighted-pink');
- }
- });
- }
- // Utility tree pathing tracker helper
- function getPathIndicesToRoot(targetIdx) {
- let pathIndices = new Set();
- let current = targetIdx;
- while (current !== null && current >= 0 && current < chapterDataArray.length) {
- pathIndices.add(current);
- current = chapterDataArray[current].parent;
- }
- return pathIndices;
- }
- /* --- END OF TARGET PATH ISOLATION SYSTEM --- */
- function getPageType() {
- let url = window.location.pathname
- if (url.search(/\/story\/.*\/map/) >= 0) {
- return "map"
- }
- if (url.search(/\/chapter\//) >= 0) {
- return "chapter"
- }
- if (url.search(/\/story\/.*\.[0-9]+$\/?/) >= 0) {
- return "chapter"
- }
- }
- async function storyIsInTheCache(storyStorageId) {
- return ((await GM.listValues()).indexOf(storyStorageId) >= 0)
- }
- async function showChapterCountOnChapterChoices() {
- let storyStorageId = getStoryStorageId()
- if (!chapterData) {
- if (!chapterDataArray) if (! await attemptLoadChapterArrayFromCache()) return false;
- chapterData = await getChapterData(storyStorageId)
- if (!chapterData) return console.error('Story found but Chapter not found. Please regenerate the map.');
- }
- let nextChapterIndexList = chapterData.children
- let chapterChoices = Array.from(document.querySelectorAll(".rd-story-branch-link"))
- if (cssStyle == "crazy") { applyCrazyCSS() } else
- if (cssStyle == "alternative") { applyAlternativeCSS() }
- chapterChoices.forEach((el, i) => {
- let mapIndex = nextChapterIndexList[i]
- if (mapIndex == undefined) { return console.log('Chapter "'+el.textContent+'" not found. Please regenerate the story map.'); }
- drawLinkChapterCount(el, chapterDataArray[mapIndex], cssStyle)
- })
- }
- async function getChapterData() {
- if (chapterData) return chapterData;
- if (!chapterDataArray) chapterDataArray = await attemptLoadChapterArrayFromCache()
- if (!chapterDataArray) {
- throw new Error("Failed to load chapterDataArray from cache")
- }
- let chapterUrl = window.location.href
- if (chapterUrl.search(/\/story\//) > -1) {
- let els = document.querySelectorAll('.rd-story-navigation__links a')
- let mapLink = [...els].find(item => item.href.includes('/map'));
- let chapterNum = mapLink ? mapLink.href.match(/chapter=(\d+)/)[1] : "1760074";
- chapterUrl = "https://chyoa.com/chapter/Introduction."+chapterNum;
- }
- try {
- let currentPath = new URL(chapterUrl).pathname;
- return chapterDataArray.find(c => new URL(c.url).pathname === currentPath);
- } catch(e) {
- return chapterDataArray.find(c=>c.url==chapterUrl);
- }
- }
- function showChapterDate() {
- getChapterData().then(data => {
- if (!data) return;
- let date = document.createElement('div')
- date.className = "tw-text-sm tw-opacity-70 tw-mt-1";
- date.textContent = data.date
- let header = document.querySelector('.rd-story-question-header');
- if (header) header.append(date)
- }).catch(() => {});
- }
- function applyCrazyCSS() {
- let style = toStyleStr({
- 'display': 'flex',
- 'width': '55px',
- 'justify-content': 'space-between',
- 'align-items': 'center',
- 'margin': '0 8px 0 -55px !important',
- 'position': 'inherit !important',
- 'float': 'left',
- 'border-right': '1px solid',
- 'padding': '0 8px 0 0',
- 'font-family': 'monospace',
- 'font-size': '14px',
- 'line-height': '27px',
- }, '.rd-story-branches .chapterCount')
- addStyleHead(style)
- }
- function applyAlternativeCSS() {
- let style = toStyleStr({
- 'position': 'absolute',
- 'left': '-45px',
- 'text-align': 'right',
- 'width': '40px',
- 'padding': '11px 0',
- 'top': '0',
- }, '.rd-story-branches .chapterCount')
- + toStyleStr({
- 'position': 'relative',
- }, '.rd-story-branches a')
- addStyleHead(style)
- }
- function drawLinkChapterCount(el, chapterObj, cssStyle = 'default') {
- const chapterCount = chapterObj.chapterCount
- const isLinkChapter = chapterObj.isLinkChapter
- let span = document.createElement('SPAN')
- // Append link emoji in story chapter view for true reference pointers
- if (isLinkChapter) {
- let linkIcon = document.createElement('span');
- linkIcon.className = "story-link-chapter-emoji";
- linkIcon.innerHTML = "๐ ";
- linkIcon.setAttribute('style', 'margin-right: 4px; font-size: 13px; vertical-align: middle;');
- let titleSpan = el.querySelector(".rd-story-branch-link__text");
- if (titleSpan) {
- titleSpan.insertAdjacentElement('beforebegin', linkIcon);
- } else {
- el.insertAdjacentElement('afterbegin', linkIcon);
- }
- return;
- }
- span.setAttribute('class', 'chapterCount')
- if (cssStyle == 'simple') {
- span.textContent = " ("+chapterCount+")"
- el.append(span)
- return
- }
- if (cssStyle == 'alternative') {
- let icon = document.createElement('i')
- icon.setAttribute('class', 'btb bt-folder')
- span.textContent = Number(chapterCount).toString()
- el.append(span)
- el.insertAdjacentElement('afterbegin', icon)
- return
- }
- let icon = document.createElement('i')
- icon.setAttribute('class', 'btb bt-folder')
- span.appendChild(icon);
- let count = document.createTextNode(" "+chapterCount)
- span.append(count)
- el.insertAdjacentElement('afterbegin',span)
- if (cssStyle != 'crazy') {
- let color = window.getComputedStyle(el).color
- span.setAttribute('style', 'position: absolute; margin: 0 0 0 -60px; color: '+color+' !important;')
- icon.setAttribute('style', 'margin: 0 2px 0 0;')
- }
- }
- function toStyleStr(obj, selector) {
- let stack = [], key;
- for (key in obj) {
- if (obj.hasOwnProperty(key)) {
- stack.push(key + ':' + obj[key]);
- }
- }
- if (selector) {
- return selector + '{' + stack.join(';') + '}';
- }
- return stack.join(';');
- }
- function loadStoryPathName() {
- storyStorageId = getStoryStorageId()
- }
- function getStoryStorageId() {
- if (storyStorageId) return storyStorageId;
- try {
- return getMapUrl();
- } catch(e) {
- return window.location.pathname;
- }
- }
- function getMapUrl() {
- if (pageType == "map") return window.location.pathname;
- if (pageType != "chapter") {
- const errorMsg = "Unrecognizable page type"
- console.error(errorMsg);
- throw new Error(errorMsg);
- }
- const mapElement = [...document.querySelectorAll('a')].find((item) => item.text.includes('Story Map') || item.href.includes('/map'))
- const location = new URL(mapElement.href)
- return location.pathname
- }
- function foldTopmostReadChapters() {
- let ignoreList = []
- const els = Array.from(document.querySelectorAll(".story-map-chapter"))
- if (chapterDataArray.length != els.length) return false;
- for (const i in chapterDataArray) {
- if (i == 0) continue
- if (els[i].classList.contains('unread') === false) {
- if (binarySearch(i, ignoreList) == -1) {
- console.log('success at i = ', i);
- let toggle = els[i].querySelector('.js-collapsable');
- if (toggle) toggle.click();
- }
- ignoreList = ignoreList.concat(chapterDataArray[i].children)
- }
- }
- }
- function foldTopmostUnreadChapters() {
- let ignoreList = []
- const els = Array.from(document.querySelectorAll(".story-map-chapter"))
- if (chapterDataArray.length != els.length) return false;
- for (const i in chapterDataArray) {
- if (i == 0) continue
- if (els[i].classList.contains('unread')) {
- if (binarySearch(i, ignoreList) == -1) {
- console.log('success at i = ', i);
- let toggle = els[i].querySelector('.js-collapsable');
- if (toggle) toggle.click();
- }
- ignoreList = ignoreList.concat(chapterDataArray[i].children)
- }
- }
- console.log('ignoreList leng',ignoreList.length);
- }
- function binarySearch(value, list) {
- return list.find(a=>a==value) ? 1 : -1;
- }
- function prevMapShow() {
- let container = document.querySelector('.prevMapContainer')
- if (container) container.classList.toggle('tw-hidden');
- }
- async function prevMapRender() {
- let storyStorageId = getStoryStorageId()
- if (!chapterDataArray) {
- await attemptLoadChapterArrayFromCache();
- }
- if (!chapterData && chapterDataArray) {
- chapterData = await getChapterData(storyStorageId);
- }
- if (!chapterData) {
- let navContainer = document.querySelector('.rd-story-navigation');
- if (navContainer) {
- 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>');
- }
- return false;
- }
- if (document.querySelector('.prevMapContainer') === null) {
- let navContainer = document.querySelector('.rd-story-navigation');
- if (navContainer) {
- 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>');
- }
- }
- drawParentTree()
- }
- function drawThisChapter() {
- let container = document.querySelector('.prevMapContainer')
- if (container && chapterData) container.insertAdjacentHTML('beforeend', '<div class="prevMap_chapter prevMap_currentChapter"><a href="'+chapterData.url+'">'+chapterData.title+'</a></div>')
- }
- function drawParentTree() {
- let container = document.querySelector('.prevMapContainer')
- if (!container) return;
- container.innerHTML = '';
- let chapter = chapterData
- while (chapter && chapter.parent !== null && chapter.parent !== undefined) {
- 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>')
- chapter = chapterDataArray[chapter.parent]
- }
- }
- async function prevMapInit() {
- let targetNav = document.querySelector('.rd-story-navigation__links');
- if (targetNav) {
- 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>');
- let prevMapButton = document.querySelector('.prevMap_button')
- prevMapButton.addEventListener('click', function(e) {
- e.preventDefault();
- prevMapShow()
- });
- await prevMapRender()
- }
- }
- function getSuperParentGroupSortedByNew() {
- if (unsafeWindow.parentChapters) return unsafeWindow.parentChapters;
- chapterDataArray.forEach((c, i) => { c.id = i; })
- let parents = chapterDataArray.filter(c => c.parent == 0)
- let parentIds = parents.map(c => c.id)
- function computeSuperParent(chapter) {
- if (chapter.superParent) return
- if (parentIds.includes(chapter.id)) {
- chapter.superParent = chapter.id;
- return
- }
- if (parentIds.includes(chapter.parent)) {
- chapter.superParent = chapter.parent
- return chapter.superParent
- }
- chapter.superParent = getSuperParent(chapterDataArray[chapter.parent])
- }
- function getSuperParent(chapter) {
- if (!chapter.superParent) computeSuperParent(chapter)
- return chapter.superParent
- }
- chapterDataArray.slice(1).forEach(computeSuperParent)
- unsafeWindow.parentChapters = parents.map(p => {
- return {
- p: p,
- c: chapterDataArray.filter(c => c.superParent === p.id)
- }
- })
- function dateComparison(a, b) {
- let aDate = Date.parse(a.date)
- if (! aDate) aDate = 1;
- let bDate = Date.parse(b.date)
- if (! bDate) bDate = 1;
- return bDate - aDate;
- }
- unsafeWindow.parentChapters.forEach(g => g.c.sort((a, b) => { return dateComparison(a, b); }))
- unsafeWindow.parentChapters.sort((a, b) => { return dateComparison(a.c[0], b.c[0]); })
- return unsafeWindow.parentChapters
- }
- function getMapAndRegenerateViaAjax() {
- console.log("!! generating map");
- const url = getMapUrl() + '.json'
- let ajaxChapters = []
- function recursiveGetChaptersFromAjax(page) {
- let currentUrl = url + (page > 1 ? `?page=${page}` : '');
- unsafeWindow.$.get({
- url: currentUrl,
- dataType: "json",
- headers: {
- "Accept": "application/json, text/javascript, */*; q=0.01",
- "X-CSRF-TOKEN": unsafeWindow.Chyoa.csrf_token,
- }
- }).done(function (response) {
- ajaxChapters = ajaxChapters.concat(Object.values(response.data.chapters))
- if (response.data.hasMorePages) {
- recursiveGetChaptersFromAjax(page + 1)
- } else {
- chapterDataArray = createStoryMapAjax(ajaxChapters)
- unsafeWindow.chapterDataArray = chapterDataArray
- buildPathNumericalCodes();
- countMapChapters();
- GM.setValue(getStoryStorageId(), JSON.stringify(chapterDataArray))
- if (showPathTracker) { updatePathTrackerDropdown(); }
- }
- }
- )
- }
- recursiveGetChaptersFromAjax(1);
- function createChapterObjFromAjax(obj) {
- const chapter = { parent: null, children: [] }
- chapter.title = obj.title
- chapter.url = obj.url
- chapter.author = obj.author
- chapter.date = obj.created_at
- chapter.margin = obj.indent
- if (chapter.views) {
- chapter.views = parseInt(obj.views.split(",").join("")) || 0;
- chapter.likes = parseInt(obj.likes) || 0;
- chapter.comments = parseInt(obj.comments) || 0;
- chapter.isLinkChapter = 0;
- } else {
- chapter.views = null;
- chapter.likes = null;
- chapter.comments = null;
- chapter.isLinkChapter = 1;
- }
- return chapter;
- }
- function createStoryMapAjax(chapters) {
- let prevParentI = -1;
- const finalList = [];
- chapters.forEach((el, chapterIndex) => {
- const chapterObj = createChapterObjFromAjax(el);
- finalList[chapterIndex] = chapterObj;
- if (chapterIndex == 0) {
- prevParentI = 0;
- return;
- }
- const currElMargin = chapterObj.margin
- const prevElMargin = finalList[chapterIndex-1].margin
- if (prevElMargin < currElMargin) {
- chapterObj.parent = parseInt(chapterIndex - 1);
- finalList[chapterIndex - 1].children.push(parseInt(chapterIndex));
- prevParentI = chapterIndex - 1;
- return;
- }
- if (prevElMargin == currElMargin) {
- chapterObj.parent = prevParentI;
- finalList[prevParentI].children.push(chapterIndex);
- return;
- }
- const selfMargin = chapterObj.margin;
- for (let parentIndex = chapterIndex - 1; parentIndex >= 0 && parentIndex !== null; parentIndex = finalList[parentIndex].parent) {
- if (finalList[parentIndex].margin < selfMargin) {
- const actualParentI = parentIndex;
- chapterObj.parent = actualParentI;
- finalList[actualParentI].children.push(chapterIndex);
- prevParentI = actualParentI;
- break;
- }
- }
- })
- return finalList;
- }
- }
Advertisement
Add Comment
Please, Sign In to add comment