Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // ==UserScript==
- // @name Danbooru/Safebooru Tag Copy Buttons (Sections + Interactive Filter)
- // @namespace https://danbooru.donmai.us/
- // @version 1.3
- // @description Copy artist/copyright/character/general/meta tags with trailing commas; combine categories; interactively ignore specific tags.
- // @match https://danbooru.donmai.us/posts/*
- // @match https://safebooru.donmai.us/posts/*
- // @grant GM_setClipboard
- // @grant GM_getValue
- // @grant GM_setValue
- // @run-at document-idle
- // ==/UserScript==
- (function() {
- 'use strict';
- // --- Config --------------------------------------------------------------
- const CATEGORY_CONFIGS = [
- { key: 'artist', label: 'Artist', selector: 'ul.artist-tag-list li.flex' },
- { key: 'copyright', label: 'Copyright', selector: 'ul.copyright-tag-list li.flex' },
- { key: 'character', label: 'Character', selector: 'ul.character-tag-list li.flex' },
- { key: 'general', label: 'General', selector: 'ul.general-tag-list li.flex' },
- { key: 'meta', label: 'Meta', selector: 'ul.meta-tag-list li.flex' }
- ];
- const ALL_SELECTOR = '#tag-list li.flex';
- const BAR_CLASS = 'danbooru-tag-copy-bar';
- const CATEGORY_BUTTON_CLASS = 'danbooru-tag-copy-btn';
- const ACTIVE_ATTR = 'data-active';
- const IGNORE_STORAGE_KEY = 'danbooru_tag_copy_ignore_list';
- // --- State ---------------------------------------------------------------
- let ignoreSet = loadIgnoreSet();
- let editFilterMode = false;
- // --- Storage helpers -----------------------------------------------------
- function loadIgnoreSet() {
- try {
- const arr = GM_getValue(IGNORE_STORAGE_KEY, []);
- if (Array.isArray(arr)) return new Set(arr);
- } catch (e) {}
- return new Set();
- }
- function saveIgnoreSet() {
- try {
- GM_setValue(IGNORE_STORAGE_KEY, Array.from(ignoreSet));
- } catch (e) {}
- }
- // --- Tag extraction helpers ---------------------------------------------
- function extractTagFromLi(li) {
- const dataName = li.getAttribute('data-tag-name');
- if (dataName) return dataName.trim();
- const link = li.querySelector('a.search-tag');
- return (link?.textContent || '').trim().replace(/\s+/g, '_');
- }
- function getTagsForSelector(selector) {
- const nodes = document.querySelectorAll(selector);
- if (!nodes.length) return [];
- return Array.from(nodes).map(extractTagFromLi).filter(Boolean);
- }
- function getAllTags() {
- return getTagsForSelector(ALL_SELECTOR);
- }
- // --- Ignore logic --------------------------------------------------------
- function filterIgnoredTags(tags) {
- if (!ignoreSet || !ignoreSet.size) return tags;
- return tags.filter(tag => !ignoreSet.has(tag));
- }
- function setLiIgnoredStyle(li, ignored) {
- if (!li) return;
- if (ignored) {
- li.dataset.ignored = 'true';
- li.style.opacity = '0.4';
- li.style.textDecoration = 'line-through';
- } else {
- delete li.dataset.ignored;
- li.style.opacity = '';
- li.style.textDecoration = '';
- }
- }
- function applyIgnoreStyles() {
- const tagSection = document.querySelector('#tag-list');
- if (!tagSection) return;
- const lis = tagSection.querySelectorAll('li.flex');
- lis.forEach(li => {
- const tagName = extractTagFromLi(li);
- const ignored = ignoreSet.has(tagName);
- setLiIgnoredStyle(li, ignored);
- });
- }
- function toggleIgnoreForLi(li) {
- if (!li) return;
- const tagName = extractTagFromLi(li);
- if (!tagName) return;
- if (ignoreSet.has(tagName)) {
- ignoreSet.delete(tagName);
- } else {
- ignoreSet.add(tagName);
- }
- saveIgnoreSet();
- setLiIgnoredStyle(li, ignoreSet.has(tagName));
- }
- // --- Clipboard + feedback -----------------------------------------------
- function setClipboard(text) {
- if (typeof GM_setClipboard === 'function') {
- GM_setClipboard(text);
- return Promise.resolve();
- }
- if (navigator.clipboard?.writeText) {
- return navigator.clipboard.writeText(text).catch(() => {});
- }
- try {
- const ta = document.createElement('textarea');
- ta.value = text;
- ta.style.position = 'fixed';
- ta.style.left = '-9999px';
- document.body.appendChild(ta);
- ta.select();
- document.execCommand('copy');
- document.body.removeChild(ta);
- } catch (e) {}
- return Promise.resolve();
- }
- function giveButtonFeedback(buttonEl, label, extra = '') {
- if (!buttonEl) return;
- const original = buttonEl.textContent;
- buttonEl.textContent = extra ? `${label} ✓ (${extra})` : `${label} ✓`;
- setTimeout(() => {
- buttonEl.textContent = original;
- }, 800);
- }
- function copyTags(tags, label, buttonEl) {
- if (!tags.length) {
- giveButtonFeedback(buttonEl, label, '0');
- return;
- }
- const text = tags.map(t => `${t},`).join(' ');
- setClipboard(text).then(() => {
- giveButtonFeedback(buttonEl, label, tags.length.toString());
- });
- }
- // --- Button helpers ------------------------------------------------------
- function styleBaseButton(btn) {
- Object.assign(btn.style, {
- padding: '3px 6px',
- cursor: 'pointer',
- fontSize: '11px',
- borderRadius: '3px',
- border: '1px solid #555',
- background: '#2a2a2a',
- color: '#eee',
- boxSizing: 'border-box',
- textAlign: 'center',
- whiteSpace: 'nowrap'
- });
- }
- function setButtonActive(btn, isActive, attr = ACTIVE_ATTR) {
- btn.setAttribute(attr, isActive ? 'true' : 'false');
- btn.style.fontWeight = isActive ? 'bold' : 'normal';
- btn.style.opacity = isActive ? '1' : '0.85';
- btn.style.outline = isActive ? '1px solid #aaa' : 'none';
- btn.style.background = isActive ? '#444' : '#2a2a2a';
- }
- function isButtonActive(btn, attr = ACTIVE_ATTR) {
- return btn.getAttribute(attr) === 'true';
- }
- function makeSection(titleText) {
- const section = document.createElement('div');
- Object.assign(section.style, {
- border: '1px solid #444',
- borderRadius: '4px',
- padding: '4px',
- background: '#1c1c1c',
- display: 'flex',
- flexDirection: 'column',
- gap: '4px'
- });
- const title = document.createElement('div');
- title.textContent = titleText;
- Object.assign(title.style, {
- fontSize: '10px',
- textTransform: 'uppercase',
- letterSpacing: '0.03em',
- color: '#aaa'
- });
- section.appendChild(title);
- return { section, title };
- }
- // --- UI creation ---------------------------------------------------------
- function createButtons() {
- const tagSection = document.querySelector('#tag-list');
- if (!tagSection) return;
- if (tagSection.querySelector(`.${BAR_CLASS}`)) return;
- const bar = document.createElement('div');
- bar.className = BAR_CLASS;
- Object.assign(bar.style, {
- margin: '0.5em 0',
- display: 'flex',
- flexDirection: 'column',
- gap: '6px'
- });
- // ===== Categories section =====
- const { section: catSection } = makeSection('Categories');
- const catRow = document.createElement('div');
- Object.assign(catRow.style, {
- display: 'flex',
- flexWrap: 'wrap',
- gap: '4px'
- });
- CATEGORY_CONFIGS.forEach(cfg => {
- const btn = document.createElement('button');
- btn.type = 'button';
- btn.textContent = cfg.label;
- btn.className = CATEGORY_BUTTON_CLASS;
- btn.dataset.categoryKey = cfg.key;
- styleBaseButton(btn);
- setButtonActive(btn, false);
- btn.addEventListener('click', () => {
- const nowActive = !isButtonActive(btn);
- setButtonActive(btn, nowActive);
- });
- catRow.appendChild(btn);
- });
- catSection.appendChild(catRow);
- // ===== Actions section =====
- const { section: actionSection } = makeSection('Actions');
- const actionRow = document.createElement('div');
- Object.assign(actionRow.style, {
- display: 'flex',
- flexWrap: 'wrap',
- gap: '4px'
- });
- // Edit Filter toggle
- const editFilterBtn = document.createElement('button');
- editFilterBtn.type = 'button';
- editFilterBtn.textContent = 'Edit Filter';
- styleBaseButton(editFilterBtn);
- setButtonActive(editFilterBtn, false, 'data-edit-filter');
- editFilterBtn.addEventListener('click', () => {
- editFilterMode = !isButtonActive(editFilterBtn, 'data-edit-filter');
- setButtonActive(editFilterBtn, editFilterMode, 'data-edit-filter');
- });
- // Copy Selected
- const copySelectedBtn = document.createElement('button');
- copySelectedBtn.type = 'button';
- copySelectedBtn.textContent = 'Copy Selected';
- styleBaseButton(copySelectedBtn);
- copySelectedBtn.addEventListener('click', () => {
- const activeButtons = catRow.querySelectorAll(
- `.${CATEGORY_BUTTON_CLASS}[${ACTIVE_ATTR}="true"]`
- );
- if (!activeButtons.length) {
- giveButtonFeedback(copySelectedBtn, 'Selected', '0');
- return;
- }
- const allTags = [];
- activeButtons.forEach(btn => {
- const key = btn.dataset.categoryKey;
- const cfg = CATEGORY_CONFIGS.find(c => c.key === key);
- if (!cfg) return;
- allTags.push(...getTagsForSelector(cfg.selector));
- });
- const uniqueTags = [...new Set(allTags)];
- const filtered = filterIgnoredTags(uniqueTags);
- copyTags(filtered, 'Selected', copySelectedBtn);
- });
- // Copy All
- const copyAllBtn = document.createElement('button');
- copyAllBtn.type = 'button';
- copyAllBtn.textContent = 'Copy All';
- styleBaseButton(copyAllBtn);
- copyAllBtn.addEventListener('click', () => {
- let tags = getAllTags();
- tags = filterIgnoredTags(tags);
- copyTags(tags, 'Copy All', copyAllBtn);
- });
- actionRow.appendChild(editFilterBtn);
- actionRow.appendChild(copySelectedBtn);
- actionRow.appendChild(copyAllBtn);
- actionSection.appendChild(actionRow);
- // Put sections into main bar
- bar.appendChild(catSection);
- bar.appendChild(actionSection);
- tagSection.insertBefore(bar, tagSection.firstElementChild);
- // Click handler for interactive filter mode
- tagSection.addEventListener('click', (e) => {
- if (!editFilterMode) return;
- const link = e.target.closest('a.search-tag');
- if (!link) return;
- const li = link.closest('li.flex');
- if (!li) return;
- e.preventDefault();
- toggleIgnoreForLi(li);
- });
- applyIgnoreStyles();
- }
- // --- Init ----------------------------------------------------------------
- if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', createButtons);
- } else {
- createButtons();
- }
- })();
Add Comment
Please, Sign In to add comment