您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Extract tags from e621 images
// ==UserScript== // @name e621 Tag Extract - API solid // @namespace http://tampermonkey.net/ // @version 2.0 // @description Extract tags from e621 images // @author cemtrex (partly with AI coding assistant) // @match https://e621.net/posts/* // @license MIT // @run-at document-idle // ==/UserScript== (function () { 'use strict'; const BTN_ID = 'e621TagExtractButton'; const MODAL_ID = 'e621TagExtractModal'; const PREF_KEY = 'e621TagExtractPrefs_v2'; const DEFAULT_PREFS = { wordStyle: 'underscores', separator: ', ', preselectGroups: ['general','species','character','artist','copyright','meta','lore','invalid'] }; const unique = (arr) => Array.from(new Set(arr)); const loadPrefs = () => { try { return Object.assign({}, DEFAULT_PREFS, JSON.parse(localStorage.getItem(PREF_KEY) || '{}')); } catch { return { ...DEFAULT_PREFS }; } }; const savePrefs = (p) => { try { localStorage.setItem(PREF_KEY, JSON.stringify(p)); } catch {} }; function copyToClipboard(text) { if (navigator.clipboard && window.isSecureContext) { return navigator.clipboard.writeText(text).catch(() => fallbackCopy(text)); } return Promise.resolve(fallbackCopy(text)); } function fallbackCopy(text) { const ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.left = '-9999px'; ta.setAttribute('readonly', ''); document.body.appendChild(ta); ta.select(); try { document.execCommand('copy'); } catch {} ta.remove(); } async function getTagsFromAPI() { const m = location.pathname.match(/\/posts\/(\d+)/); if (!m) return { flat: [], grouped: {}, order: [] }; const postId = m[1]; try { const res = await fetch(`/posts/${postId}.json`, { headers: { 'Accept': 'application/json' }, credentials: 'same-origin', cache: 'no-store' }); if (!res.ok) return { flat: [], grouped: {}, order: [] }; const data = await res.json(); const post = data.post || data; const tagGroups = post.tags || {}; const grouped = Array.isArray(tagGroups) ? { general: tagGroups.map(String) } : Object.fromEntries(Object.entries(tagGroups).map(([k,v])=>[k,(v||[]).map(String)])); const preferred = ['artist','species','character','copyright','general','meta','lore','invalid']; const order = Object.keys(grouped).sort((a,b) => { const ia = preferred.indexOf(a); const ib = preferred.indexOf(b); const sa = ia === -1 ? 1e9 : ia; const sb = ib === -1 ? 1e9 : ib; return (sa - sb) || a.localeCompare(b); }); const flat = unique(Object.values(grouped).flat().map(t => t.trim()).filter(Boolean)); return { flat, grouped, order }; } catch (e) { console.error('[e621 Tag Extract] API error:', e); return { flat: [], grouped: {}, order: [] }; } } function buildButton() { const button = document.createElement('button'); button.id = BTN_ID; button.type = 'button'; button.textContent = '+'; button.title = 'Extract, review, and copy tags'; button.setAttribute('aria-label', 'Extract, review, and copy tags'); Object.assign(button.style, { position: 'fixed', bottom: '20px', right: '20px', backgroundColor: '#2563eb', color: '#fff', border: 'none', borderRadius: '50%', width: '50px', height: '50px', textAlign: 'center', fontSize: '24px', lineHeight: '50px', cursor: 'pointer', boxShadow: '2px 2px 6px rgba(0,0,0,0.3)', zIndex: 2147483647 }); return button; } function makeModal(tagsInfo) { const prefs = loadPrefs(); const overlay = document.createElement('div'); overlay.id = MODAL_ID; Object.assign(overlay.style, { position: 'fixed', inset: '0', background: 'rgba(0,0,0,0.45)', zIndex: 2147483647, display: 'flex', alignItems: 'center', justifyContent: 'center' }); const modal = document.createElement('div'); Object.assign(modal.style, { background: '#111827', color: '#e5e7eb', width: 'min(920px, 94vw)', maxHeight: '86vh', overflow: 'hidden', borderRadius: '10px', boxShadow: '0 10px 30px rgba(0,0,0,0.5)', display: 'flex', flexDirection: 'column', border: '1px solid #1f2937' }); const header = document.createElement('div'); header.textContent = 'e621 Tag Extract'; Object.assign(header.style, { padding: '12px 16px', background: '#0b1220', fontWeight: '600', position:'sticky', top:'0', zIndex:'2' }); const subline = document.createElement('div'); subline.textContent = 'Check or uncheck the tags below to include or exclude them before copying:'; Object.assign(subline.style, { padding: '8px 16px', fontSize: '13px', opacity: '0.8', borderBottom:'1px solid #1f2937' }); const content = document.createElement('div'); Object.assign(content.style, { display: 'grid', gridTemplateColumns: '300px 1fr', gap: '12px', padding: '12px 16px', overflow: 'hidden' }); const controls = document.createElement('div'); Object.assign(controls.style, { position: 'sticky', top: '48px', alignSelf: 'start' }); controls.innerHTML = ` <style> #${MODAL_ID} label { white-space: normal; word-break: break-word; display: block; line-height: 1.4; } #${MODAL_ID} input[type=radio], #${MODAL_ID} input[type=checkbox] { margin-right:4px; } </style> <div style="margin-bottom:10px"> <div style="font-weight:600;margin-bottom:6px">Format</div> <label><input type="radio" name="wordStyle" value="underscores"> Keep underscores</label> <label><input type="radio" name="wordStyle" value="spaces"> Replace underscores with spaces</label> </div> <div style="margin:10px 0"> <div style="font-weight:600;margin-bottom:6px">Groups</div> ${['general','species','character','artist','copyright','meta','lore','invalid'].map(g=>` <label><input type="checkbox" name="grp" value="${g}" checked> ${g}</label> `).join('')} </div> `; const rightCol = document.createElement('div'); Object.assign(rightCol.style, { maxHeight: '72vh', overflowY: 'auto', border: '1px solid #1f2937', borderRadius: '8px', padding: '8px' }); const footer = document.createElement('div'); Object.assign(footer.style, { display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '10px', padding: '12px 16px', background: '#0b1220', borderTop: '1px solid #1f2937', position:'sticky', bottom:'0', zIndex:'2' }); const cancelBtn = document.createElement('button'); cancelBtn.textContent = 'Cancel'; const copyBtn = document.createElement('button'); copyBtn.textContent = 'Copy'; for (const b of [cancelBtn, copyBtn]) Object.assign(b.style, { padding: '8px 12px', borderRadius: '8px', border: '1px solid #374151', background: '#1f2937', color: '#e5e7eb', cursor: 'pointer' }); Object.assign(copyBtn.style, { background: '#2563eb', borderColor: '#1d4ed8' }); content.append(controls, rightCol); modal.append(header, subline, content, footer); footer.append(cancelBtn, copyBtn); overlay.appendChild(modal); const { grouped, order } = tagsInfo; const allItems = []; for (const g of order) { const tags = grouped[g] || []; if (!tags.length) continue; const details = document.createElement('details'); details.open = true; details.style.marginBottom = '10px'; const summary = document.createElement('summary'); summary.textContent = `${g} (${tags.length})`; Object.assign(summary.style, { cursor: 'pointer', fontWeight: '600', padding:'4px 2px' }); details.appendChild(summary); const section = document.createElement('div'); section.style.paddingLeft = '8px'; for (const t of tags) { const row = document.createElement('label'); Object.assign(row.style, { display: 'grid', gridTemplateColumns: 'auto 1fr', gap: '8px', alignItems: 'center', padding: '2px 0' }); row.dataset.group = g; row.dataset.tag = t; const cb = document.createElement('input'); cb.type = 'checkbox'; cb.checked = true; const name = document.createElement('span'); name.textContent = t; row.append(cb, name); section.appendChild(row); allItems.push(row); } details.appendChild(section); rightCol.appendChild(details); } controls.querySelector(`input[name="wordStyle"][value="${prefs.wordStyle}"]`).checked = true; function syncGroupChecks() { const allowed = new Set(Array.from(controls.querySelectorAll('input[name="grp"]:checked')).map(i=>i.value)); for (const r of allItems) { const cb = r.querySelector('input[type="checkbox"]'); if (!cb._touched) cb.checked = allowed.has(r.dataset.group); } } controls.addEventListener('change', (e)=>{ if (e.target && e.target.name === 'grp') syncGroupChecks(); savePrefs(currentPrefs()); }); function currentPrefs() { const wordStyle = controls.querySelector('input[name="wordStyle"]:checked')?.value || 'underscores'; const separator = DEFAULT_PREFS.separator; const preselectGroups = Array.from(controls.querySelectorAll('input[name="grp"]:checked')).map(i=>i.value); return { wordStyle, separator, preselectGroups }; } rightCol.addEventListener('change', (e)=>{ if (e.target && e.target.type === 'checkbox') e.target._touched = true; }); cancelBtn.onclick = () => overlay.remove(); overlay.addEventListener('click', (e)=>{ if (e.target === overlay) overlay.remove(); }); document.addEventListener('keydown', function esc(e){ if (e.key==='Escape'){ overlay.remove(); document.removeEventListener('keydown', esc); } }); copyBtn.onclick = async () => { const prefsNow = currentPrefs(); savePrefs(prefsNow); let selected = allItems.filter(r=>r.querySelector('input').checked).map(r=>r.dataset.tag); if (prefsNow.wordStyle === 'spaces') selected = selected.map(t=>t.replace(/_/g,' ')); const text = selected.join(prefsNow.separator); if (!text) { overlay.remove(); return; } await copyToClipboard(text); overlay.remove(); console.log('[e621 Tag Extract] Copied', selected.length, 'tags'); }; return overlay; } async function handleClick() { const info = await getTagsFromAPI(); if (!info.flat.length) { const anchors = document.querySelectorAll('a.search-tag, aside a[href^="/posts?tags="]'); const tags = Array.from(anchors).map(a => (a.textContent || '').trim()).filter(Boolean); info.grouped = { general: tags }; info.order = ['general']; info.flat = unique(tags); } if (!info.flat.length) return; const modal = makeModal(info); document.body.appendChild(modal); } function addButtonOnce() { if (document.getElementById(BTN_ID)) return; const btn = buildButton(); btn.addEventListener('click', handleClick); document.body.appendChild(btn); } function init() { if (document.readyState === 'complete' || document.readyState === 'interactive') { addButtonOnce(); } else { document.addEventListener('DOMContentLoaded', addButtonOnce, { once: true }); } const mo = new MutationObserver(() => addButtonOnce()); mo.observe(document.documentElement, { childList: true, subtree: true }); } init(); })();