Danbooru Prompt Extractor

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

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu budete musieť nainštalovať rozšírenie, ako je napríklad Tampermonkey alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==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');
})();