Danbooru Prompt Extractor

Danbooru提示词提取器 - 新增删除模式/过滤/搜索

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Danbooru Prompt Extractor
// @namespace    danbooru-prompt-extractor
// @version      1.0.8
// @description  Danbooru提示词提取器 - 新增删除模式/过滤/搜索
// @match        https://danbooru.donmai.us/posts/*
// @match        https://danbooru.donmai.us/posts?*
// @grant        none
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // ===== 预设过滤词(马赛克/审查/遮挡相关,可自行增删) =====
    var FILTER_WORDS = [
        'mosaic', 'mosaic_censoring', 'censored', 'censor', 'censoring',
        'bar_censor', 'black_bar', 'pixelated', 'pixelation',
        'patchwork_censor', 'spotlight_censor', 'blur', 'blurred',
        'covering', 'pubic_hair'
    ];

    var css =
        '#dpe-wrap{position:fixed;left:20px;top:300px;width:50px;height:50px;z-index:2147483647;font-family:-apple-system,BlinkMacSystemFont,sans-serif}' +
        '#dpe-toggle{width:50px;height:50px;border-radius:50%;background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;display:flex;align-items:center;justify-content:center;font-size:24px;box-shadow:0 4px 16px rgba(102,126,234,0.5);cursor:pointer;user-select:none;-webkit-user-select:none;-webkit-touch-callout:none;touch-action:none}' +
        '#dpe-panel{display:none;flex-direction:column;position:absolute;top:56px;left:0;background:#fff;border-radius:12px;box-shadow:0 12px 40px rgba(0,0,0,0.25);overflow:hidden}' +
        '#dpe-head{padding:10px 12px;background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;display:flex;align-items:center;justify-content:space-between;font-size:14px;font-weight:600;flex-shrink:0;gap:6px}' +
        '#dpe-tools{display:flex;gap:4px;align-items:center}' +
        '#dpe-tools button{background:rgba(255,255,255,0.2);border:none;color:#fff;font-size:14px;padding:3px 8px;border-radius:6px;cursor:pointer;line-height:1.4}' +
        '#dpe-tools button.on{background:#ff5252}' +
        '#dpe-search{display:none;padding:8px 10px;border-bottom:1px solid #eee;flex-shrink:0}' +
        '#dpe-search input{width:100%;box-sizing:border-box;padding:7px 10px;border:1px solid #ddd;border-radius:6px;font-size:13px;outline:none}' +
        '#dpe-body{flex:1;overflow-y:auto;padding:10px;-webkit-overflow-scrolling:touch;min-height:0;touch-action:pan-y}' +
        '.dpe-cat{margin-bottom:10px}' +
        '.dpe-cat-t{font-size:12px;font-weight:600;color:#888;margin-bottom:5px}' +
        '.dpe-cat-c{background:#eee;color:#666;padding:1px 7px;border-radius:10px;font-size:10px;margin-left:6px}' +
        '.dpe-tgs{display:flex;flex-wrap:wrap;gap:4px}' +
        '.dpe-tg{background:#f3f3f3;color:#333;padding:5px 10px;border-radius:5px;font-size:12px;cursor:pointer;border:1px solid #e8e8e8;user-select:none;-webkit-user-select:none;white-space:nowrap;touch-action:pan-y;transition:border-color .15s}' +
        '.dpe-delx{display:inline-block;margin-left:5px;color:#fff;background:#e74c3c;border-radius:50%;width:15px;height:15px;line-height:14px;text-align:center;font-size:10px;font-weight:bold;cursor:pointer;vertical-align:1px}' +
        '.dpe-hl{border:2px solid #ff5252 !important;background:#ffe3e3 !important;box-shadow:0 0 6px rgba(255,60,60,.5) !important}' +
        '.dpe-hl-first{border:3px solid #ff1744 !important;background:#ffd0d0 !important}' +
        '#dpe-foot{padding:10px 14px;border-top:1px solid #eee;display:flex;gap:8px;align-items:stretch;flex-shrink:0}' +
        '#dpe-preview{flex:1;background:#f8f8f8;border:1px solid #e0e0e0;border-radius:6px;padding:6px 8px;font-size:11px;color:#555;max-height:70px;overflow-y:auto;word-break:break-all;-webkit-overflow-scrolling:touch;touch-action:pan-y}' +
        '#dpe-copy{background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;border:none;padding:8px 14px;border-radius:6px;cursor:pointer;font-size:13px;font-weight:500;white-space:nowrap}' +
        '#dpe-toast{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(0,0,0,0.85);color:#fff;padding:10px 20px;border-radius:8px;font-size:14px;z-index:2147483648;pointer-events:none;transition:opacity 0.3s}';

    var st = document.createElement('style');
    st.textContent = css;
    document.head.appendChild(st);

    var wrapper = document.createElement('div');
    wrapper.id = 'dpe-wrap';
    wrapper.innerHTML =
        '<div id="dpe-toggle">🏷</div>' +
        '<div id="dpe-panel">' +
        '<div id="dpe-head"><span>🎨 Danbooru</span><span id="dpe-tools"><button id="dpe-search-btn" title="搜索">🔍</button><button id="dpe-filter-btn" title="过滤马赛克词">🧹</button><button id="dpe-del-btn" title="删除模式">🗑️</button><button id="dpe-ok-btn" title="完成" style="display:none">✓</button><button id="dpe-refresh" title="刷新">🔄</button></span></div>' +
        '<div id="dpe-search"><input type="text" placeholder="输入关键词搜索标签..."/></div>' +
        '<div id="dpe-body"></div>' +
        '<div id="dpe-foot"><div id="dpe-preview"></div><button id="dpe-copy">📋 复制</button></div>' +
        '</div>';
    document.body.appendChild(wrapper);

    var panel = wrapper.querySelector('#dpe-panel');
    var pbody = wrapper.querySelector('#dpe-body');
    var preview = wrapper.querySelector('#dpe-preview');
    var copyBtn = wrapper.querySelector('#dpe-copy');
    var btn = wrapper.querySelector('#dpe-toggle');
    var refreshBtn = wrapper.querySelector('#dpe-refresh');
    var delBtn = wrapper.querySelector('#dpe-del-btn');
    var okBtn = wrapper.querySelector('#dpe-ok-btn');
    var filterBtn = wrapper.querySelector('#dpe-filter-btn');
    var searchBtn = wrapper.querySelector('#dpe-search-btn');
    var searchBox = wrapper.querySelector('#dpe-search');
    var searchInput = wrapper.querySelector('#dpe-search input');

    var isOpen = false, data = null;
    var deleteMode = false, lastKeyword = '';
    // 拖动状态:psx/psy=按下时手指位置;baseL/baseT=按下时浮窗位置(只记录一次)
    var psx = 0, psy = 0, baseL = 20, baseT = 300, moved = false, dragging = false;
    var touchLock = false;

    function setPos(l, t) {
        var vw = window.innerWidth, vh = window.innerHeight;
        l = Math.max(0, Math.min(vw - 50, l));
        t = Math.max(0, Math.min(vh - 50, t));
        wrapper.style.left = l + 'px';
        wrapper.style.top = t + 'px';
    }
    function getL() { return parseInt(wrapper.style.left) || 20; }
    function getT() { return parseInt(wrapper.style.top) || 300; }
    setPos(20, 300);

    function openPanel() {
        var vw = window.innerWidth, vh = window.innerHeight;
        var pw = Math.min(380, vw - 16), ph = Math.min(Math.floor(vh * 0.68), 420);
        var r = wrapper.getBoundingClientRect();
        panel.style.width = pw + 'px';
        panel.style.maxHeight = ph + 'px';
        panel.style.top = '56px'; panel.style.bottom = 'auto';
        panel.style.left = '0'; panel.style.right = 'auto';
        if (r.bottom + 8 + ph > vh) { panel.style.top = 'auto'; panel.style.bottom = (vh - r.top) + 8 + 'px'; }
        if (r.left + pw > vw) { panel.style.left = 'auto'; panel.style.right = '0'; }
        panel.style.display = 'flex';
        if (!data) load();
    }
    function closePanel() { isOpen = false; panel.style.display = 'none'; }
    function togglePanel() { isOpen ? closePanel() : (isOpen = true, openPanel()); }

    // ==== 拖拽逻辑(位移=按下时基准+本次增量,跟随手指) ====
    function dragStart(x, y, target) {
        if (panel.contains(target)) return; // 面板内不拖拽,可滚动
        psx = x; psy = y;
        baseL = getL(); baseT = getT();
        moved = false;
        dragging = true;
    }
    function dragMove(x, y, ev) {
        if (!dragging) return;
        var dx = x - psx, dy = y - psy;
        if (!moved && (Math.abs(dx) > 8 || Math.abs(dy) > 8)) moved = true;
        if (moved) {
            if (ev && ev.cancelable) ev.preventDefault();
            setPos(baseL + dx, baseT + dy);
        }
    }
    function dragEnd() {
        var wasMoved = moved;
        dragging = false; moved = false;
        if (wasMoved && isOpen) openPanel();
    }

    // 触摸通道(手机)
    wrapper.addEventListener('touchstart', function(e) {
        touchLock = true;
        dragStart(e.touches[0].clientX, e.touches[0].clientY, e.target);
    }, { passive: true });
    document.addEventListener('touchmove', function(e) {
        if (!dragging) return;
        dragMove(e.touches[0].clientX, e.touches[0].clientY, e);
    }, { passive: false });
    document.addEventListener('touchend', function() {
        dragEnd();
        setTimeout(function() { touchLock = false; }, 350);
    });

    // 鼠标通道(桌面)
    wrapper.addEventListener('mousedown', function(e) {
        if (touchLock) return;
        dragStart(e.clientX, e.clientY, e.target);
    });
    document.addEventListener('mousemove', function(e) {
        if (touchLock || !dragging) return;
        dragMove(e.clientX, e.clientY, null);
    });
    document.addEventListener('mouseup', function() {
        if (touchLock) return;
        dragEnd();
    });

    // ==== 点击开关 ====
    btn.addEventListener('click', function(e) {
        if (moved) { e.preventDefault(); e.stopPropagation(); return; }
        togglePanel();
    });
    panel.addEventListener('click', function(e) { e.stopPropagation(); });
    document.addEventListener('click', function(e) {
        if (!isOpen) return;
        if (e.target === document.body || e.target === document.documentElement) closePanel();
    });

    // ==== 工具函数 ====
    function getPostId() { var m = window.location.pathname.match(/\/posts\/(\d+)/); return m ? m[1] : null; }
    function esc(s) { return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '"'); }
    function toast(msg) {
        var o = document.getElementById('dpe-toast'); if (o) o.remove();
        var t = document.createElement('div'); t.id = 'dpe-toast'; t.textContent = msg;
        document.body.appendChild(t);
        setTimeout(function() { t.style.opacity = '0'; setTimeout(function() { t.remove(); }, 300); }, 1500);
    }
    function fbCopy(text) {
        var ta = document.createElement('textarea');
        ta.value = text; ta.style.cssText = 'position:fixed;opacity:0;top:-100px';
        document.body.appendChild(ta); ta.select();
        document.execCommand('copy'); ta.remove();
        toast('已复制: ' + text);
    }

    async function load() {
        var id = getPostId();
        if (!id) { pbody.innerHTML = '<div style="text-align:center;padding:20px;color:#e74c3c">未获取到帖子ID</div>'; return; }
        pbody.innerHTML = '<div style="text-align:center;padding:20px;color:#999">加载中...</div>';
        var tags;
        try {
            var r = await fetch('https://danbooru.donmai.us/posts/' + id + '.json');
            if (!r.ok) throw 0;
            var d = await r.json(); var p = d.post || d;
            tags = {
                general: (p.tag_string_general || '').split(' ').filter(Boolean),
                artist: (p.tag_string_artist || '').split(' ').filter(Boolean),
                copyright: (p.tag_string_copyright || '').split(' ').filter(Boolean),
                character: (p.tag_string_character || '').split(' ').filter(Boolean),
                meta: (p.tag_string_meta || '').split(' ').filter(Boolean)
            };
        } catch (e) { tags = parseDom(); }
        data = tags; render(tags);
    }

    function parseDom() {
        var t = { general: [], artist: [], copyright: [], character: [], meta: [] };
        document.querySelectorAll('a').forEach(function(a) {
            var tx = a.textContent.trim(), hf = a.getAttribute('href') || '';
            if (!tx || tx.length > 50 || hf.indexOf('/tags/') !== 0) return;
            var p = a.closest('.tag-type-general,.tag-type-artist,.tag-type-copyright,.tag-type-character,.tag-type-meta');
            var c = 'general';
            if (p) {
                if (p.classList.contains('tag-type-artist')) c = 'artist';
                else if (p.classList.contains('tag-type-copyright')) c = 'copyright';
                else if (p.classList.contains('tag-type-character')) c = 'character';
                else if (p.classList.contains('tag-type-meta')) c = 'meta';
            }
            if (t[c].indexOf(tx) === -1) t[c].push(tx);
        });
        return t;
    }

    // 从 data 中移除指定标签
    function removeTag(tag) {
        for (var k in data) {
            var i = data[k].indexOf(tag);
            if (i > -1) { data[k].splice(i, 1); return true; }
        }
        return false;
    }

    // 预设过滤:删除所有黑名单词
    function applyFilter() {
        if (!data) return;
        var removed = [];
        FILTER_WORDS.forEach(function(w) {
            for (var k in data) {
                var i = data[k].indexOf(w);
                while (i > -1) {
                    data[k].splice(i, 1);
                    removed.push(w);
                    i = data[k].indexOf(w);
                }
            }
        });
        render(data);
        if (removed.length) toast('已过滤 ' + removed.length + ' 个标签: ' + removed.join(', '));
        else toast('没有需要过滤的标签');
    }

    // 搜索高亮
    function doSearch(kw) {
        var tgs = pbody.querySelectorAll('.dpe-tg');
        tgs.forEach(function(el) { el.classList.remove('dpe-hl', 'dpe-hl-first'); });
        if (!kw) return;
        var q = kw.toLowerCase(), first = null;
        tgs.forEach(function(el) {
            if (el.textContent.indexOf('×') > -1) {
                if (el.childNodes[0] && el.childNodes[0].textContent.toLowerCase().indexOf(q) > -1) {
                    el.classList.add('dpe-hl');
                    if (!first) { first = el; el.classList.add('dpe-hl-first'); }
                }
            } else if (el.textContent.toLowerCase().indexOf(q) > -1) {
                el.classList.add('dpe-hl');
                if (!first) { first = el; el.classList.add('dpe-hl-first'); }
            }
        });
        if (first) first.scrollIntoView({ block: 'center', behavior: 'smooth' });
    }

    function syncPreview() {
        var sep = ', ';
        var all = [];
        for (var k in data) {
            var arr = data[k].filter(function(x) { return x && x.length > 1; });
            if (arr.length) all.push(arr.join(sep));
        }
        if (preview) preview.textContent = all.join(sep) || '';
    }

    function render(tags) {
        var sep = ', ';
        var names = { general: '通用', artist: '画师', copyright: '版权', character: '角色', meta: '元' };
        var icons = { general: '🏷', artist: '🎨', copyright: '©', character: '👤', meta: '⚙' };
        var html = '';
        for (var k in names) {
            var arr = tags[k].filter(function(x) { return x && x.length > 1; });
            if (!arr.length) continue;
            html += '<div class="dpe-cat"><div class="dpe-cat-t">' + icons[k] + ' ' + names[k] + ' <span class="dpe-cat-c">' + arr.length + '</span></div><div class="dpe-tgs">' +
                arr.map(function(x) {
                    return '<span class="dpe-tg" data-v="' + esc(x) + '">' + esc(x) + (deleteMode ? '<span class="dpe-delx">×</span>' : '') + '</span>';
                }).join('') +
                '</div></div>';
        }
        if (!html) html = '<div style="text-align:center;padding:20px;color:#e74c3c">未找到标签</div>';
        pbody.innerHTML = html;
        syncPreview();

        pbody.querySelectorAll('.dpe-delx').forEach(function(x) {
            x.addEventListener('click', function(e) {
                e.stopPropagation();
                var tg = x.closest('.dpe-tg');
                if (tg) { removeTag(tg.getAttribute('data-v')); render(data); }
            });
        });
        pbody.querySelectorAll('.dpe-tg').forEach(function(el) {
            el.addEventListener('click', function() {
                if (deleteMode) return;
                fbCopy(el.getAttribute('data-v'));
            });
        });
        if (lastKeyword) doSearch(lastKeyword);
    }

    // ==== 工具栏事件 ====
    // 删除模式开关
    delBtn.addEventListener('click', function() {
        deleteMode = !deleteMode;
        delBtn.classList.toggle('on', deleteMode);
        okBtn.style.display = deleteMode ? '' : 'none';
        toast(deleteMode ? '删除模式开启,点击标签旁的 × 删除' : '删除模式已关闭');
        render(data);
    });
    okBtn.addEventListener('click', function() {
        deleteMode = false;
        delBtn.classList.remove('on');
        okBtn.style.display = 'none';
        toast('已确认删除');
        render(data);
    });
    // 预设过滤
    filterBtn.addEventListener('click', function() { applyFilter(); });
    // 搜索
    searchBtn.addEventListener('click', function() {
        var show = searchBox.style.display !== 'block';
        searchBox.style.display = show ? 'block' : 'none';
        if (show) { searchInput.focus(); } else { searchInput.value = ''; lastKeyword = ''; doSearch(''); }
    });
    searchInput.addEventListener('input', function() {
        lastKeyword = this.value.trim();
        doSearch(lastKeyword);
    });

    refreshBtn.addEventListener('click', function() { data = null; load(); });
    copyBtn.addEventListener('click', function() {
        var txt = preview.textContent;
        if (!txt) { toast('没有内容'); return; }
        if (navigator.clipboard && navigator.clipboard.writeText) {
            navigator.clipboard.writeText(txt).then(function() {
                copyBtn.textContent = '✅ 已复制'; toast('已复制!');
                setTimeout(function() { copyBtn.textContent = '📋 复制'; }, 1800);
            }).catch(function() { fbCopy(txt); });
        } else { fbCopy(txt); }
    });

    console.log('[PromptExtractor] v1.0.8 loaded');
})();