Skip to content

Instantly share code, notes, and snippets.

@DontTalkToMeThx
Last active October 14, 2023 20:08
Show Gist options
  • Save DontTalkToMeThx/74a4a271e19cbe14195cd2c6671736ba to your computer and use it in GitHub Desktop.
Save DontTalkToMeThx/74a4a271e19cbe14195cd2c6671736ba to your computer and use it in GitHub Desktop.
e621 Tag Helper

This only applies to the auto fetch tag version, the old version won't be updated:

2023-10-14.14-21-29.mp4
// ==UserScript==
// @name e621 Tag Helper
// @version 0.2
// @description Provies a useful UI for tagging common things on e621
// @author DefinitelyNotAFurry4
// @match https://e621.net/uploads/new
// @icon https://www.google.com/s2/favicons?sz=64&domain=e621.net
// @grant GM_addElement
// @grant GM_addStyle
// @grant GM.xmlHttpRequest
// @connect e621.net
// @run-at document-end
// ==/UserScript==
/*
This allows the definition of starting tags for a tag, these are added regardless of whether they are implied or not
ex:
let manualOverrides = {
animal_penis: ["some","tags","you","want","to","start","with"]
}
*/
let manualOverrides = {}
/*
This allows the definition of replacement tags under a parent:
ex:
let replacements = {
animal_penis: { // only affects when the parent is animal_penis
anteater_penis: "whatever_you_want_to_replace_anteater_penis_with"
}
}
*/
let replacements = {}
let lastRequestTime = 0
function wait(ms) { return new Promise(r => setTimeout(r, ms)) }
let queued = 0
async function fetchAllImplicationsTo(tag) {
let tags = manualOverrides[tag] || []
let waitTime = 500 - (Date.now() - lastRequestTime)
if (waitTime > 0) await wait(waitTime * ++queued)
lastRequestTime = Date.now()
return new Promise(resolve => {
GM.xmlHttpRequest({
method: "GET",
url: `https://e621.net/tag_implications.json?search%5Bconsequent_name%5D=${tag}`,
onload: async (response) => {
queued--
let data = JSON.parse(response.responseText)
if (data.tag_implications) return resolve(tags)
for (let implication of data) {
if (implication.status == "active") {
if (!replacements[tag] || replacements[tag][implication.antecedent_name] === undefined) tags.push(implication.antecedent_name)
else if (replacements[tag][implication.antecedent_name]) tags.push(replacements[tag][implication.antecedent_name])
}
}
resolve(tags.sort((a, b) => {
let startingDigitsA = a.match(/^\d+/)
let startingDigitsB = b.match(/^\d+/)
if (startingDigitsA && startingDigitsB) {
return parseInt(startingDigitsA[0]) - parseInt(startingDigitsB[0])
}
return a.localeCompare(b)
}))
}
})
})
}
function toTitle(text) {
return text.toLowerCase().split("_").map(s => s.charAt(0).toUpperCase() + s.substring(1)).join(" ")
}
function removeFromText(text, toRemove) {
return text.split(" ").filter(t => t != toRemove).join(" ")
}
function dispatchEvents(target) {
target.dispatchEvent(new Event("input"))
}
async function buildGroup(group, parentTag, parent = null, hr = null) {
if (group[0] === true) group = await fetchAllImplicationsTo(parentTag)
if (group.length == 0) return []
let nodes = []
let topDiv = parent
if (!topDiv) {
let hr = document.createElement("hr")
hr.classList.add("hidden")
nodes.push(hr)
topDiv = document.createElement("div")
topDiv.classList.add("flex-wrap", "hidden")
nodes.push(topDiv)
} else {
nodes.push(hr)
nodes.push(topDiv)
}
let postTags = document.getElementById("post_tags")
for (let i = 0; i < group.length; i++) {
let nextTag = group[i]
let button = document.createElement("button")
button.classList.add("toggle-button")
button.setAttribute("deta-tag", nextTag)
button.innerText = toTitle(nextTag)
topDiv.appendChild(button)
let possibleHr = document.createElement("hr")
possibleHr.classList.add("hidden")
let possibleDiv = document.createElement("div")
possibleDiv.id = `${nextTag}-button-container`
possibleDiv.classList.add("flex-wrap", "hidden")
let fetchedChildren = false
let hasChildren = false
button.addEventListener("click", async (e) => {
e.preventDefault()
button.classList.toggle("active")
if (button.classList.contains("active")) {
postTags.value += ` ${nextTag}`
postTags.value = postTags.value.trim()
} else {
if (hasChildren) {
for (let child of possibleDiv.children) {
if (child.classList.contains("active")) {
child.click()
}
}
}
postTags.value = removeFromText(postTags.value, nextTag)
}
if (!fetchedChildren) {
let implications = await fetchAllImplicationsTo(nextTag)
if (implications.length > 0) {
hasChildren = true
let nodes = (await buildGroup(implications, nextTag, possibleDiv, possibleHr)).reverse()
for (let node of nodes) {
let nextTagDiv = topDiv.nextSibling
if (i != 0) {
nextTagDiv = document.getElementById(`${group[i - 1]}-button-container`)
let x = i - 1
while (nextTagDiv == null && x > 0) {
nextTagDiv = document.getElementById(`${group[--x]}-button-container`)
}
if (!nextTagDiv) nextTagDiv = topDiv.nextSibling
else nextTagDiv = nextTagDiv.nextSibling
}
topDiv.parentElement.insertBefore(node, nextTagDiv)
}
}
fetchedChildren = true
}
if (hasChildren) {
possibleHr.classList.toggle("hidden")
possibleDiv.classList.toggle("hidden")
}
dispatchEvents(postTags)
})
}
return nodes
}
async function buildButtons(startingPoint) {
let nodes = []
let topDiv = document.createElement("div")
topDiv.classList.add("flex-wrap")
nodes.push(topDiv)
let postTags = document.getElementById("post_tags")
for (let group of startingPoint) {
let button = document.createElement("button")
button.classList.add("toggle-button")
button.setAttribute("deta-tag", group[0])
button.innerText = toTitle(group[0])
topDiv.appendChild(button)
let nodeGroup = await buildGroup(group[1], group[0])
button.addEventListener("click", (e) => {
e.preventDefault()
if (nodeGroup.length > 0) {
nodeGroup[0].classList.toggle("hidden")
nodeGroup[1].classList.toggle("hidden")
}
button.classList.toggle("active")
if (!button.classList.contains("active")) {
postTags.value = removeFromText(postTags.value, group[0])
for (let node of nodeGroup) {
for (let child of node.children) {
if (child.classList.contains("active")) child.click()
}
}
} else {
postTags.value += ` ${group[0]}`
postTags.value = postTags.value.trim()
}
dispatchEvents(postTags)
})
nodes.push(...nodeGroup)
}
return nodes
}
async function buildGrid(options) {
let grid = document.createElement("div")
grid.classList.add("flex-grid", "border-bottom")
let left = document.createElement("div")
left.classList.add("col")
grid.appendChild(left)
let label = document.createElement("label")
label.classList.add("section-label")
label.innerText = options.title//"Common Explicit Tags"
left.appendChild(label)
let explanationDiv = document.createElement("div")
explanationDiv.innerText = options.description//"What common explicit tags are present in the image, if any?"
left.appendChild(explanationDiv)
let right = document.createElement("div")
right.classList.add("col2")
grid.appendChild(right)
right.append(...(await buildButtons(options.startingPoint)))
return { grid, options }
}
function toggleGrids(grids, activeType) {
for (let data of grids) {
if (data.options.ratingTypes.includes(activeType)) {
data.grid.classList.remove("hidden")
} else {
data.grid.classList.add("hidden")
for (let button of data.grid.getElementsByClassName("toggle-button")) {
if (button.classList.contains("active")) button.click()
}
}
}
}
(async function () {
'use strict'
GM_addStyle(".hidden { display:none !important; }")
/*
If you want to add your own, here's a template of what to add to the array:
await buildGrid(
{
title: "This is what shows in bold on the left",
description: "This shows under the title",
startingPoint: [
["these", ["these"]],
["are", ["are"]],
["top", ["second-level"]],
["tags", ["tags"]], // Tags after the second level are automatically fetched by implication,
// if you don't want any second-level tags, use an empty array: `[]`
// Alternatively, you can use `["tag", [true]],` which will auto fetch implications in place of the tags, notice it's still in an array.
],
ratingTypes: ["explicit", "questionable", "safe"] // Remove any that don't apply to the tags
}
),
If the above was added, the array would look like this:
let grids = [
await buildGrid(
{
title: "Media Tags",
description: "What kind of media is this?",
startingPoint: [
["digital_media_(artwork)", [true]],
["photography_(artwork)", [true]],
["traditional_media_(artwork)", [true]]
],
ratingTypes: ["explicit", "questionable", "safe"]
}
),
await buildGrid(
{
title: "Common Explicit Tags",
description: "What common explicit tags are present in the image, if any?",
startingPoint: [
["anus", []],
["cloaca", ["horizontal_cloaca", "round_cloaca", "triangular_cloaca", "vertical_cloaca"]],
["genital_slit", []],
["penis", ["animal_penis", "humanoid_penis", "hybrid_penis", "mechanical_penis", "multi_penis", "prehensile_penis", "unusual_penis", "vacuum_penis"]],
["pussy", ["animal_pussy", "humanoid_pussy", "hybrid_pussy", "mechanical_pussy", "unusual_pussy"]]
],
ratingTypes: ["explicit"]
}
),
await buildGrid(
{
title: "This is what shows in bold on the left",
description: "This shows under the title",
startingPoint: [
["these", ["these"]],
["are", ["are"]],
["top", ["second-level"]],
["tags", ["tags"]],
],
ratingTypes: ["explicit", "questionable", "safe"]
}
),
]
*/
let grids = [
await buildGrid(
{
title: "Media Tags",
description: "What kind of media is this?",
startingPoint: [
["digital_media_(artwork)", [true]],
["mixed_media", [true]],
["photography_(artwork)", [true]],
["traditional_media_(artwork)", [true]]
],
ratingTypes: ["explicit", "questionable", "safe"]
}
),
await buildGrid(
{
title: "Common Explicit Tags",
description: "What common explicit tags are present in the image, if any?",
startingPoint: [
["anus", []],
["balls", ["glowing_balls", "lemon_testicles", "marsupial_balls", "multi_balls", "translucent_balls", "udder_balls", "uniball", "unusual_balls"]],
["clitoris", ["animal_clitoris", "multi_clitoris", "prehensile_clitoris", "tapering_clitoris"]],
["cloaca", ["horizontal_cloaca", "round_cloaca", "triangular_cloaca", "vertical_cloaca"]],
["genital_slit", []],
["penis", ["animal_penis", "humanoid_penis", "hybrid_penis", "mechanical_penis", "multi_penis", "prehensile_penis", "unusual_penis", "vacuum_penis"]],
["pussy", ["animal_pussy", "humanoid_pussy", "hybrid_pussy", "mechanical_pussy", "unusual_pussy"]],
["sheath", ["attached_sheath", "detached_sheath", "fully_sheathed", "glowing_sheath", "knot_in_sheath", "multi_sheath", "retracted_sheath", "sheathed_humanoid_penis", "unusual_sheath"]]
],
ratingTypes: ["explicit"]
}
),
]
let prevChild = null
for (let i = 0; i < grids.length; i++) {
let data = grids[i]
data.grid.classList.add("hidden")
let box = document.querySelector(".col.box-section")
box.insertBefore(data.grid, i == 0 ? box.children[8] : prevChild.nextSibling)
prevChild = data.grid
}
prevChild = null
let explicitRatingToggler = document.querySelector(".toggle-button.rating-e")
let questionableRatingToggler = document.querySelector(".toggle-button.rating-q")
let safeRatingToggler = document.querySelector(".toggle-button.rating-s")
explicitRatingToggler.addEventListener("click", () => {
toggleGrids(grids, "explicit")
})
questionableRatingToggler.addEventListener("click", () => {
toggleGrids(grids, "questionable")
})
safeRatingToggler.addEventListener("click", () => {
toggleGrids(grids, "safe")
})
})()
// ==UserScript==
// @name e621 Genitalia Tag Helper
// @version 0.1
// @description Provies a useful UI for tagging genitals on e621
// @author DefinitelyNotAFurry4
// @match https://e621.net/uploads/new
// @icon https://www.google.com/s2/favicons?sz=64&domain=e621.net
// @grant GM_addElement
// @grant GM_addStyle
// @run-at document-end
// ==/UserScript==
/* Simple function to get the structure of the below array after you add the id...
let types = document.getElementById("types")
function recurse(topChild) {
let arr = []
for (let child of topChild.children) {
if (child.tagName == "UL") {
arr[arr.length - 1].push(recurse(child))
} else if (child.tagName == "LI") {
arr.push([child.innerText])
}
}
return arr
}
console.log(recurse(types))
*/
let penises = [["animal_penis", [["avian_penis"], ["bovine_penis"], ["canine_penis"], ["caprine_penis"], ["cervine_penis"], ["tapering_penis", [["cetacean_penis"]]], ["crocodilian_penis"], ["echidna_penis"], ["equine_penis", [["knotted_equine_penis"]]], ["feline_penis"], ["fossa_penis"], ["lagomorph_penis"], ["marsupial_penis"], ["mustelid_penis"], ["porcine_penis"], ["raccoon_penis"], ["turtle_penis"], ["ursine_penis"]]], ["humanoid_penis"], ["hybrid_penis"], ["mechanical_penis"], ["unusual_penis"], ["vacuum_penis"]];
let pussies = [["animal_pussy", [["bovine_pussy"], ["canine_pussy"], ["caprine_pussy"], ["cervine_pussy"], ["cephalopussy"], ["cetacean_pussy"], ["feline_pussy"], ["equine_pussy"], ["lagomorph_pussy"], ["marsupial_pussy"], ["mustelid_pussy"], ["porcine_pussy"], ["raccoon_pussy"], ["ursine_pussy"]]], ["humanoid_pussy"], ["hybrid_pussy"], ["mechanical_pussy"], ["unusual_pussy"]];
function toTitle(text) {
return text.toLowerCase().split("_").map(s => s.charAt(0).toUpperCase() + s.substring(1)).join(" ")
}
function removeFromText(text, toRemove) {
return text.split(" ").filter(t => t != toRemove).join(" ")
}
function buildGroup(group) {
let nodes = []
let hr = document.createElement("hr")
hr.classList.add("hidden")
nodes.push(hr)
let topDiv = document.createElement("div")
topDiv.classList.add("flex-wrap", "hidden")
nodes.push(topDiv)
for (let nextGroup of group) {
let postTags = document.getElementById("post_tags")
let button = document.createElement("button")
button.classList.add("toggle-button")
button.setAttribute("deta-tag", nextGroup[0])
button.innerText = toTitle(nextGroup[0])
topDiv.appendChild(button)
if (nextGroup.length == 2) {
let nextNodes = buildGroup(nextGroup[1])
nodes.push(...nextNodes)
button.addEventListener("click", (e) => {
e.preventDefault()
let inputEvent = new Event("input")
nextNodes[0].classList.toggle("hidden")
nextNodes[1].classList.toggle("hidden")
button.classList.toggle("active")
if (!button.classList.contains("active")) {
postTags.value = removeFromText(postTags.value, nextGroup[0])
for (let node of nextNodes) {
for (let child of node.children) {
if (child.classList.contains("active")) child.click()
}
}
} else {
postTags.value += ` ${nextGroup[0]}`
postTags.value = postTags.value.trim()
}
postTags.dispatchEvent(inputEvent)
})
} else {
button.addEventListener("click", (e) => {
e.preventDefault()
let inputEvent = new Event("input")
button.classList.toggle("active")
if (button.classList.contains("active")) {
postTags.value += ` ${nextGroup[0]}`
postTags.value = postTags.value.trim()
} else {
postTags.value = removeFromText(postTags.value, nextGroup[0])
}
postTags.dispatchEvent(inputEvent)
})
}
}
return nodes
}
function buildButtons() {
let nodes = []
let topDiv = document.createElement("div")
topDiv.classList.add("flex-wrap")
nodes.push(topDiv)
let penisButton = document.createElement("button")
penisButton.classList.add("toggle-button", "top-level-genital-toggle-button")
penisButton.setAttribute("deta-tag", "penis")
penisButton.innerText = "Penis"
topDiv.appendChild(penisButton)
let pussyButton = document.createElement("button")
pussyButton.classList.add("toggle-button", "top-level-genital-toggle-button")
pussyButton.setAttribute("deta-tag", "pussy")
pussyButton.innerText = "Pussy"
topDiv.appendChild(pussyButton)
let penisGroup = buildGroup(penises)
nodes.push(...penisGroup)
let postTags = document.getElementById("post_tags")
penisButton.addEventListener("click", (e) => {
e.preventDefault()
let inputEvent = new Event("input")
penisGroup[0].classList.toggle("hidden")
penisGroup[1].classList.toggle("hidden")
penisButton.classList.toggle("active")
if (!penisButton.classList.contains("active")) {
postTags.value = removeFromText(postTags.value, "penis")
for (let node of penisGroup) {
for (let child of node.children) {
if (child.classList.contains("active")) child.click()
}
}
} else {
postTags.value += " penis"
postTags.value = postTags.value.trim()
}
postTags.dispatchEvent(inputEvent)
})
let pussyGroup = buildGroup(pussies)
nodes.push(...pussyGroup)
pussyButton.addEventListener("click", (e) => {
e.preventDefault()
let inputEvent = new Event("input")
pussyGroup[0].classList.toggle("hidden")
pussyGroup[1].classList.toggle("hidden")
pussyButton.classList.toggle("active")
if (!pussyButton.classList.contains("active")) {
postTags.value = removeFromText(postTags.value, "pussy")
for (let node of pussyGroup) {
for (let child of node.children) {
if (child.classList.contains("active")) child.click()
}
}
} else {
postTags.value += " pussy"
postTags.value = postTags.value.trim()
}
postTags.dispatchEvent(inputEvent)
})
return nodes
}
function buildGrid() {
let genitaliaGrid = document.createElement("div")
genitaliaGrid.classList.add("flex-grid", "border-bottom")
let left = document.createElement("div")
left.classList.add("col")
genitaliaGrid.appendChild(left)
let label = document.createElement("label")
label.classList.add("section-label")
label.innerText = "Genitalia"
left.appendChild(label)
let explanationDiv = document.createElement("div")
explanationDiv.innerText = "What genitalia is present in the image, if any?"
left.appendChild(explanationDiv)
let right = document.createElement("div")
right.classList.add("col2")
genitaliaGrid.appendChild(right)
right.append(...buildButtons())
return genitaliaGrid
}
(function () {
'use strict'
GM_addStyle(".hidden { display:none !important; }")
let grid = buildGrid()
grid.classList.add("hidden")
let box = document.querySelector(".col.box-section")
box.insertBefore(grid, box.children[8])
let explicitRatingToggler = document.querySelector(".toggle-button.rating-e")
let questionableRatingToggler = document.querySelector(".toggle-button.rating-q")
let safeRatingToggler = document.querySelector(".toggle-button.rating-s")
explicitRatingToggler.addEventListener("click", () => {
grid.classList.remove("hidden")
})
questionableRatingToggler.addEventListener("click", () => {
grid.classList.add("hidden")
for (let button of document.getElementsByClassName("top-level-genital-toggle-button")) {
if (button.classList.contains("active")) button.click()
}
})
safeRatingToggler.addEventListener("click", () => {
grid.classList.add("hidden")
for (let button of document.getElementsByClassName("top-level-genital-toggle-button")) {
if (button.classList.contains("active")) button.click()
}
})
})()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment