Guest User

Danbooru/Safebooru Tag Copy Buttons (Sections + Interactive Filter)

a guest
Nov 23rd, 2025
183
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 10.85 KB | Source Code | 0 0
  1. // ==UserScript==
  2. // @name Danbooru/Safebooru Tag Copy Buttons (Sections + Interactive Filter)
  3. // @namespace https://danbooru.donmai.us/
  4. // @version 1.3
  5. // @description Copy artist/copyright/character/general/meta tags with trailing commas; combine categories; interactively ignore specific tags.
  6. // @match https://danbooru.donmai.us/posts/*
  7. // @match https://safebooru.donmai.us/posts/*
  8. // @grant GM_setClipboard
  9. // @grant GM_getValue
  10. // @grant GM_setValue
  11. // @run-at document-idle
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. // --- Config --------------------------------------------------------------
  18.  
  19. const CATEGORY_CONFIGS = [
  20. { key: 'artist', label: 'Artist', selector: 'ul.artist-tag-list li.flex' },
  21. { key: 'copyright', label: 'Copyright', selector: 'ul.copyright-tag-list li.flex' },
  22. { key: 'character', label: 'Character', selector: 'ul.character-tag-list li.flex' },
  23. { key: 'general', label: 'General', selector: 'ul.general-tag-list li.flex' },
  24. { key: 'meta', label: 'Meta', selector: 'ul.meta-tag-list li.flex' }
  25. ];
  26.  
  27. const ALL_SELECTOR = '#tag-list li.flex';
  28.  
  29. const BAR_CLASS = 'danbooru-tag-copy-bar';
  30. const CATEGORY_BUTTON_CLASS = 'danbooru-tag-copy-btn';
  31. const ACTIVE_ATTR = 'data-active';
  32.  
  33. const IGNORE_STORAGE_KEY = 'danbooru_tag_copy_ignore_list';
  34.  
  35. // --- State ---------------------------------------------------------------
  36.  
  37. let ignoreSet = loadIgnoreSet();
  38. let editFilterMode = false;
  39.  
  40. // --- Storage helpers -----------------------------------------------------
  41.  
  42. function loadIgnoreSet() {
  43. try {
  44. const arr = GM_getValue(IGNORE_STORAGE_KEY, []);
  45. if (Array.isArray(arr)) return new Set(arr);
  46. } catch (e) {}
  47. return new Set();
  48. }
  49.  
  50. function saveIgnoreSet() {
  51. try {
  52. GM_setValue(IGNORE_STORAGE_KEY, Array.from(ignoreSet));
  53. } catch (e) {}
  54. }
  55.  
  56. // --- Tag extraction helpers ---------------------------------------------
  57.  
  58. function extractTagFromLi(li) {
  59. const dataName = li.getAttribute('data-tag-name');
  60. if (dataName) return dataName.trim();
  61. const link = li.querySelector('a.search-tag');
  62. return (link?.textContent || '').trim().replace(/\s+/g, '_');
  63. }
  64.  
  65. function getTagsForSelector(selector) {
  66. const nodes = document.querySelectorAll(selector);
  67. if (!nodes.length) return [];
  68. return Array.from(nodes).map(extractTagFromLi).filter(Boolean);
  69. }
  70.  
  71. function getAllTags() {
  72. return getTagsForSelector(ALL_SELECTOR);
  73. }
  74.  
  75. // --- Ignore logic --------------------------------------------------------
  76.  
  77. function filterIgnoredTags(tags) {
  78. if (!ignoreSet || !ignoreSet.size) return tags;
  79. return tags.filter(tag => !ignoreSet.has(tag));
  80. }
  81.  
  82. function setLiIgnoredStyle(li, ignored) {
  83. if (!li) return;
  84. if (ignored) {
  85. li.dataset.ignored = 'true';
  86. li.style.opacity = '0.4';
  87. li.style.textDecoration = 'line-through';
  88. } else {
  89. delete li.dataset.ignored;
  90. li.style.opacity = '';
  91. li.style.textDecoration = '';
  92. }
  93. }
  94.  
  95. function applyIgnoreStyles() {
  96. const tagSection = document.querySelector('#tag-list');
  97. if (!tagSection) return;
  98. const lis = tagSection.querySelectorAll('li.flex');
  99. lis.forEach(li => {
  100. const tagName = extractTagFromLi(li);
  101. const ignored = ignoreSet.has(tagName);
  102. setLiIgnoredStyle(li, ignored);
  103. });
  104. }
  105.  
  106. function toggleIgnoreForLi(li) {
  107. if (!li) return;
  108. const tagName = extractTagFromLi(li);
  109. if (!tagName) return;
  110.  
  111. if (ignoreSet.has(tagName)) {
  112. ignoreSet.delete(tagName);
  113. } else {
  114. ignoreSet.add(tagName);
  115. }
  116. saveIgnoreSet();
  117. setLiIgnoredStyle(li, ignoreSet.has(tagName));
  118. }
  119.  
  120. // --- Clipboard + feedback -----------------------------------------------
  121.  
  122. function setClipboard(text) {
  123. if (typeof GM_setClipboard === 'function') {
  124. GM_setClipboard(text);
  125. return Promise.resolve();
  126. }
  127. if (navigator.clipboard?.writeText) {
  128. return navigator.clipboard.writeText(text).catch(() => {});
  129. }
  130. try {
  131. const ta = document.createElement('textarea');
  132. ta.value = text;
  133. ta.style.position = 'fixed';
  134. ta.style.left = '-9999px';
  135. document.body.appendChild(ta);
  136. ta.select();
  137. document.execCommand('copy');
  138. document.body.removeChild(ta);
  139. } catch (e) {}
  140. return Promise.resolve();
  141. }
  142.  
  143. function giveButtonFeedback(buttonEl, label, extra = '') {
  144. if (!buttonEl) return;
  145. const original = buttonEl.textContent;
  146. buttonEl.textContent = extra ? `${label} ✓ (${extra})` : `${label} ✓`;
  147. setTimeout(() => {
  148. buttonEl.textContent = original;
  149. }, 800);
  150. }
  151.  
  152. function copyTags(tags, label, buttonEl) {
  153. if (!tags.length) {
  154. giveButtonFeedback(buttonEl, label, '0');
  155. return;
  156. }
  157. const text = tags.map(t => `${t},`).join(' ');
  158. setClipboard(text).then(() => {
  159. giveButtonFeedback(buttonEl, label, tags.length.toString());
  160. });
  161. }
  162.  
  163. // --- Button helpers ------------------------------------------------------
  164.  
  165. function styleBaseButton(btn) {
  166. Object.assign(btn.style, {
  167. padding: '3px 6px',
  168. cursor: 'pointer',
  169. fontSize: '11px',
  170. borderRadius: '3px',
  171. border: '1px solid #555',
  172. background: '#2a2a2a',
  173. color: '#eee',
  174. boxSizing: 'border-box',
  175. textAlign: 'center',
  176. whiteSpace: 'nowrap'
  177. });
  178. }
  179.  
  180. function setButtonActive(btn, isActive, attr = ACTIVE_ATTR) {
  181. btn.setAttribute(attr, isActive ? 'true' : 'false');
  182. btn.style.fontWeight = isActive ? 'bold' : 'normal';
  183. btn.style.opacity = isActive ? '1' : '0.85';
  184. btn.style.outline = isActive ? '1px solid #aaa' : 'none';
  185. btn.style.background = isActive ? '#444' : '#2a2a2a';
  186. }
  187.  
  188. function isButtonActive(btn, attr = ACTIVE_ATTR) {
  189. return btn.getAttribute(attr) === 'true';
  190. }
  191.  
  192. function makeSection(titleText) {
  193. const section = document.createElement('div');
  194. Object.assign(section.style, {
  195. border: '1px solid #444',
  196. borderRadius: '4px',
  197. padding: '4px',
  198. background: '#1c1c1c',
  199. display: 'flex',
  200. flexDirection: 'column',
  201. gap: '4px'
  202. });
  203.  
  204. const title = document.createElement('div');
  205. title.textContent = titleText;
  206. Object.assign(title.style, {
  207. fontSize: '10px',
  208. textTransform: 'uppercase',
  209. letterSpacing: '0.03em',
  210. color: '#aaa'
  211. });
  212.  
  213. section.appendChild(title);
  214. return { section, title };
  215. }
  216.  
  217. // --- UI creation ---------------------------------------------------------
  218.  
  219. function createButtons() {
  220. const tagSection = document.querySelector('#tag-list');
  221. if (!tagSection) return;
  222. if (tagSection.querySelector(`.${BAR_CLASS}`)) return;
  223.  
  224. const bar = document.createElement('div');
  225. bar.className = BAR_CLASS;
  226. Object.assign(bar.style, {
  227. margin: '0.5em 0',
  228. display: 'flex',
  229. flexDirection: 'column',
  230. gap: '6px'
  231. });
  232.  
  233. // ===== Categories section =====
  234. const { section: catSection } = makeSection('Categories');
  235.  
  236. const catRow = document.createElement('div');
  237. Object.assign(catRow.style, {
  238. display: 'flex',
  239. flexWrap: 'wrap',
  240. gap: '4px'
  241. });
  242.  
  243. CATEGORY_CONFIGS.forEach(cfg => {
  244. const btn = document.createElement('button');
  245. btn.type = 'button';
  246. btn.textContent = cfg.label;
  247. btn.className = CATEGORY_BUTTON_CLASS;
  248. btn.dataset.categoryKey = cfg.key;
  249.  
  250. styleBaseButton(btn);
  251. setButtonActive(btn, false);
  252.  
  253. btn.addEventListener('click', () => {
  254. const nowActive = !isButtonActive(btn);
  255. setButtonActive(btn, nowActive);
  256. });
  257.  
  258. catRow.appendChild(btn);
  259. });
  260.  
  261. catSection.appendChild(catRow);
  262.  
  263. // ===== Actions section =====
  264. const { section: actionSection } = makeSection('Actions');
  265.  
  266. const actionRow = document.createElement('div');
  267. Object.assign(actionRow.style, {
  268. display: 'flex',
  269. flexWrap: 'wrap',
  270. gap: '4px'
  271. });
  272.  
  273. // Edit Filter toggle
  274. const editFilterBtn = document.createElement('button');
  275. editFilterBtn.type = 'button';
  276. editFilterBtn.textContent = 'Edit Filter';
  277. styleBaseButton(editFilterBtn);
  278. setButtonActive(editFilterBtn, false, 'data-edit-filter');
  279.  
  280. editFilterBtn.addEventListener('click', () => {
  281. editFilterMode = !isButtonActive(editFilterBtn, 'data-edit-filter');
  282. setButtonActive(editFilterBtn, editFilterMode, 'data-edit-filter');
  283. });
  284.  
  285. // Copy Selected
  286. const copySelectedBtn = document.createElement('button');
  287. copySelectedBtn.type = 'button';
  288. copySelectedBtn.textContent = 'Copy Selected';
  289. styleBaseButton(copySelectedBtn);
  290.  
  291. copySelectedBtn.addEventListener('click', () => {
  292. const activeButtons = catRow.querySelectorAll(
  293. `.${CATEGORY_BUTTON_CLASS}[${ACTIVE_ATTR}="true"]`
  294. );
  295. if (!activeButtons.length) {
  296. giveButtonFeedback(copySelectedBtn, 'Selected', '0');
  297. return;
  298. }
  299.  
  300. const allTags = [];
  301. activeButtons.forEach(btn => {
  302. const key = btn.dataset.categoryKey;
  303. const cfg = CATEGORY_CONFIGS.find(c => c.key === key);
  304. if (!cfg) return;
  305. allTags.push(...getTagsForSelector(cfg.selector));
  306. });
  307.  
  308. const uniqueTags = [...new Set(allTags)];
  309. const filtered = filterIgnoredTags(uniqueTags);
  310.  
  311. copyTags(filtered, 'Selected', copySelectedBtn);
  312. });
  313.  
  314. // Copy All
  315. const copyAllBtn = document.createElement('button');
  316. copyAllBtn.type = 'button';
  317. copyAllBtn.textContent = 'Copy All';
  318. styleBaseButton(copyAllBtn);
  319.  
  320. copyAllBtn.addEventListener('click', () => {
  321. let tags = getAllTags();
  322. tags = filterIgnoredTags(tags);
  323. copyTags(tags, 'Copy All', copyAllBtn);
  324. });
  325.  
  326. actionRow.appendChild(editFilterBtn);
  327. actionRow.appendChild(copySelectedBtn);
  328. actionRow.appendChild(copyAllBtn);
  329.  
  330. actionSection.appendChild(actionRow);
  331.  
  332. // Put sections into main bar
  333. bar.appendChild(catSection);
  334. bar.appendChild(actionSection);
  335.  
  336. tagSection.insertBefore(bar, tagSection.firstElementChild);
  337.  
  338. // Click handler for interactive filter mode
  339. tagSection.addEventListener('click', (e) => {
  340. if (!editFilterMode) return;
  341. const link = e.target.closest('a.search-tag');
  342. if (!link) return;
  343. const li = link.closest('li.flex');
  344. if (!li) return;
  345. e.preventDefault();
  346. toggleIgnoreForLi(li);
  347. });
  348.  
  349. applyIgnoreStyles();
  350. }
  351.  
  352. // --- Init ----------------------------------------------------------------
  353.  
  354. if (document.readyState === 'loading') {
  355. document.addEventListener('DOMContentLoaded', createButtons);
  356. } else {
  357. createButtons();
  358. }
  359. })();
  360.  
Tags: danbooru
Add Comment
Please, Sign In to add comment