Skip to content

Instantly share code, notes, and snippets.

@RedHatter
Last active March 8, 2026 03:26
Show Gist options
  • Select an option

  • Save RedHatter/081d7efe9ec89969b6a3cba049dc3444 to your computer and use it in GitHub Desktop.

Select an option

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.
// ==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