Guest User

Quick Options 1.0.0

a guest
Dec 19th, 2025
50
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. // ==UserScript==
  2. // @name         Booru Quick Options
  3. // @version      1.0.0
  4. // @description  Adds a handful of quick utilities to post thumbnails.
  5. // @author       https://danbooru.donmai.us/users/1247062
  6. // @match        https://*.donmai.us/
  7. // @match        https://*.donmai.us/posts
  8. // @match        https://*.donmai.us/posts?*
  9. // @match        https://donmai.moe/
  10. // @match        https://donmai.moe/posts
  11. // @match        https://donmai.moe/posts?*
  12. // @grant        GM_addStyle
  13. // ==/UserScript==
  14.  
  15. GM_addStyle(`
  16.  
  17. body:not([data-mode-menu-active="true"]) picture:hover .quickoptions-buttons { display: block; }
  18.  
  19. .quickoptions-buttons {
  20.     display: none;
  21.     position: absolute;
  22.     top: 6px;
  23.     left: 4px;
  24. }
  25.  
  26. .quickoptions-buttons[data-loading="true"] {
  27.     opacity: 40%;
  28.     pointer-events: none;
  29. }
  30.  
  31. .quickoptions-buttons a {
  32.     background-color: #1e1d1da8;
  33.     padding: 2px;
  34.     font-size: smaller;
  35.     color: #fff;
  36.     display: inline-block;
  37. }
  38.  
  39. `)
  40.  
  41. const optionsTemplate = `
  42.     <a href="#" id="qo-action-download">save</a>
  43.     <a href="#" id="qo-action-copy">copy</a>
  44.     <a href="#" id="qo-action-copy-id">copy id</a>
  45.     <a href="#" id="qo-action-artist">artist »</a>
  46.     <a href="#" id="qo-action-char">char »</a>
  47.     <a href="#" id="qo-action-goto-source">source »</a>
  48. `
  49.  
  50. var posts = {}
  51.  
  52. function normalizePixiv(link) {
  53.     try {
  54.         let regex = /https:\/\/i\.pximg\.net\/img-original\/img\/[0-9]+\/[0-9]+\/[0-9]+\/[0-9]+\/[0-9]+\/[0-9]+\/([0-9]+)_p.+\.(png|jpg|webp)/
  55.         let matched = link.match(regex)
  56.  
  57.         if(matched == null) throw new Error()
  58.  
  59.         return `https://www.pixiv.net/artworks/${matched[1]}`
  60.     } catch(e) {
  61.         console.log(e)
  62.         return Danbooru.notice(`extracting pixiv id failed: ${link}`)
  63.     }
  64. }
  65.  
  66. async function requirePost(button) {
  67.     let buttonHolder = button.parentNode
  68.     let pid = buttonHolder.getAttribute("post-id")
  69.     let postRecord = posts[pid]
  70.  
  71.     if(postRecord.lock == true) return
  72.     if(postRecord.data != null) return postRecord
  73.  
  74.     postRecord.lock = true
  75.     buttonHolder.setAttribute("data-loading", "true")
  76.  
  77.     try {
  78.         let resp = await fetch(`/posts/${pid}.json`)
  79.         let postData = await resp.json()
  80.  
  81.         postRecord.data = postData
  82.     } catch(e) {
  83.         console.log(e)
  84.         Danbooru.notice(e)
  85.     }
  86.  
  87.     postRecord.lock = false
  88.     buttonHolder.setAttribute("data-loading", "false")
  89.  
  90.     if(postRecord.data != null) return postRecord
  91. }
  92.  
  93. async function doDownload(event) {
  94.     event.preventDefault()
  95.  
  96.     let post = await requirePost(event.target)
  97.     let url = new URL(post.data.file_url)
  98.     url.searchParams.set("download", "1")
  99.  
  100.     window.open(url, "_self")
  101. }
  102.  
  103. async function doCopyImg(event) {
  104.     event.preventDefault()
  105.  
  106.     let post = await requirePost(event.target)
  107.     let url = new URL(post.data.file_url)
  108.  
  109.     if(!["png", "jpg", "jpeg", "webp"].includes(post.data.file_ext)) return Danbooru.notice("Sorry, only static images are supported.")
  110.  
  111.     Danbooru.notice("Fetching image...")
  112.  
  113.     let imgResp
  114.     let imgData
  115.  
  116.     try {
  117.         imgResp = await fetch(url)
  118.         imgData = await imgResp.blob()
  119.     } catch(e) {
  120.         console.log(e)
  121.         Danbooru.notice(e)
  122.  
  123.         return
  124.     }
  125.  
  126.     /*
  127.      here's where it gets stupid. you see, the clipboard write api
  128.      *does not* support any image file type other than PNG.
  129.  
  130.      so to copy a file, we have to do this dumb dance of converting
  131.      our file to a bitmap, rendering that bitmap to a canvas, then
  132.      exporting that canvas to a PNG to copy.
  133.  
  134.      this means i cannot support things that aren't static images
  135.      and the copied image has its file size inflated. this sucks.
  136.  
  137.      https://developer.mozilla.org/en-US/docs/Web/API/ClipboardItem/supports_static
  138.     */
  139.  
  140.     let imgBitmap = await window.createImageBitmap(imgData)
  141.  
  142.     let canvas = document.createElement("canvas")
  143.     canvas.width = imgBitmap.width
  144.     canvas.height = imgBitmap.height
  145.     let ctx = canvas.getContext("2d")
  146.     ctx.drawImage(imgBitmap, 0, 0)
  147.  
  148.     canvas.toBlob(async (pngData) => {
  149.         let clipItem = new ClipboardItem({
  150.             [pngData.type]: pngData
  151.         })
  152.         await navigator.clipboard.write([clipItem])
  153.         Danbooru.notice(`Post #${post.data.id} image copied to clipboard`)
  154.     }, "image/png")
  155. }
  156.  
  157. async function doCopyId(event) {
  158.     event.preventDefault()
  159.  
  160.     let pid = event.target.parentNode.getAttribute("post-id")
  161.  
  162.     await navigator.clipboard.write([
  163.         new ClipboardItem({ ["text/plain"]: `post #${pid}` })
  164.     ])
  165.     Danbooru.notice(`post #${pid} copied to clipboard`)
  166. }
  167.  
  168. async function doBrowseArtist(event) {
  169.     if(event.button == 2) return
  170.     event.preventDefault()
  171.  
  172.     let post = await requirePost(event.target)
  173.  
  174.     if(post.data.tag_count_artist == 0) return Danbooru.notice("Post has no artist tag.")
  175.     let artistTag = post.data.tag_string_artist.split(" ")[0]
  176.  
  177.     let url = new URL(window.location)
  178.     url.path = "/posts"
  179.     url.searchParams.set("tags", artistTag + " status:any")
  180.     url.searchParams.set("page", "1")
  181.  
  182.     window.open(url, "_blank")
  183. }
  184.  
  185. async function doBrowseCharacter(event) {
  186.     if(event.button == 2) return
  187.     event.preventDefault()
  188.  
  189.     let post = await requirePost(event.target)
  190.  
  191.     if(post.data.tag_count_character == 0) return Danbooru.notice("Post has no character tag.")
  192.     let charTag = post.data.tag_string_character.split(" ")[0]
  193.  
  194.     let url = new URL(window.location)
  195.     url.path = "/posts"
  196.     url.searchParams.set("tags", charTag)
  197.     url.searchParams.set("page", "1")
  198.  
  199.     window.open(url, "_blank")
  200. }
  201.  
  202. async function doBrowseSource(event) {
  203.     event.preventDefault()
  204.  
  205.     let post = await requirePost(event.target)
  206.     let sourceLink = post.data.source
  207.     if(sourceLink.length == 0) return Danbooru.notice(`Post has no source`)
  208.  
  209.     // it appears danbooru api doesn't return the normalized source and
  210.     // pixiv rejects requests to the source link as given by the api. let's normalize
  211.     if(sourceLink.includes("i.pximg.net")) sourceLink = normalizePixiv(sourceLink)
  212.  
  213.     if(post.data.tag_string.includes("non-web_source")) return Danbooru.notice(`Post has a non-web source (${post.data.source})`)
  214.  
  215.     if(event.button != 2) { window.open(sourceLink, "_blank") } else {
  216.         await navigator.clipboard.write([
  217.             new ClipboardItem({ ["text/plain"]: sourceLink })
  218.         ])
  219.         Danbooru.notice(`Copied post #${post.data.id} source link to clipboard`)
  220.     }
  221. }
  222.  
  223. $(document).on("danbooru:post-preview-updated", onGalleryMutation)
  224.  
  225. function onGalleryMutation(_, mut) {
  226.     attachPost(document.querySelector(`#post_${mut.id}`))
  227. }
  228.  
  229. function attachPost(element) {
  230.     let pid = element.getAttribute("data-id")
  231.     let picture = element.querySelector("picture")
  232.     let holder = document.createElement("div")
  233.  
  234.     holder.classList.add("quickoptions-buttons")
  235.     holder.setAttribute("post-id", pid)
  236.  
  237.     picture.appendChild(holder)
  238.     holder.innerHTML = optionsTemplate
  239.  
  240.     posts[pid] = {
  241.         lock: false,
  242.         options: holder,
  243.         data: null
  244.     }
  245.  
  246.     holder.querySelector("#qo-action-download").addEventListener("click", doDownload)
  247.     holder.querySelector("#qo-action-copy").addEventListener("click", doCopyImg)
  248.     holder.querySelector("#qo-action-copy-id").addEventListener("click", doCopyId)
  249.  
  250.     holder.querySelector("#qo-action-artist").addEventListener("click", doBrowseArtist)
  251.     holder.querySelector("#qo-action-artist").addEventListener("auxclick", doBrowseArtist)
  252.  
  253.     holder.querySelector("#qo-action-char").addEventListener("click", doBrowseCharacter)
  254.     holder.querySelector("#qo-action-char").addEventListener("auxclick", doBrowseCharacter)
  255.  
  256.     holder.querySelector("#qo-action-goto-source").addEventListener("click", doBrowseSource)
  257.     holder.querySelector("#qo-action-goto-source").addEventListener("auxclick", doBrowseSource)
  258.     holder.querySelector("#qo-action-goto-source").addEventListener("contextmenu", (e) => {e.preventDefault()})
  259. }
  260.  
  261. function init() {
  262.     document.querySelectorAll(".post-preview").forEach(post => attachPost(post))
  263. }
  264.  
  265. init()
Add Comment
Please, Sign In to add comment