Danbooru提示词提取器 - 新增删除模式/过滤/搜索
// ==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, '&').replace(/</g, '<').replace(/>/g, '>').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');
})();