您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Counts and displays how many chapters each branch has. Has commands on the menu
当前为
// ==UserScript== // @name CHYOA Map Chapter Counter // @version 1.0.1 // @description Counts and displays how many chapters each branch has. Has commands on the menu // @author sllypper // @namespace https://greasyfork.org/en/users/55535-sllypper // @homepage https://greasyfork.org/en/users/55535-sllypper // @match *://chyoa.com/story/*/map* // @icon https://chyoa.com/favicon.png // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_listValues // @grant GM_registerMenuCommand // ==/UserScript== /* How to use: * Load the entire map by moving to the bottom, then click the Regenerate button. * There's commands on the Right-Click menu or clicking on the thingie you used to load the script * The script only counts and draws the currently loaded chapters. * If the story has more chapters, the user needs to Redraw or Regenrate for the chapters loaded afterwards. * Move to the bottom of the page to load more chapters. What's happening: * When you load a map page, the script attempts to retrieve the saved map data from cache. * If it doesn't find the data, it generates and stores in the cache. * Then it draw the counters on the screen. */ // runs automatically when opening a new map or not let runAutomatically = true // shows the floating button on the bottom-right of the page let showFloatingButton = true /************************/ GM_registerMenuCommand("Regenerate", generateDraw, "r"); GM_registerMenuCommand("Redraw counters from cache", drawChildrenCounter, "d"); GM_registerMenuCommand("Delete cache", deleteCache, "c"); GM_registerMenuCommand("Delete all cache", deleteAllCache, "a"); /************************/ // exposing storyMap for the user to play with the data window.storyMap; if (!attemptLoadCache()) { // no map data on cache if (runAutomatically) { generateDraw() } } else { // got map data from cache drawChildrenCounter() } if (showFloatingButton) { document.body.appendChild(btnLoad()) } addStyle(".title-wrapper > .children { padding-right: 8px; }") /************************/ function generateDraw() { generateMap() drawChildrenCounter() } function createChildrenElement(childrenCount) { 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(" "+childrenCount) child.appendChild(count) return child } function addStyle(css) { var style = document.createElement('style'); document.head.appendChild(style); style.textContent = css; }; function attemptLoadCache() { const cachedMap = GM_getValue(window.location.pathname) if (cachedMap) { console.log("!! got map from cache"); window.storyMap = JSON.parse(cachedMap); // console.log(window.storyMap[0]) return true } console.log("!! map not found in cache"); return false } function deleteCache() { GM_deleteValue(window.location.pathname) } function deleteAllCache() { GM_listValues().forEach((value) => { GM_deleteValue(value) }) } function readOrGenerateMap() { if (! attemptLoadCache()) generateMap() // const entries = exportAllLevel1Chapters(); return } function generateMap() { console.log("!! generating map"); window.storyMap = getParsedList() console.log("storyMap length = ", window.storyMap.length); console.log("!! counting children"); countMapChildren(); console.log("!! done counting children"); // cache it GM_setValue(window.location.pathname, JSON.stringify(window.storyMap)) console.log("Map available as a javascript object on `window.storyMap`") } function drawChildrenCounter() { let list = document.querySelectorAll(".title-wrapper"); // let list = document.getElementsByClassName("story-map-content"); // if (list == null || !list) return; // list = Array.from(list[0].children); list.forEach((elem, i) => { let existingChildren = elem.querySelector('.children') if (existingChildren) { existingChildren.remove() } let child = createChildrenElement(window.storyMap[i].childrenCount) let page = elem.querySelector('.page') elem.insertBefore(child, page) // let title = elem.querySelector('.title') // elem.insertBefore(child, title) // let controls = elem.querySelector('.controls-wrapper') // controls.appendChild(child) }) } /** * counts and assigns the children count of all elements */ function countMapChildren() { for (const node of window.storyMap) { if (node.children.length !== 0) continue; node.childrenCount = 0; let next = node.parent != null ? node.parent : -1; // rise through the leaf until it can't while (next != -1) { //console.log("from", node.id, "to", next); next = countChildNodesOf(next); } // then continue to the next node } // done } /** * Counts and assigns the childrenCount of the node at that index * Aborts returning -1 if it already has been counted * or can't because one of its children doesn't have their childrenCount * * @param {number} currIndex - The index of the node * @returns {number} The index of the next node. -1 to abort */ function countChildNodesOf(currIndex) { let nextIndex = -1; const currentNode = window.storyMap[currIndex]; if (currentNode.childrenCount != undefined) { // this node was already been processed // abort return nextIndex; } // sum the counts of its children const childrenCountSum = getChildrenCountSum(currentNode); if (childrenCountSum === -1) { // one or more children haven't been processed yet // abort return nextIndex; } // on successful sum currentNode.childrenCount = childrenCountSum + currentNode.children.length; nextIndex = currentNode.parent !== null ? currentNode.parent : -1; return nextIndex; } /** * Sums the childrenCount of all children of the attribute node */ function getChildrenCountSum(node) { return node.children.reduce((acc, curr) => { const currentNode = window.storyMap[curr]; if (currentNode.childrenCount == undefined) { return -1; } return acc !== -1 ? acc + currentNode.childrenCount : acc; }, 0); } // gets html dom element // return parsed element as an object function parseEl(el, id) { const pel = {}; pel.id = parseInt(id); 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 : ""; // is always wrong for loaded chapters. It resets pel.page = el.querySelector(".page").textContent; // Sometimes the date is empty, but there's no issue here 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 [ parsedElement(), (...) ] // where every element has its parent and children noted function getParsedList() { // 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 prevParentI = -1; const finalList = []; for (const i in list) { // console.log(" Processing Chapter", i); const el = list[i]; // parse el and add it to the final list const parsedEl = parseEl(el, i); finalList[i] = parsedEl; // console.log(parsedEl) // 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; } // check margins const currElMargin = parsedEl.margin const prevElMargin = finalList[i-1].margin // check if el is child of prev el if (prevElMargin < currElMargin) { // prev el is parent parsedEl.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; } // check if el is sibling of prev el if (prevElMargin == currElMargin) { // they share the same parent // prevParent is parent parsedEl.parent = prevParentI; // add this el as child of prevParent finalList[prevParentI].children.push(i); continue; } // then el must be the "uncle" of prev el // prevElMargin > currElMargin /* ALTERNATIVE METHOD */ // use a loop go back through the parents from the previous node // to find the first element with margin smaller than self const selfMargin = parsedEl.margin; for (let j = i - 1; j >= 0; j = finalList[j].parent) { if (finalList[j].margin < selfMargin) { // found the parent: j const actualParentI = j; parsedEl.parent = actualParentI; // add this el as child of actual parent finalList[actualParentI].children.push(parsedEl.id); // set prev parent to actual parent prevParentI = actualParentI; break; } } continue; /* END OF ALTERNATIVE METHOD */ // deleted old method of searching parent when uncle } 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) { let stack = [], key; for (key in obj) { if (obj.hasOwnProperty(key)) { stack.push(key + ':' + obj[key]); } } 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()); }