您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
使用所有Danbooru Tag进行联想,并附带部分中文翻译和中文联想功能。
// ==UserScript== // @name NovelAI Tag联想 (Danbooru-API版) // @namespace http://tampermonkey.net/ // @version 2.0 // @description 使用所有Danbooru Tag进行联想,并附带部分中文翻译和中文联想功能。 // @author Takoro // @match https://novelai.net/image* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_openInTab // @grant GM_getValue // @grant GM_setValue // @connect danbooru.donmai.us // @connect raw.githubusercontent.com // @license MIT // ==/UserScript== // Gemini YES Gemini YES (function() { 'use strict'; // --- 配置项 --- const MAX_SUGGESTIONS = 10; const DEBOUNCE_DELAY = 250; const API_BASE_URL = 'https://danbooru.donmai.us'; const TRANSLATION_URL = 'https://raw.githubusercontent.com/Yellow-Rush/zh_CN-Tags/main/danbooru.csv'; const CACHE_KEY = 'danbooru_translations_cache'; const CACHE_DURATION_MS = 7 * 24 * 60 * 60 * 1000; const HINT_MESSAGES = [ "- 按住Ctrl+左键单击Tag可以打开Wiki页面。", "- 标紫的是系列/IP名,标绿的是角色名哦(使用英文查询时)。", "- Tag后面是数字是对应图片数量~", "- Tab和Enter以及方向键都可以试试看。", "- Takoro" ]; const TITLE_COLOR = '#e3dccc'; // --- 全局变量 --- let popup = null; let selectedIndex = -1; let currentMatches = []; let isPopupActive = false; let apiAbortController = null; let activeAutocompletion = null; // 存储当前自动完成的上下文:{ start: number, end: number } let translations = new Map(); // --- 样式注入 --- GM_addStyle(` .autocomplete-container { position: absolute; background: #0e0f21; border: 1px solid #3B3B52; border-radius: 8px; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3); max-height: 450px; display: flex; flex-direction: column; z-index: 100000; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; color: #EAEAEB; min-width: 350px; max-width: 500px; } .suggestion-info-display { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; background: #1a1c30; border-bottom: 1px solid #3B3B52; font-size: 13px; line-height: 1.6; min-height: 17px; font-weight: bold; } .info-hint { color: #8a8a9e; font-weight: normal; font-size: 12px; margin-left: 10px; white-space: nowrap; } .info-title { color: ${TITLE_COLOR}; } .suggestion-scroll-wrapper { padding: 7px; overflow-y: auto; } .suggestion-flex { display: flex; flex-wrap: wrap; gap: 6px; } .suggestion-item { display: flex; align-items: center; justify-content: space-between; padding: 4px 10px; background: #22253f; border: 1px solid transparent; border-radius: 4px; cursor: pointer; transition: all 0.2s ease; font-size: 13px; height: 26px; white-space: nowrap; flex-shrink: 0; } .suggestion-item:hover, .suggestion-item.selected { background: #34395f; border-color: #F5F3C2; } .suggestion-text { overflow: hidden; text-overflow: ellipsis; color: #EAEAEB; } .suggestion-count { color: #8a8a9e; margin-left: 12px; font-size: 12px; } .suggestion-item[data-category='4'] .suggestion-text { color: #a6f3a6; } /* Character */ .suggestion-item[data-category='3'] .suggestion-text { color: #d6bcf5; } /* Copyright/Series */ `); // --- 核心功能函数 --- function loadTranslations() { const cachedData = GM_getValue(CACHE_KEY); if (cachedData && cachedData.timestamp && (Date.now() - cachedData.timestamp < CACHE_DURATION_MS)) { translations = new Map(cachedData.translations); return; } GM_xmlhttpRequest({ method: "GET", url: TRANSLATION_URL, onload: function(response) { if (response.status === 200) { const lines = response.responseText.split('\n').filter(line => line.trim()); lines.forEach(line => { const columns = line.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/); if (columns.length >= 2) { const en = (columns[0] || '').trim().replace(/^"|"$/g, ''); const zh = (columns[1] || '').trim().replace(/^"|"$/g, ''); if (en && zh) { translations.set(en, zh); } } }); GM_setValue(CACHE_KEY, { translations: Array.from(translations.entries()), timestamp: Date.now() }); } } }); } function openWikiPage(tagName) { if (!tagName) return; GM_openInTab(`${API_BASE_URL}/wiki_pages/show_or_new?title=${tagName}`, { active: true }); } function getRandomHint() { return Math.random() < 0.5 ? "" : HINT_MESSAGES[Math.floor(Math.random() * HINT_MESSAGES.length)]; } function searchLocalSuggestions(query, input) { if (!translations.size) return; const queryLower = query.toLowerCase(); const rankedItems = []; for (const [en, zh] of translations.entries()) { const zhLower = zh.toLowerCase(); let score = 0; if (zhLower.startsWith(queryLower)) score = 1; else if (zhLower.includes(queryLower)) score = 2; if (score > 0) rankedItems.push({ score: score, data: { en: en, count: undefined, category: 0 } }); } const finalMatches = rankedItems.sort((a, b) => a.score - b.score).map(item => item.data).slice(0, MAX_SUGGESTIONS); updatePopup(input, finalMatches); } function fetchSuggestions(query, input) { if (apiAbortController) apiAbortController.abort(); apiAbortController = new AbortController(); const params = new URLSearchParams({'search[name_matches]': `${query}*`, 'search[order]': 'count', 'limit': MAX_SUGGESTIONS, 'search[hide_empty]': 'true' }); GM_xmlhttpRequest({ method: "GET", url: `${API_BASE_URL}/tags.json?${params.toString()}`, signal: apiAbortController.signal, onload: function(response) { if (response.status === 200) { const matches = JSON.parse(response.responseText).map(tag => ({ en: tag.name, count: tag.post_count, category: tag.category })); updatePopup(input, matches); } else { hidePopup(); } }, onerror: () => { hidePopup(); }, }); } function updatePopup(input, matches) { createPopup(); const hintText = getRandomHint(); const hintHTML = hintText ? `<span class="info-hint">${hintText}</span>` : ''; popup.innerHTML = `<div class="suggestion-info-display"><span class="info-title">Did you mean...?</span>${hintHTML}</div><div class="suggestion-scroll-wrapper"><div class="suggestion-flex"></div></div>`; const flexContainer = popup.querySelector('.suggestion-flex'); currentMatches = matches; if (matches.length === 0) { hidePopup(); return; } matches.forEach((tag, index) => { const item = document.createElement('div'); item.className = 'suggestion-item'; item.dataset.category = tag.category || '0'; const enText = tag.en.replace(/_/g, ' '); const zhText = translations.get(tag.en); const displayText = zhText ? `${enText} (${zhText})` : enText; let countHTML = ''; if (tag.count !== undefined && tag.count !== null) { const countText = tag.count > 1000 ? `${(tag.count / 1000).toFixed(1)}k` : tag.count; countHTML = `<span class="suggestion-count">${countText}</span>`; } item.innerHTML = `<span class="suggestion-text">${displayText}</span>${countHTML}`; item.addEventListener('mouseover', () => { if (selectedIndex !== index) { selectedIndex = index; updateSelectionUI(); }}); item.addEventListener('mousedown', (e) => { e.preventDefault(); if (e.ctrlKey) { openWikiPage(tag.en); } else if (activeAutocompletion) { insertTag(tag.en); } }); flexContainer.appendChild(item); }); positionPopup(input); popup.style.display = 'flex'; isPopupActive = true; if (matches.length > 0) { selectedIndex = 0; updateSelectionUI(); } } function insertTag(tag) { if (!activeAutocompletion) return; const input = getActiveInputElement(); if (!input) { hidePopup(); return; } const range = createRangeFromOffsets(input, activeAutocompletion.start, activeAutocompletion.end); if (!range) { hidePopup(); return; } tag = tag.replace(/_/g, ' '); const fullText = input.textContent; const textBefore = fullText.substring(0, activeAutocompletion.start); const textAfter = fullText.substring(activeAutocompletion.end); let textToInsert = tag; const lastCharBefore = textBefore.slice(-1); if (lastCharBefore && !/[\s({[]/.test(lastCharBefore)) { textToInsert = ' ' + textToInsert; } if (!/^\s*[,)\]}:]/.test(textAfter)) { textToInsert += ', '; } const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); range.deleteContents(); const textNode = document.createTextNode(textToInsert); range.insertNode(textNode); range.setStartAfter(textNode); range.collapse(true); selection.removeAllRanges(); selection.addRange(range); hidePopup(); } // --- 通用与辅助函数 --- function createPopup() { if (!popup) { popup = document.createElement('div'); popup.className = 'autocomplete-container'; document.body.appendChild(popup); } } function hidePopup() { if (popup) popup.style.display = 'none'; if (apiAbortController) apiAbortController.abort(); selectedIndex = -1; isPopupActive = false; currentMatches = []; activeAutocompletion = null; } const debounce = (func, delay) => { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => func(...args), delay); }; }; function getActiveInputElement() { const selection = window.getSelection(); if (!selection.rangeCount) return null; const node = selection.focusNode; const pElement = node.nodeType === 3 ? node.parentElement.closest('p') : node.closest('p'); if (pElement && (pElement.closest('[class*="prompt-input"]') || pElement.closest('[class*="character-prompt-input"]'))) { return pElement; } return null; } function updateSelectionUI() { const items = popup.querySelectorAll('.suggestion-item'); items.forEach((item, index) => item.classList.toggle('selected', index === selectedIndex)); const selectedEl = popup.querySelector('.selected'); if (selectedEl) selectedEl.scrollIntoView({ block: 'nearest' }); } function createRangeFromOffsets(element, startOffset, endOffset) { const range = document.createRange(); range.selectNode(element); range.collapse(true); const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false); let charCount = 0; let startNode, endNode, startIdx, endIdx; while (walker.nextNode()) { const node = walker.currentNode; const nodeLength = node.textContent.length; if (!startNode && startOffset <= charCount + nodeLength) { startNode = node; startIdx = startOffset - charCount; } if (!endNode && endOffset <= charCount + nodeLength) { endNode = node; endIdx = endOffset - charCount; break; } charCount += nodeLength; } if (startNode && endNode) { range.setStart(startNode, startIdx); range.setEnd(endNode, endIdx); return range; } return null; } function positionPopup(input) { let rect; if (activeAutocompletion) { const range = createRangeFromOffsets(input, activeAutocompletion.start, activeAutocompletion.end); if (range) { if (range.collapsed) { const tempSpan = document.createElement('span'); tempSpan.textContent = '\u200b'; range.insertNode(tempSpan); rect = tempSpan.getBoundingClientRect(); tempSpan.remove(); } else { rect = range.getBoundingClientRect(); } } } if (!rect || (rect.width === 0 && rect.height === 0)) { rect = input.getBoundingClientRect(); } popup.style.top = `${rect.bottom + window.scrollY + 5}px`; popup.style.left = `${rect.left + window.scrollX}px`; } const handleInput = debounce((e) => { const input = getActiveInputElement(); if (!input) { hidePopup(); return; } const selection = window.getSelection(); if (!selection.rangeCount || !selection.isCollapsed) { hidePopup(); return; } const caretRange = selection.getRangeAt(0); const precedingRange = document.createRange(); precedingRange.selectNodeContents(input); precedingRange.setEnd(caretRange.startContainer, caretRange.startOffset); const textBeforeCursor = precedingRange.toString(); const lastDelimiterIndex = Math.max(textBeforeCursor.lastIndexOf(','), textBeforeCursor.lastIndexOf(':'), textBeforeCursor.lastIndexOf('['), textBeforeCursor.lastIndexOf('('), textBeforeCursor.lastIndexOf('{'), textBeforeCursor.lastIndexOf('.')); const untrimmedQuery = textBeforeCursor.substring(lastDelimiterIndex + 1); const currentQuery = untrimmedQuery.trim(); if (currentQuery.length < 1 || /^[\d.]+$/.test(currentQuery)) { hidePopup(); return; } const cursorEndOffset = textBeforeCursor.length; const cursorStartOffset = cursorEndOffset - untrimmedQuery.length; activeAutocompletion = { start: cursorStartOffset, end: cursorEndOffset }; if (/[\u4e00-\u9fa5]/.test(currentQuery)) { searchLocalSuggestions(currentQuery, input); } else { fetchSuggestions(currentQuery, input); } }, DEBOUNCE_DELAY); function handleKeydown(e) { if (!isPopupActive) return; const keyMap = { 'ArrowDown': 1, 'ArrowRight': 1, 'ArrowUp': -1, 'ArrowLeft': -1 }; if (keyMap[e.key] !== undefined) { e.preventDefault(); selectedIndex = (selectedIndex + keyMap[e.key] + currentMatches.length) % currentMatches.length; updateSelectionUI(); } else if (e.key === 'Enter' || e.key === 'Tab') { e.preventDefault(); e.stopPropagation(); if (selectedIndex >= 0 && currentMatches[selectedIndex] && activeAutocompletion) { insertTag(currentMatches[selectedIndex].en); } else { hidePopup(); } } else if (e.key === 'Escape') { e.preventDefault(); hidePopup(); } } function handleClickOutside(e) { const input = getActiveInputElement(); if (isPopupActive && popup && !popup.contains(e.target) && (!input || !input.contains(e.target))) { hidePopup(); } } function init() { console.log('NovelAI Tag联想 (Danbooru-API版) 加载成功~'); loadTranslations(); document.addEventListener('input', handleInput); document.addEventListener('keydown', handleKeydown, true); document.addEventListener('mousedown', handleClickOutside); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();