-
-
Save IntermittentlyRupert/a709e8c9908d55a44e24a1c44786509c to your computer and use it in GitHub Desktop.
Anilist Unread Chapters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Anilist Unread Highlighter | |
| // @namespace http://tampermonkey.net/ | |
| // @version 0.1 | |
| // @description Add an Unread Chapters list to Anilist (requires MAL-Sync) | |
| // @author IntermittentlyRupert | |
| // @match https://anilist.co/user/*/mangalist* | |
| // @icon https://www.google.com/s2/favicons?domain=anilist.co | |
| // @grant GM_addStyle | |
| // @grant GM_log | |
| // ==/UserScript== | |
| (async function () { | |
| "use strict"; | |
| // ======================= | |
| // UTIL | |
| // ======================= | |
| /** | |
| * @param {string | Error} msg | |
| */ | |
| function log(msg) { | |
| GM_log(`*** HIGHLIGHTER: ${msg}`); | |
| } | |
| /** | |
| * @param {HTMLElement} element | |
| * @param {string} selector | |
| * @returns {string} | |
| */ | |
| function text(element, selector) { | |
| return element.querySelector(selector)?.innerText?.trim() ?? "???"; | |
| } | |
| /** | |
| * @param {number} ms | |
| * @returns {Promise<void>} | |
| */ | |
| function delay(ms) { | |
| return new Promise((resolve) => setTimeout(resolve, ms)); | |
| } | |
| let __showingUnread = false; | |
| function isShowingReading() { | |
| return ( | |
| window.location.pathname.endsWith("/mangalist/Reading") && | |
| !__showingUnread | |
| ); | |
| } | |
| function isShowingUnread() { | |
| return ( | |
| window.location.pathname.endsWith("/mangalist/Reading") && __showingUnread | |
| ); | |
| } | |
| function getReadingListButton() { | |
| return Array.from( | |
| document.querySelectorAll( | |
| ".filters > .filter-group:nth-child(1) > span:not(.group-header)" | |
| ) | |
| ).find((el) => el.innerText.trim().startsWith("Reading")); | |
| } | |
| // ======================= | |
| // DOM MONITORING | |
| // ======================= | |
| /** | |
| * @param {number} prevScrollMax | |
| * @param {number} expiry | |
| * @returns {Promise<boolean>} whether more data was loaded before `expiry` | |
| */ | |
| async function waitForMoreData(prevScrollMax, expiry = Date.now() + 2000) { | |
| await delay(100); | |
| if (window.scrollMaxY > prevScrollMax) { | |
| return true; | |
| } | |
| if (Date.now() > expiry) { | |
| return false; | |
| } | |
| return waitForMoreData(prevScrollMax, expiry); | |
| } | |
| /** | |
| * @param {number} timeout | |
| * @returns {Promise<void>} a promise that resolves when the DOM has been stable | |
| * for `timeout` ms. | |
| */ | |
| async function waitForDomStable(timeout = 5000) { | |
| return new Promise((resolve) => { | |
| const done = () => { | |
| ob.disconnect(); | |
| resolve(); | |
| }; | |
| let stableTimeout = setTimeout(done, timeout); | |
| const ob = new MutationObserver((mutations) => { | |
| if (mutations.length > 0) { | |
| clearTimeout(stableTimeout); | |
| stableTimeout = setTimeout(done, timeout); | |
| } | |
| }); | |
| ob.observe(document, { | |
| childList: true, | |
| attributes: true, | |
| subtree: true, | |
| }); | |
| }); | |
| } | |
| // ======================= | |
| // DOM MANIPULATION | |
| // ======================= | |
| async function showUnread() { | |
| log("showing unread START"); | |
| if (!isShowingReading() && !isShowingUnread()) { | |
| throw new Error("not on reading list"); | |
| } | |
| __showingUnread = true; | |
| document | |
| .querySelector(".highlighter--button-unread") | |
| .classList.add("active"); | |
| getReadingListButton().classList.remove("active"); | |
| const domStablePromise = waitForDomStable(); | |
| while (true) { | |
| log("scrolling..."); | |
| let prevScrollMax = window.scrollMaxY; | |
| window.scroll({ top: prevScrollMax }); | |
| if (!(await waitForMoreData(prevScrollMax)) || !isShowingUnread()) { | |
| log("stopping scroll"); | |
| break; | |
| } | |
| } | |
| await domStablePromise; | |
| log("dom is stable"); | |
| if (!isShowingUnread()) { | |
| // user has clicked off the unread list - just bail | |
| log("bailing from showUnread"); | |
| return; | |
| } | |
| window.scroll({ top: 0 }); | |
| for (const row of document.querySelectorAll(".entry.row")) { | |
| row.classList.remove("highlighter--read"); | |
| row.classList.remove("highlighter--unread"); | |
| const title = text(row, ".title"); | |
| const progress = text(row, ".progress:not(.progress-volumes)"); | |
| const m = progress.match(/(\d+)(\/\d+)?\s*\[(\d+)\]/); | |
| const hasUnread = m && Number(m[1]) < Number(m[3]); | |
| log(`${title} --- ${progress}${hasUnread ? " --- HAS UNREAD" : ""}`); | |
| row.classList.add( | |
| hasUnread ? "highlighter--unread" : "highlighter--read" | |
| ); | |
| } | |
| log("showing unread DONE"); | |
| } | |
| function removeUnread() { | |
| log("removing unread START"); | |
| __showingUnread = false; | |
| document | |
| .querySelector(".highlighter--button-unread") | |
| .classList.remove("active"); | |
| if (isShowingReading()) { | |
| getReadingListButton().classList.add("active"); | |
| } | |
| for (const row of document.querySelectorAll(".entry.row")) { | |
| row.classList.remove("highlighter--read"); | |
| row.classList.remove("highlighter--unread"); | |
| } | |
| log("removing unread DONE"); | |
| } | |
| // ======================= | |
| // EVENT LISTENERS | |
| // ======================= | |
| async function onListButtonClicked() { | |
| try { | |
| log("list button clicked"); | |
| await waitForDomStable(1000); | |
| removeUnread(); | |
| } catch (e) { | |
| log(e); | |
| } | |
| } | |
| async function onUnreadButtonClicked() { | |
| try { | |
| log("unread button clicked"); | |
| if (isShowingUnread()) { | |
| log("already on unread"); | |
| return; | |
| } | |
| if (!isShowingReading()) { | |
| getReadingListButton().click(); | |
| await waitForDomStable(1000); | |
| } | |
| await showUnread(); | |
| } catch (e) { | |
| log(e); | |
| } | |
| } | |
| // ======================= | |
| // INITIALIZATION | |
| // ======================= | |
| async function waitForListButtons() { | |
| let readingListButton = getReadingListButton(); | |
| while (!readingListButton) { | |
| await delay(100); | |
| readingListButton = getReadingListButton(); | |
| } | |
| } | |
| async function spyOnListButtons() { | |
| const lists = document.querySelectorAll( | |
| ".filters > .filter-group:nth-child(1) > span:not(.group-header)" | |
| ); | |
| for (const list of lists) { | |
| list.addEventListener("click", () => { | |
| onListButtonClicked(); | |
| }); | |
| } | |
| } | |
| function addUnreadList() { | |
| const unreadButton = document.createElement("span"); | |
| const readingButton = getReadingListButton(); | |
| for (const name of readingButton.getAttributeNames()) { | |
| if (name !== "class") { | |
| unreadButton.setAttribute(name, readingButton.getAttribute(name)); | |
| } | |
| } | |
| unreadButton.classList.add("highlighter--button-unread"); | |
| unreadButton.innerHTML = " ➤ Unread Chapters"; | |
| unreadButton.addEventListener("click", () => { | |
| onUnreadButtonClicked(); | |
| }); | |
| readingButton.after(unreadButton); | |
| } | |
| try { | |
| log("waiting for page to be ready"); | |
| await waitForListButtons(); | |
| log("init START"); | |
| GM_addStyle(` | |
| .entry.row.highlighter--read { | |
| display: none !important; | |
| } | |
| `); | |
| await spyOnListButtons(); | |
| addUnreadList(); | |
| log("init DONE"); | |
| if (isShowingUnread()) { | |
| log("already on unread"); | |
| await waitForDomStable(5000); | |
| await showUnread(); | |
| } | |
| } catch (e) { | |
| log(e); | |
| } | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment