Skip to content

Instantly share code, notes, and snippets.

@IntermittentlyRupert
Created August 7, 2021 06:14
Show Gist options
  • Save IntermittentlyRupert/a709e8c9908d55a44e24a1c44786509c to your computer and use it in GitHub Desktop.
Save IntermittentlyRupert/a709e8c9908d55a44e24a1c44786509c to your computer and use it in GitHub Desktop.
Anilist Unread Chapters
// ==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 = "&nbsp;&nbsp;&#10148;&nbsp;&nbsp;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