Skip to content

Instantly share code, notes, and snippets.

@SpedNSFW
Last active November 26, 2025 04:17
Show Gist options
  • Select an option

  • Save SpedNSFW/15a88e59a927261029bc574837183800 to your computer and use it in GitHub Desktop.

Select an option

Save SpedNSFW/15a88e59a927261029bc574837183800 to your computer and use it in GitHub Desktop.

StashDB Edit Snippets

On pages that require an edit note of some sorts, the textarea will now have a "Create New Snippet" button to the right of it. When you click that button, anything within the textarea will be saved as a snippet and will appear above the button. Clicking on the snippet will set the textarea to the snippet's contents.

Snippets are saved dependent on what type of page you're on.

Each of the following will have their own list of snippets:

  • New drafts
  • Editing a draft
  • Creating a new performer
  • Creating a new scene
  • Creating a new studio
  • Creating a new tag
  • Editing a performer
  • Editing a scene
  • Editing a studio
  • Editing a tag
// ==UserScript==
// @name Add useful text snippets for StashDB
// @icon https://cdn.discordapp.com/attachments/559159668912553989/841890253707149352/stash2.png
// @namespace https://github.com/SpedNSFW
// @version 0.4
// @description Adds a button below the notes fields for various edit pages to add common snippets easily
// @author Norse
// @match https://stashdb.org/*
// @grant GM.setValue
// @grant GM.getValue
// @grant GM.deleteValue
// @homepageURL https://gist.githubusercontent.com/SpedNSFW/15a88e59a927261029bc574837183800
// @downloadURL https://gist.githubusercontent.com/SpedNSFW/15a88e59a927261029bc574837183800/raw/stashdb_edit_snippets.js
// @updateURL https://gist.githubusercontent.com/SpedNSFW/15a88e59a927261029bc574837183800/raw/stashdb_edit_snippets.js
// ==/UserScript==
addEventListener('DOMContentLoaded', _ => {
const pattern = /^https:\/\/stashdb\.org\/((performers|scenes|tags|studios)\/(add|([a-f0-9-]+\/edit))|(?:(edits)\/[a-f0-9-]+\/update)|drafts\/[a-f0-9-]+)$/i
let previousUrl = ''
const observer = new MutationObserver(mutations => {
if (location.href !== previousUrl) {
previousUrl = location.href
let m
if (m = pattern.exec(location.href)) {
init(m)
}
}
})
observer.observe(document, { subtree: true, childList: true })
})
function waitForElement(selector) {
return new Promise(resolve => {
let element
if (element = document.querySelector(selector)) {
return resolve(element)
}
const observer = new MutationObserver(mutations => {
if (element = document.querySelector(selector)) {
resolve(element)
observer.disconnect()
}
})
observer.observe(document.body, { subtree: true, childList: true })
})
}
async function init(match) {
const noteWrapper = await waitForElement('.NoteInput-tab')
if (!noteWrapper) {
console.warn('Note wrapper is not present. Aborting...')
return
}
const updateDraft = match[5] === 'edits'
const newDraft = match[6] === 'drafts'
const addNew = !(updateDraft || newDraft) && match[3] === 'add'
const editExisting = !(updateDraft || newDraft) && !addNew
const type = !(updateDraft || newDraft) && match[2]
const key = valueKey(updateDraft, newDraft, addNew, editExisting, type)
const value = await GM.getValue(key, "[]")
const snippets = JSON.parse(value)
await buildPrompt(key, noteWrapper, snippets)
}
async function buildPrompt(key, noteWrapper, snippets) {
noteWrapper.classList.add('position-relative')
const parent = document.createElement('div')
parent.classList.add('position-absolute', 'd-flex', 'flex-column')
parent.style.setProperty('top', 0)
parent.style.setProperty('left', 'calc(100% + 2rem)')
noteWrapper.appendChild(parent)
const addNew = document.createElement('button')
addNew.setAttribute('type', 'button')
addNew.classList.add('btn', 'btn-primary', 'order-1', 'mt-3')
addNew.style.setProperty('width', 'max-content')
addNew.appendChild(document.createTextNode('Create New Snippet'))
addNew.addEventListener('click', _ => {
const textarea = noteWrapper.querySelector('textarea')
if (textarea.value) {
void newSnippet(key, noteWrapper, parent, snippets, textarea.value)
}
})
parent.appendChild(addNew)
for (let i = 0; i < snippets.length; i++) {
await newSnippet(key, noteWrapper, parent, snippets, snippets[i], false)
}
}
async function newSnippet(key, noteWrapper, parent, snippets, snippet, isNew = true) {
if (isNew) {
console.log(arguments)
snippets.push(snippet)
await GM.setValue(key, JSON.stringify(snippets))
}
const wrapper = document.createElement('div')
wrapper.classList.add('position-relative', 'd-flex')
parent.appendChild(wrapper)
const deleteSnippet = document.createElement('a')
deleteSnippet.setAttribute('role', 'button')
deleteSnippet.classList.add('text-danger', 'text-decoration-none')
deleteSnippet.style.setProperty('margin-right', '1ch')
deleteSnippet.appendChild(document.createTextNode('\u2717'))
deleteSnippet.addEventListener('click', _ => {
const index = snippets.findIndex(s => s === snippet)
if (index !== -1) {
snippets.splice(index, 1)
void GM.setValue(key, JSON.stringify(snippets))
wrapper.remove()
}
})
wrapper.appendChild(deleteSnippet)
const snippetEl = document.createElement('span')
snippetEl.appendChild(document.createTextNode(snippet))
snippetEl.addEventListener('click', _ => {
const textarea = noteWrapper.querySelector('textarea')
textarea.value = snippet
setTimeout(_ => fireChangeEvent(textarea))
})
wrapper.appendChild(snippetEl)
}
function fireChangeEvent(el) {
// May need browser specific code here
const eventOptions = { bubbles: true }
;['change', 'input', 'invalid']
.map(ev => new Event(ev, eventOptions))
.forEach(ev => el.dispatchEvent(ev))
}
function valueKey(updateEdit, newDraft, addNew, editExisting, type) {
if (updateEdit) {
return 'edits'
}
if (newDraft) {
return 'drafts'
}
if (addNew) {
return `new${type}`
} else if (editExisting) {
return `edit${type}`
}
return null
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment