Last active
March 8, 2026 03:26
-
-
Save RedHatter/081d7efe9ec89969b6a3cba049dc3444 to your computer and use it in GitHub Desktop.
Automatically adds a checkmark to editions that have already been edited and saved. Adds a button to manually add a checkmark to books.
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 Hardcover.app: Edition checkmark | |
| // @namespace https://gist.github.com/RedHatter/ | |
| // @match https://hardcover.app/* | |
| // @version 1.6 | |
| // @author Ava Johnson (ava.johnson@zohomail.com) | |
| // @description Automatically adds a checkmark to editions that have already been edited and saved. Adds a button to manually add a checkmark to books. | |
| // @license MIT | |
| // @downloadURL https://gist.github.com/RedHatter/081d7efe9ec89969b6a3cba049dc3444/raw/hardcover-app-edition-checkmark.user.js | |
| // @grant GM.getValue | |
| // @grant GM.setValue | |
| // @grant GM.deleteValue | |
| // @require https://cdn.jsdelivr.net/npm/@violentmonkey/dom@2 | |
| // ==/UserScript== | |
| function editPage(edition) { | |
| const button = document | |
| .evaluate('//button[contains(., "Update Edition")]', document, null, XPathResult.ANY_TYPE, null) | |
| .iterateNext() | |
| if (!button) return false | |
| button.addEventListener("click", () => GM.setValue(edition, true)) | |
| return true | |
| } | |
| function editionsPage() { | |
| const links = document.querySelectorAll("a[href*='/editions/']") | |
| if (!links.length) return false | |
| for (const link of links) { | |
| if (link.href.endsWith("/new")) continue | |
| const edition = link.href.substring(link.href.indexOf("/editions/") + "/editions/".length) | |
| GM.getValue(edition, false).then((checked) => { | |
| if (checked) link.parentNode.append(" ✓") | |
| }) | |
| } | |
| return true | |
| } | |
| function bookPage(slug) { | |
| const elements = document.querySelectorAll("button[aria-label='Change status']") | |
| if (!elements.length) return false | |
| for (const button of document.querySelectorAll(".greasemonkey-book-checkmark")) { | |
| button.remove() | |
| } | |
| for (let element of elements) { | |
| const button = document.createElement("button") | |
| button.append("✓") | |
| button.className = | |
| "greasemonkey-book-checkmark cursor-pointer rounded-lg active:translate-y-1 transition-all text-foreground hover:bg-tertiary p-2 mx-2" | |
| button.addEventListener("click", async () => { | |
| if (await GM.getValue(slug, false)) { | |
| GM.deleteValue(slug) | |
| button.style.opacity = "0.5" | |
| } else { | |
| GM.setValue(slug, true) | |
| button.style.opacity = "1" | |
| } | |
| }) | |
| do { | |
| element = element.parentNode | |
| } while (!(element.classList.length === 0 || element.className === "flex")) | |
| element.classList.add("flex") | |
| element.append(button) | |
| GM.getValue(slug, false).then((checked) => { | |
| if (!checked) button.style.opacity = "0.5" | |
| }) | |
| } | |
| return true | |
| } | |
| function authorPage() { | |
| const links = document.querySelectorAll("a.no-underline[href*='/books/']") | |
| if (!links.length) return false | |
| for (const link of links) { | |
| const slug = link.href.substring(link.href.indexOf("/books/") + "/books/".length) | |
| GM.getValue(slug, false).then((checked) => { | |
| if (!checked) return | |
| const span = document.createElement("span") | |
| span.append("✓") | |
| span.className = "absolute" | |
| link.parentNode.append(span) | |
| }) | |
| } | |
| return true | |
| } | |
| function searchPage() { | |
| const links = document.querySelectorAll("a[href^='/books/']") | |
| if (!links.length) return false | |
| for (const link of links) { | |
| const slug = link.href.substring(link.href.indexOf("/books/") + "/books/".length) | |
| GM.getValue(slug, false).then((checked) => { | |
| if (checked) link.querySelector("span.text-lg").append(" ✓") | |
| }) | |
| } | |
| return true | |
| } | |
| const bookPattern = /^https:\/\/hardcover\.app\/books\/(?<slug>[^/]+)(?:\/[^/]+)?$/ | |
| const authorPattern = /^https:\/\/hardcover\.app\/authors\/[^/]+$/ | |
| const searchPattern = /^https:\/\/hardcover\.app\/search\?q=[^/]+$/ | |
| const editPattern = | |
| /^https:\/\/hardcover\.app(?:\/books\/[^/]+)?\/editions\/(?<edition>[^/]+)\/edit$/ | |
| const editionsPattern = /^https:\/\/hardcover\.app\/books\/[^/]+\/editions[^/]*(?:\/[^/]+)?$/ | |
| let url = null | |
| window.navigation.addEventListener("navigate", (e) => { | |
| if (e.destination.url === url) return | |
| url = e.destination.url | |
| const slug = bookPattern.exec(url)?.groups.slug | |
| if (slug) { | |
| VM.observe(document.body, () => bookPage(slug)) | |
| } else if (authorPattern.test(url)) { | |
| VM.observe(document.body, authorPage) | |
| } else if (searchPattern.test(url)) { | |
| VM.observe(document.body, searchPage) | |
| } | |
| const edition = editPattern.exec(url)?.groups.edition | |
| if (edition) { | |
| VM.observe(document.body, () => editPage(edition)) | |
| } else if (editionsPattern.test(url)) { | |
| VM.observe(document.body, editionsPage) | |
| } | |
| }) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment