您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Sleazy Fork is available in English.
Counts and displays how many chapters each branch has inside on the map screen and when choosing your next chapter
// ==UserScript== // @name Chapter counter for Map and Next Chapter choice // @version 3.0.2 // @description Counts and displays how many chapters each branch has inside on the map screen and when choosing your next chapter // @author sllypper // @namespace https://greasyfork.org/en/users/55535-sllypper // @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 // 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); /************************/ (async() => { GM.registerMenuCommand("Get map & regenerate (experimental)", getMapAndRegenerateViaAjax, "k" ) 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(".title-wrapper > .children { padding-right: 8px; }") if (showTogglerButton) { collapsibator() } } // /chapter/* and /story/* pages if (pageType == "chapter") { await showChapterCountOnChapterChoices() showChapterDate() // previous chapters map tree on clicking the link await prevMapInit() } 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() } 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"); countMapChapters(); console.log("!! done counting the children"); // cache it GM.setValue(getStoryStorageId(), JSON.stringify(chapterDataArray)) } /** * counts and assigns the children count of all elements */ function countMapChapters() { for (const chapter of chapterDataArray) { // find the leaves of the tree if (chapter.children.length !== 0) continue; chapter.chapterCount = 0; let next = chapter.parent != null ? chapter.parent : -1; // rise through the branch until it can't while (next != -1) { //console.log("from", chapter.id, "to", next); next = countChildChaptersOf(next); } // then continue to the next chapter childless chapter } // done } /** * Counts and assigns the chapterCount of the node at that index * Aborts returning -1 if it already has been counted * or can't because one of its children hasn't been counted yet * * @param {number} currIndex - The index of the node * @returns {number} The index of the next node. -1 to abort */ function countChildChaptersOf(currIndex) { let nextIndex = -1; // if (currIndex > chapterDataArray.length) console.log('currIndex > chapterDataArray.length', currIndex, chapterDataArray.length); const currentNode = chapterDataArray[currIndex]; if (currentNode.chapterCount != undefined) { // this node was already been processed // abort return nextIndex; } // sum the counts of its children const chapterCountSum = sumChildrenChaptersCount(currentNode); if (chapterCountSum === -1) { // one or more children haven't been processed yet // abort return nextIndex; } // on successful sum currentNode.chapterCount = chapterCountSum + currentNode.children.length; nextIndex = currentNode.parent !== null ? currentNode.parent : -1; return nextIndex; } /** * Sums the chapterCount of all children of the attribute node */ 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(".title-wrapper"); 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) { // redraw existingChildren.remove() } let child = createChildrenElement(chapterDataArray[i].chapterCount) let page = elem.querySelector('.page') elem.insertBefore(child, page) }) } // receives html dom element // returns parsed element as an object function createChapterObj(el) { const pel = {}; let tempEl = el.querySelector(".title"); pel.title = tempEl.textContent; pel.url = tempEl.href; // sometimes the author is empty and there's no <a> inside it tempEl = el.querySelector(".username > a"); pel.author = tempEl ? tempEl.textContent : ""; // page is completely unreliable for chapters loaded afterwards. It resets on loading // pel.page = el.querySelector(".page").textContent; // Sometimes the date is empty, but there's no issue here // console.log(el); pel.date = el.querySelector(".date").textContent; pel.parent = null; pel.children = []; // pel.linksTo = null; pel.margin = parseInt(el.style["margin-left"]); // link chapters don't have views, likes, or comments // so find out if the chapter is a link chapter or not const viewsEl = el.querySelector(".views"); if (viewsEl == null) { pel.views = null; pel.likes = null; pel.comments = null; pel.isLinkChapter = 1; return pel; } pel.views = parseInt(viewsEl.textContent.split(",").join("")) || 0; pel.likes = parseInt(el.querySelector(".likes").textContent) || 0; pel.comments = parseInt(el.querySelector(".comments").textContent) || 0; pel.isLinkChapter = 0; return pel; } // final list like [ chapterObj, (...) ] // where every element has its parent and children noted function createStoryMap() { // temporary list, to get the DOM element from the page // let list = document.getElementsByClassName("story-map-content"); // if (list == null || !list) return; // list = Array.from(list[0].children); let chapterElementArray = Array.from(document.querySelectorAll(".story-map-chapter")) let prevParentI = -1; const finalList = []; chapterElementArray.forEach((el, i) => { // for (const i in chapterElementArray) { // console.log("- Processing Chapter", i); // const el = chapterElementArray[i]; // parse el and add it to the final list const chapterObj = createChapterObj(el); finalList[i] = chapterObj; // console.log(chapterObj); // now we find the parent of el // before checking margin // check if it's the first element of the list if (i == 0) { prevParentI = 0; // continue; // when using a for loop return; } // check margins const currElMargin = chapterObj.margin const prevElMargin = finalList[i-1].margin // check if el is child of prev el if (prevElMargin < currElMargin) { // prev el is parent chapterObj.parent = parseInt(i - 1); // add this el as child of prev element finalList[i - 1].children.push(parseInt(i)); // set prev parent to prev element prevParentI = i - 1; // continue; // when using a for loop return; } // check if el is sibling of prev el if (prevElMargin == currElMargin) { // they share the same parent // prevParent is parent chapterObj.parent = prevParentI; // add this el as child of prevParent finalList[prevParentI].children.push(i); // continue; // when using a for loop return; } // then el must be the "uncle" of prev el // prevElMargin > currElMargin // use a loop go back through the parents from the previous node // to find the first element with margin smaller than self const selfMargin = chapterObj.margin; for (let j = i - 1; j >= 0; j = finalList[j].parent) { if (finalList[j].margin < selfMargin) { // found the parent: j const actualParentI = j; chapterObj.parent = actualParentI; // add this el as child of actual parent // finalList[actualParentI].children.push(chapterObj.id); finalList[actualParentI].children.push(i); // set prev parent to actual parent prevParentI = actualParentI; break; } } // } // when using a for loop }) return finalList; } // button stuff function createButton(text, action, styleStr) { let button = document.createElement('button'); button.textContent = text; button.onclick = action; button.setAttribute('style', styleStr || ''); return button; }; 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 btnLoadCss() { return toStyleStr({ 'position': 'fixed', 'bottom': 0, 'right': 0, 'padding': '2px', 'margin': '0 10px 10px 0', 'color': '#333', 'background-color': 'rgb(246, 245, 244)', 'z-index': '9999999999' }) } function btnLoad() { return createButton('Regenerate', function() { generateDraw() // this.remove(); }, btnLoadCss()); } /* Depth Toggler */ function collapsibator() { const input = document.createElement('input') input.defaultValue = 1 input.setAttribute('id', 'toggler-input') input.setAttribute('style', toStyleStr({ // 'padding': '2px', 'color': '#333', 'width': '30px' // 'display': 'inline-block' })) const button = document.createElement('button') button.setAttribute('id', 'toggler-btn') button.textContent = 'Toggle' button.setAttribute('style', toStyleStr({ 'padding': '2px', 'color': '#333', 'background-color': 'rgb(246, 245, 244)', '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': '2px', 'z-index': '9999999999' })) cont.appendChild(input) cont.appendChild(button) document.body.appendChild(cont) } // toggle all collapsibles from depht level "level" function toggleCollapsibleLevel(level) { const chapters = Array.from(document.getElementsByClassName("story-map-chapter")); if (chapters == null) return; // if (!chapterDataArray.length) { console.error('chapterDataArray is undefined'); return; } if (!chapterDataArray) { console.error('chapterDataArray is undefined'); return; } const firstMargin = parseInt(chapters[0].style['margin-left']) const marginGap = parseInt(chapters[1].style['margin-left']) - firstMargin; for (let i = 0; i < chapterDataArray.length; i++) { // if (st.margin == (level*marginGap + firstMargin)) { if (chapterDataArray[i].margin == (level*marginGap + firstMargin)) { // toggle it const btn = chapters[i].querySelector('.btn.btn-link.collapsable.js-collapsable') if (btn) btn.click() // maybe will use this in the future for better performance??? // wasn't able to figure it out // expand // let clazz = Array.from(chapters[i].getAttribute('class')).split(' ').filter(c=>c!="hidden") // chapters[i].setAttribute('class', clazz) // clazz = Array.from(chapters[i].querySelector(".js-collapsable > i").getAttribute('class')).split(' ').filter(c=>c!="bt-minus"&&c!="bt-plus") // clazz.push('bt-minus') // chapters[i].querySelector(".js-collapsable > i").setAttribute('class', clazz) // collapse // if (chapterDataArray[i].margin == (level*marginGap + firstMargin)) { // let el = chapters[i].querySelector(".js-collapsable > i") // let clazz = el.getAttribute('class').split(' ').filter(c=>c!="bt-minus").join(' ') + " bt-plus" // el.setAttribute('class', clazz) // if (chapterDataArray[i].margin > (level*marginGap + firstMargin)) { // let el = chapters[i] // let clazz = el.getAttribute('class') + " hidden" // el.setAttribute('class', clazz) } } // }) } function getPageType() { let url = window.location.pathname // // console.log(url); if (url.search(/\/story\/.*\/map/) >= 0) { return "map" } if (url.search(/\/chapter\//) >= 0) { return "chapter" } if (url.search(/\/story\/.*\.[0-9]+$\/?/) >= 0) { // first chapter of story return "chapter" } } async function storyIsInTheCache(storyStorageId) { return ((await GM.listValues()).indexOf(storyStorageId) >= 0) } // get story url // find it on cache // get chapter url // compare with chapters listed async function showChapterCountOnChapterChoices() { let storyStorageId = getStoryStorageId() // check if chapterData is undefined // if it is, try fetching it if (!chapterData) { //fetch story data array first if (!chapterDataArray) if (! await attemptLoadChapterArrayFromCache()) return false; // fetch story data chapterData = await getChapterData(storyStorageId) if (!chapterData) return console.error('Story found but Chapter not found. Please regenerate the map.'); } let nextChapterIndexList = chapterData.children // get chapter choices let chapterChoices = Array.from(document.querySelectorAll(".question-content a")) // Remove "Add a new chapter" link from the list (if the story is not private) // its link ends with "/new" if (chapterChoices[chapterChoices.length-1].href.search(/\/new$/) > -1) { chapterChoices.pop() } // prepare Count Number css style 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") // return false } // story is on cache as chapterDataArray // let chapterDataArray = JSON.parse(await GM.getValue(storyStorageId)) // getting the chapter url if we're in /story/ let chapterUrl = window.location.href if (chapterUrl.search(/\/story\//) > -1) { let els = document.querySelectorAll('.controls-left a') let chapterNum = els[els.length-1].href.match(/\d+\/?$/)[0] chapterUrl = "https://chyoa.com/chapter/Introduction."+chapterNum; } // console.log(chapterDataArray, chapterDataArray[1].url, chapterUrl); return chapterDataArray.find(c=>c.url==chapterUrl) } // Show chapter date under its Title // assuming pageType == "chapter" function showChapterDate() { if (!chapterData) return false; let date = document.createElement('div') date.textContent = chapterData.date document.querySelector('.meta').append(date) } // crazy css 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', }, '.question-content .chapterCount') addStyleHead(style) } function applyAlternativeCSS() { let style = toStyleStr({ 'position': 'absolute', 'left': '-45px', 'text-align': 'right', 'width': '40px', 'padding': '11px 0', 'top': '0', }, '.question-content .chapterCount') + toStyleStr({ 'position': 'relative', }, '.question-content a') addStyleHead(style) } function drawLinkChapterCount(el, chapterObj, cssStyle = 'default') { const chapterCount = chapterObj.chapterCount const isLinkChapter = chapterObj.isLinkChapter let span = document.createElement('SPAN') if (isLinkChapter) { let icon = document.createElement('i') icon.setAttribute('class', 'btb bt-external-link') el.insertAdjacentElement('afterbegin',icon) 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 } // default & carzy markup 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) // skip styling if cssStyle is set to 'crazy' 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 loadStoryPathName() { storyStorageId = getStoryStorageId() } function getStoryStorageId() { if (storyStorageId) return storyStorageId; return getMapUrl(); } function getMapUrl() { if (pageType == "map") return window.location.pathname; if (pageType != "chapter") { const errorMsg = "Unrecognizable page type" console.error(errorMsg); throw new Error(errorMsg); } // pageType is chapter const mapElement = [...document.querySelectorAll('.controls-left a')].find((item) => item.text.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; // iterate all chapters // fold all read chapters not in the ignore list // put its children in the ignore list // put every ignored chapter children in the ignored list // console.log(chapterDataArray); for (const i in chapterDataArray) { if (i == 0) continue // console.log('in for'); // check if chapter read && i different from 0 if (els[i].classList.length == 1) { // console.log('unread chapter found'); // if (binarySearch(i, ignoreList) == -1 && i != 0) { if (binarySearch(i, ignoreList) == -1) { console.log('success at i = ', i); // console.log('first unread not in ignore list, fold'); els[i].querySelector('.btn').click() } // console.log('add children to ignored list'); 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.length != 1) { if (binarySearch(i, ignoreList) == -1) { console.log('success at i = ', i); els[i].querySelector('.btn').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; let first = 0; //left endpoint let last = list.length - 1; //right endpoint let position = -1; let found = false; let middle; if (value < 100) console.log('search', value, list); while (found === false && first <= last) { middle = Math.floor((first + last)/2); if (list[middle] == value) { found = true; position = middle; } else if (list[middle] > value) { //if in lower half last = middle - 1; } else { //in in upper half first = middle + 1; } } return position; } // TODO // index to remain in cache forever containing a story index with the number of chapters and other story stats // update index if the generated data has more chapters than what's on the index // detect branches (chapters where child chapters have many chapters) and note them down // on chapter pages, check if it belongs to any recognizable branch and print its name on the page // fold all topmost read chapters // prev chapters map // let prevMapContainer; function prevMapShow() { let container = document.querySelector('.prevMapContainer') // if (container === null) { // container = prevMapRender() // container = document.querySelector('.prevMapContainer') // } container.classList.toggle('show'); // prevMapContainer.classList.toggle('show'); } async function prevMapRender() { let storyStorageId = getStoryStorageId() // check if chapterData is undefined // if it is, try fetching it if (!chapterData) { //fetch story data array first // if (!chapterDataArray) if (!attemptLoadChapterArrayFromCache()) { if (!chapterDataArray) { if (! await attemptLoadChapterArrayFromCache()) { document.querySelector('.question').insertAdjacentText('afterend', 'Map data not found. Please regenerate the map.') return false; } } // fetch story data chapterData = getChapterData(storyStorageId) if (!chapterData) { document.querySelector('.question').insertAdjacentText('afterend', 'Unable to find Chapter in the story data. Please regenerate the mapp.') // return console.error('Unable to find Chapter in the story data. Please regenerate the map.'); return false; } } if (document.querySelector('.prevMapContainer') === null) { // if (prevMapContainer === null) { document.querySelector('.question').insertAdjacentHTML('beforeend', '<div class="prevMapContainer"></div>') // prevMapContainer = document.querySelector('.prevMapContainer') } // drawThisChapter() drawParentTree() } function drawThisChapter() { let container = document.querySelector('.prevMapContainer') container.insertAdjacentHTML('beforend', '<div class="prevMap_chapter prevMap_currentChapter"><a href="'+chapterData.url+'">'+chapterData.title+'</a></div>') } function drawParentTree() { let container = document.querySelector('.prevMapContainer') let chapter = chapterData // let chapter = chapterDataArray[chapterData.parent] while (chapter.parent != null) { container.insertAdjacentHTML('afterbegin', '<div class="prevMap_chapter"><a href="'+chapter.url+'">'+chapter.title+'</a></div>') chapter = chapterDataArray[chapter.parent] } } async function prevMapInit() { document.querySelector('.controls-left').insertAdjacentHTML('beforeend','<br class="visible-xs-inline"> <a href="#" class="prevMap_button"><i class="btb bt-sitemap"></i>Prev Chapters</a>') let prevMapButton = document.querySelector('.prevMap_button') prevMapButton.addEventListener('click', function(e) { e.preventDefault(); prevMapShow() }); await prevMapRender() let style = toStyleStr({ 'display': 'none', }, '.prevMapContainer') + toStyleStr({ 'display': 'block', }, '.prevMapContainer.show') addStyleHead(style) } 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); })) /* Date.parse(b.c[0].date) - Date.parse(a.c[0].date) */ 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 = [] console.log("!! url: "+url); function recursiveGetChaptersFromAjax(page) { let currentUrl = url + (page > 1 ? `?page=${page}` : ''); console.log("!! currentUrl: "+currentUrl); unsafeWindow.$.get({ url: currentUrl, // method: "GET", dataType: "json", headers: { "Accept": "application/json, text/javascript, */*; q=0.01", "X-CSRF-TOKEN": unsafeWindow.Chyoa.csrf_token, } }).done(function (response) { // console.log(response); ajaxChapters = ajaxChapters.concat(Object.values(response.data.chapters)) if (response.data.hasMorePages) { recursiveGetChaptersFromAjax(page + 1) } else { chapterDataArray = createStoryMapAjax(ajaxChapters) console.log("Chapter Count = ", chapterDataArray.length); unsafeWindow.chapterDataArray = chapterDataArray console.log("Map available as a javascript array on `chapterDataArray`") console.log("!! assembling hierarchy tree"); countMapChapters(); console.log("!! done counting the children"); // cache it GM.setValue(getStoryStorageId(), JSON.stringify(chapterDataArray)) } } ) } // Recursivelly get all the chapters from page 1 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; } // final list like [ chapterObj, (...) ] // where every element has its parent and children noted function createStoryMapAjax(chapters) { let prevParentI = -1; const finalList = []; chapters.forEach((el, chapterIndex) => { // parse el and add it to the final list const chapterObj = createChapterObjFromAjax(el); finalList[chapterIndex] = chapterObj; // console.log(chapterObj) // now we find the parent of el // before checking margin // check if it's the first element of the list if (chapterIndex == 0) { prevParentI = 0; // continue; // when using a for loop return; } // check margins const currElMargin = chapterObj.margin const prevElMargin = finalList[chapterIndex-1].margin // check if el is child of prev el if (prevElMargin < currElMargin) { // prev el is parent chapterObj.parent = parseInt(chapterIndex - 1); // add this el as child of prev element finalList[chapterIndex - 1].children.push(parseInt(chapterIndex)); // set prev parent to prev element prevParentI = chapterIndex - 1; // continue; // when using a for loop return; } // check if el is sibling of prev el if (prevElMargin == currElMargin) { // they share the same parent // prevParent is parent chapterObj.parent = prevParentI; // add this el as child of prevParent finalList[prevParentI].children.push(chapterIndex); // continue; // when using a for loop return; } // then el must be the "uncle" of prev el // prevElMargin > currElMargin // use a loop go back through the parents from the previous node // to find the first element with margin smaller than self const selfMargin = chapterObj.margin; for (let parentIndex = chapterIndex - 1; parentIndex >= 0; parentIndex = finalList[parentIndex].parent) { if (finalList[parentIndex].margin < selfMargin) { // found the parent: parentIndex const actualParentI = parentIndex; chapterObj.parent = actualParentI; // add this el as child of actual parent // finalList[actualParentI].children.push(chapterObj.id); finalList[actualParentI].children.push(chapterIndex); // set prev parent to actual parent prevParentI = actualParentI; break; } } // } // when using a for loop }) return finalList; } }