您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds tag autocomplete and wiki lookup features
当前为
// ==UserScript== // @name Civitai Prompt Autocomplete & Tag Wiki // @namespace http://tampermonkey.net/ // @version 3.1 // @description Adds tag autocomplete and wiki lookup features // @author AndroidXL // @match https://civitai.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=civitai.com // @grant GM.xmlHttpRequest // @grant GM_addStyle // @license MIT // ==/UserScript== (function() { 'use strict'; // All variable declarations moved to top let promptInput = null; let suggestionsBox = null; let currentSuggestions = []; let selectedSuggestionIndex = -1; let debounceTimer; const debounceDelay = 50; let lastCurrentWord = ""; let wikiOverlay = null; let wikiSearchContainer = null; let wikiContent = null; let currentPosts = []; let currentPostIndex = 0; let wikiInitialized = false; const customTags = { 'quality': 'masterpiece, best quality, amazing quality, very detailed', 'quality_pony': 'score_9, score_8_up, score_7_up, score_6_up', // Add more custom tags here following the same format }; // Modify the CSS GM_addStyle(` #autocomplete-suggestions-box { position: absolute; background-color: #1a1b1e; border: 1px solid #333; border-radius: 5px; margin-top: 2px; z-index: 100; overflow-y: auto; max-height: 150px; width: calc(100% - 6px); padding: 2px; box-shadow: 2px 2px 5px rgba(0,0,0,0.3); } #autocomplete-suggestions-box div { padding: 4px 8px; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #C1C2C5; font-size: 14px; } #autocomplete-suggestions-box div:hover { background-color: #282a2d; } .autocomplete-selected { background-color: #383a3e; } .suggestion-count { color: #98C379; font-weight: normal; margin-left: 8px; font-size: 0.9em; } .wiki-search-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 9999; display: none; overflow-y: auto; padding: 20px; } .wiki-search-container { position: relative; width: 90%; max-width: 800px; margin: 40px auto; transition: all 0.3s ease; } .wiki-search-bar { width: 100%; padding: 12px; background: rgba(26,27,30,0.95); border: 1px solid #383a3e; border-radius: 8px; color: #fff; font-size: 16px; } .wiki-content { background: rgba(26,27,30,0.95); border-radius: 8px; margin-top: 20px; padding: 20px; width: 100%; position: relative; } .wiki-text-content { padding-right: 420px; min-height: 500px; word-break: break-word; overflow-wrap: break-word; } .wiki-description { line-height: 1.4; white-space: pre-line; font-size: 15px; } .wiki-image-section { position: absolute; top: 20px; right: 20px; width: 400px; background: rgba(0,0,0,0.2); border-radius: 8px; padding: 10px; display: flex; flex-direction: column; gap: 10px; } .wiki-image-navigation { display: flex; justify-content: space-between; align-items: center; width: 100%; padding: 0 10px; } .image-nav-button { background: rgba(0,0,0,0.5); color: white; border: none; padding: 8px 12px; cursor: pointer; border-radius: 4px; opacity: 0.7; transition: opacity 0.3s; font-size: 16px; } .wiki-image-container { width: 100%; height: 350px; display: flex; justify-content: center; align-items: center; position: relative; margin: 0; background: rgba(0,0,0,0.1); border-radius: 4px; } .wiki-image { max-width: 100%; max-height: 100%; object-fit: contain; border-radius: 4px; } .wiki-nav-buttons { width: 100%; display: flex; justify-content: center; } .wiki-button { padding: 8px 16px; background: #383a3e; border: none; border-radius: 4px; color: #fff; cursor: pointer; width: 100%; text-align: center; } .wiki-tag { display: inline-block; margin: 2px 4px; padding: 2px 4px; background: rgba(97, 175, 239, 0.1); border-radius: 3px; color: #61afef; cursor: pointer; text-decoration: underline; } .wiki-tag:hover { background: rgba(97, 175, 239, 0.2); } .wiki-link { color: #98c379; text-decoration: underline; } .wiki-loading { text-align: center; padding: 20px; } .wiki-description { line-height: 1.6; white-space: pre-wrap; font-size: 15px; } .wiki-description p { margin: 1em 0; } .wiki-search-suggestions { position: fixed; /* Changed from absolute to fixed */ margin-top: 2px; background: rgba(26,27,30,0.95); border: 1px solid #383a3e; border-radius: 0 0 8px 8px; max-height: 200px; overflow-y: auto; z-index: 10001; /* Increased z-index */ width: 90%; max-width: 800px; left: 50%; transform: translateX(-50%); } .wiki-search-suggestion { padding: 8px 12px; cursor: pointer; color: #fff; } .wiki-search-suggestion:hover, .wiki-search-suggestion.selected { background: #383a3e; } .no-images-message { color: #666; text-align: center; padding: 20px; font-style: italic; } @keyframes slideUp { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } // Update header styles .wiki-description h1 { font-size: 1.8em; margin: 0.8em 0 0.4em; } .wiki-description h2 { font-size: 1.6em; margin: 0.7em 0 0.4em; } .wiki-description h3 { font-size: 1.4em; margin: 0.6em 0 0.4em; } .wiki-description h4 { font-size: 1.2em; margin: 0.5em 0 0.4em; } .wiki-description h5 { font-size: 1.1em; margin: 0.5em 0 0.4em; } .wiki-description h6 { font-size: 1em; margin: 0.5em 0 0.4em; } .wiki-description p { margin: 0.5em 0; } .wiki-description ul { margin: 0.5em 0 0.5em 1.5em; padding: 0; } .wiki-description li { margin: 0.3em 0; line-height: 1.4; } `); // Replace all initialization code with this new version function handleInputEvents(e) { const input = e.target; if (input.id === 'input_prompt') { const currentWord = getCurrentWord(input.value, input.selectionStart); lastCurrentWord = currentWord; fetchSuggestions(currentWord); } } function handleKeydownEvents(e) { if (e.target.id !== 'input_prompt') return; if (e.key === 'ArrowDown') { e.preventDefault(); if (suggestionsBox?.style.display === 'block' && currentSuggestions.length > 0) { selectedSuggestionIndex = Math.min(selectedSuggestionIndex + 1, currentSuggestions.length - 1); updateSuggestionSelection(); } } else if (e.key === 'ArrowUp') { e.preventDefault(); if (suggestionsBox?.style.display === 'block' && currentSuggestions.length > 0) { selectedSuggestionIndex = Math.max(selectedSuggestionIndex - 1, -1); updateSuggestionSelection(); } } else if (e.key === 'Tab' || e.key === 'Enter') { if (suggestionsBox?.style.display === 'block' && currentSuggestions.length > 0) { e.preventDefault(); if (selectedSuggestionIndex !== -1) { insertSuggestion(currentSuggestions[selectedSuggestionIndex].label); } else { insertSuggestion(currentSuggestions[0].label); } } } else if (e.key === 'Escape') { clearSuggestions(); } } function setupAutocomplete() { // Clean up old elements if (suggestionsBox) { suggestionsBox.remove(); } promptInput = document.getElementById('input_prompt'); if (!promptInput) return; // Create new suggestions box suggestionsBox = document.createElement('div'); suggestionsBox.id = 'autocomplete-suggestions-box'; suggestionsBox.style.display = 'none'; promptInput.parentNode.insertBefore(suggestionsBox, promptInput.nextSibling); // Remove old event listeners and add new ones using event delegation document.removeEventListener('input', handleInputEvents, true); document.removeEventListener('keydown', handleKeydownEvents, true); document.addEventListener('input', handleInputEvents, true); document.addEventListener('keydown', handleKeydownEvents, true); // Handle clicks outside document.addEventListener('click', (e) => { if (!promptInput?.contains(e.target) && !suggestionsBox?.contains(e.target)) { clearSuggestions(); } }); } // Set up a more aggressive observer const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { const addedNodes = Array.from(mutation.addedNodes); const hasPromptInput = addedNodes.some(node => node.id === 'input_prompt' || node.querySelector?.('#input_prompt') ); if (hasPromptInput || !document.getElementById('autocomplete-suggestions-box')) { setupAutocomplete(); break; } } }); // Start observing with more specific config observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['id'] }); // Initial setup setupAutocomplete(); initializeWiki(); function cleanupAutocomplete() { if (suggestionsBox) { suggestionsBox.remove(); suggestionsBox = null; } // Remove old event listeners if prompt input exists if (promptInput) { const newPromptInput = promptInput.cloneNode(true); promptInput.parentNode.replaceChild(newPromptInput, promptInput); promptInput = null; } } function fetchSuggestions(term) { if (!term) { clearSuggestions(); return; } // First, check custom tags const matchingCustomTags = Object.keys(customTags) .filter(tag => tag.toLowerCase().startsWith(term.toLowerCase())) .map(tag => ({ label: tag, count: '⭐', // Star to indicate custom tag isCustom: true, insertText: customTags[tag] })); // If we have matching custom tags, show them immediately if (matchingCustomTags.length > 0) { currentSuggestions = matchingCustomTags; showSuggestions(); } // Continue with API request for regular tags const apiTerm = term.replace(/ /g, '_'); clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { GM.xmlHttpRequest({ method: 'GET', url: `https://gelbooru.com/index.php?page=autocomplete2&term=${encodeURIComponent(apiTerm)}&type=tag_query&limit=10`, onload: function(response) { if (response.status === 200) { try { const data = JSON.parse(response.responseText); const fetchedSuggestions = data.map(item => ({ label: item.label, count: item.post_count, isCustom: false })); // Combine custom and API suggestions filterAndShowSuggestions([...matchingCustomTags, ...fetchedSuggestions]); } catch (e) { console.error("Error parsing Gelbooru API response:", e); clearSuggestions(); } } else { console.error("Gelbooru API request failed:", response.status, response.statusText); clearSuggestions(); } }, onerror: function(error) { console.error("Gelbooru API request error:", error); clearSuggestions(); } }); }, debounceDelay); } function filterAndShowSuggestions(fetchedSuggestions) { const existingTags = promptInput.value.split(',').map(tag => tag.trim().toLowerCase()); const filteredSuggestions = fetchedSuggestions.filter(suggestion => { return !existingTags.includes(suggestion.label.toLowerCase()); }); currentSuggestions = filteredSuggestions; showSuggestions(); } function showSuggestions() { if (currentSuggestions.length === 0) { clearSuggestions(); return; } suggestionsBox.innerHTML = ''; currentSuggestions.forEach((suggestion, index) => { const suggestionDiv = document.createElement('div'); suggestionDiv.innerHTML = `${suggestion.label} <span class="suggestion-count">[${suggestion.count}]</span>`; suggestionDiv.addEventListener('click', () => { insertSuggestion(suggestion.label); }); suggestionsBox.appendChild(suggestionDiv); }); suggestionsBox.style.display = 'block'; selectedSuggestionIndex = -1; } function clearSuggestions() { if (suggestionsBox) { suggestionsBox.style.display = 'none'; suggestionsBox.innerHTML = ''; } currentSuggestions = []; selectedSuggestionIndex = -1; } function insertSuggestion(suggestion) { const currentPrompt = promptInput.value; const cursorPosition = promptInput.selectionStart; let textBeforeCursor = currentPrompt.substring(0, cursorPosition); const textAfterCursor = currentPrompt.substring(cursorPosition); // Remove the typed prefix (lastCurrentWord) from textBeforeCursor if (lastCurrentWord) { const lastWordIndex = textBeforeCursor.lastIndexOf(lastCurrentWord); if (lastWordIndex !== -1) { textBeforeCursor = textBeforeCursor.substring(0, lastWordIndex); } } // Find the matching suggestion object const suggestionObj = currentSuggestions.find(s => s.label === suggestion); const textToInsert = suggestionObj?.isCustom ? suggestionObj.insertText : suggestion; // Insert suggestion at cursor, preserving newlines promptInput.value = textBeforeCursor + textToInsert + ', ' + textAfterCursor; // Move cursor to the end of the inserted suggestion promptInput.selectionStart = promptInput.selectionEnd = (textBeforeCursor + textToInsert + ', ').length; clearSuggestions(); promptInput.focus(); } function updateSuggestionSelection() { if (!suggestionsBox) return; const suggestionDivs = suggestionsBox.querySelectorAll('div'); suggestionDivs.forEach((div, index) => { if (index === selectedSuggestionIndex) { div.classList.add('autocomplete-selected'); div.scrollIntoView({ block: 'nearest' }); } else { div.classList.remove('autocomplete-selected'); } }); } function getCurrentWord(text, cursorPosition) { if (cursorPosition === undefined) cursorPosition = text.length; const textBeforeCursor = text.substring(0, cursorPosition); const lastCommaIndex = textBeforeCursor.lastIndexOf(','); let currentWord; if (lastCommaIndex !== -1) { currentWord = textBeforeCursor.substring(lastCommaIndex + 1); } else { currentWord = textBeforeCursor; } return currentWord.trim(); } // Add debug logging function function debug(msg) { console.log(`[Wiki Debug] ${msg}`); } // Initialize wiki interface immediately function initializeWiki() { if (wikiInitialized) { debug('Wiki already initialized'); return; } debug('Initializing wiki interface'); wikiOverlay = document.createElement('div'); wikiOverlay.className = 'wiki-search-overlay'; wikiSearchContainer = document.createElement('div'); wikiSearchContainer.className = 'wiki-search-container'; const searchBar = document.createElement('input'); searchBar.className = 'wiki-search-bar'; searchBar.placeholder = 'Search tag wiki...'; wikiContent = document.createElement('div'); wikiContent.className = 'wiki-content'; wikiContent.style.display = 'none'; wikiSearchContainer.appendChild(searchBar); wikiSearchContainer.appendChild(wikiContent); wikiOverlay.appendChild(wikiSearchContainer); document.body.appendChild(wikiOverlay); // Separate 't' key handler document.addEventListener('keydown', function(e) { // debug(`Key pressed: ${e.key}, Input focused: ${isInputFocused()}`); if (e.key === 't' && !isInputFocused()) { debug('T key pressed, showing wiki search'); e.preventDefault(); showWikiSearch(); } }); searchBar.addEventListener('keydown', async function(e) { if (e.key === 'Enter') { e.preventDefault(); await loadWikiInfo(searchBar.value); } else if (e.key === 'Escape') { hideWikiSearch(); } }); wikiOverlay.addEventListener('click', function(e) { if (e.target === wikiOverlay) { hideWikiSearch(); } }); setupWikiSearchAutocomplete(searchBar); wikiInitialized = true; debug('Wiki interface initialized'); } function hideWikiSearch() { debug('Hiding wiki search interface'); wikiOverlay.style.display = 'none'; } // Modified showWikiSearch function function showWikiSearch() { if (!wikiInitialized) { debug('Attempting to show wiki before initialization'); initializeWiki(); } debug('Showing wiki search interface'); wikiOverlay.style.display = 'block'; const searchBar = wikiSearchContainer.querySelector('.wiki-search-bar'); searchBar.value = ''; searchBar.focus(); wikiContent.style.display = 'none'; } // Initialize wiki immediately initializeWiki(); // Wiki helper functions async function loadWikiInfo(tag) { // Reset animation wikiSearchContainer.style.animation = 'none'; wikiSearchContainer.offsetHeight; // Trigger reflow wikiSearchContainer.style.animation = null; // Update search bar value const searchBar = wikiSearchContainer.querySelector('.wiki-search-bar'); searchBar.value = tag; wikiContent.innerHTML = '<div class="wiki-loading">Loading...</div>'; wikiContent.style.display = 'block'; wikiSearchContainer.style.animation = 'slideUp 0.3s forwards'; try { const [wikiData, postsData] = await Promise.all([ fetchDanbooruWiki(tag), fetchDanbooruPosts(tag) ]); currentPosts = postsData; currentPostIndex = 0; displayWikiContent(wikiData, tag); if (currentPosts.length > 0) { displayPostImage(currentPosts[0]); } } catch (error) { wikiContent.innerHTML = `<div class="error">Error loading wiki: ${error.message}</div>`; } } function fetchDanbooruWiki(tag) { // Convert to lowercase and replace spaces with underscores const formattedTag = tag.trim().toLowerCase().replace(/\s+/g, '_'); return new Promise((resolve, reject) => { GM.xmlHttpRequest({ method: 'GET', url: `https://danbooru.donmai.us/wiki_pages.json?search[title]=${encodeURIComponent(formattedTag)}`, onload: response => resolve(JSON.parse(response.responseText)), onerror: reject }); }); } function fetchDanbooruPosts(tag) { const formattedTag = tag.trim().toLowerCase().replace(/\s+/g, '_'); return new Promise((resolve, reject) => { GM.xmlHttpRequest({ method: 'GET', url: `https://danbooru.donmai.us/posts.json?tags=${encodeURIComponent(formattedTag)}&limit=10`, onload: response => resolve(JSON.parse(response.responseText)), onerror: reject }); }); } function displayWikiContent(wikiData, tag) { const hasWiki = wikiData && wikiData[0]; const hasPosts = currentPosts && currentPosts.length > 0; wikiContent.innerHTML = ` <div class="wiki-text-content"> <h2>${tag}</h2> <div class="wiki-description"> ${hasWiki ? `<p>${formatWikiText(wikiData[0].body)}</p>` : `<p>No wiki information available for this tag${hasPosts ? ', but images are available.' : '.'}</p>`} </div> </div> <div class="wiki-image-section"> ${hasPosts ? ` <div class="wiki-image-navigation"> <button class="image-nav-button prev" title="Previous image">←</button> <button class="image-nav-button next" title="Next image">→</button> </div> <div class="wiki-image-container"> <img class="wiki-image" src="" alt="Tag example"> </div> <div class="wiki-nav-buttons"> <button class="wiki-button view-on-danbooru">View on Danbooru</button> </div> ` : ` <div class="no-images-message">No images available for this tag</div> `} </div> `; // Always attach wiki tag event listeners attachWikiEventListeners(); // Only display images if we have posts if (hasPosts) { displayPostImage(currentPosts[0]); } } function formatWikiText(text) { // Remove backticks that sometimes wrap the content text = text.replace(/^`|`$/g, ''); // First handle the complex patterns text = text // Handle list items with proper indentation .replace(/^\* (.+)$/gm, '<li>$1</li>') // Handle Danbooru internal paths (using absolute URLs) .replace(/"([^"]+)":\s*\/((?:[\w-]+\/)*[\w-]+(?:\?[^"\s]+)?)/g, (match, text, path) => { const fullUrl = `https://danbooru.donmai.us/${path.trim()}`; return `<a class="wiki-link" href="${fullUrl}" target="_blank">${text}</a>`; }) // Handle named links with square brackets .replace(/"([^"]+)":\[([^\]]+)\]/g, '<a class="wiki-link" href="$2" target="_blank">$1</a>') // Handle post references .replace(/!post #(\d+)/g, '<a class="wiki-link" href="https://danbooru.donmai.us/posts/$1" target="_blank">post #$1</a>') // Handle external links with proper URL capture (must come before wiki links) .replace(/"([^"]+)":\s*(https?:\/\/[^\s"]+)/g, '<a class="wiki-link" href="$2" target="_blank">$1</a>') // Handle wiki links with display text, preserving special characters .replace(/\[\[([^\]|]+)\|([^\]]+)\]\]/g, (match, tag, display) => { const cleanTag = tag.trim(); return `<span class="wiki-tag" data-tag="${cleanTag}">${display}</span>`; }) // Handle simple wiki links, preserving special characters .replace(/\[\[([^\]]+)\]\]/g, (match, tag) => { const cleanTag = tag.trim(); return `<span class="wiki-tag" data-tag="${cleanTag}">${cleanTag}</span>`; }) // Handle BBCode .replace(/\[b\](.*?)\[\/b\]/g, '<strong>$1</strong>') .replace(/\[i\](.*?)\[\/i\]/g, '<em>$1</em>') .replace(/\[code\](.*?)\[\/code\]/g, '<code>$1</code>') .replace(/\[u\](.*?)\[\/u\]/g, '<u>$1</u>') // Handle headers with proper spacing .replace(/^h([1-6])\.\s*(.+)$/gm, (_, size, content) => `\n<h${size}>${content}</h${size}>\n`) // Add spacing after tag name at start of line // Handle line breaks and paragraphs text = text .replace(/\r\n/g, '\n') // Normalize line endings .replace(/\n\n+/g, '</p><p>') .replace(/\n/g, '<br>'); // Wrap lists in ul tags text = text.replace(/(<li>.*?<\/li>)\s*(?=<li>|$)/gs, '<ul>$1</ul>'); // Wrap in paragraph if not already wrapped if (!text.startsWith('<p>')) { text = `<p>${text}</p>`; } return text; } function isInputFocused() { const activeElement = document.activeElement; return activeElement && ( activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA' || activeElement.isContentEditable ); } // Separate the keyboard handler into its own function function handleWikiKeydown(e) { if (wikiOverlay.style.display === 'block') { if (e.key === 'ArrowLeft') navigateImage(-1); if (e.key === 'ArrowRight') navigateImage(1); } } function attachWikiEventListeners() { const prevButton = wikiContent.querySelector('.image-nav-button.prev'); const nextButton = wikiContent.querySelector('.image-nav-button.next'); const viewButton = wikiContent.querySelector('.view-on-danbooru'); const wikiImage = wikiContent.querySelector('.wiki-image'); const wikiTags = wikiContent.querySelectorAll('.wiki-tag'); // Only attach image navigation related listeners if we have posts if (currentPosts.length > 0) { if (prevButton) { prevButton.addEventListener('click', () => navigateImage(-1)); } if (nextButton) { nextButton.addEventListener('click', () => navigateImage(1)); } // Add keyboard navigation only if we have posts document.removeEventListener('keydown', handleWikiKeydown); document.addEventListener('keydown', handleWikiKeydown); if (wikiImage) { wikiImage.addEventListener('click', () => { if (currentPosts[currentPostIndex]) { window.open(currentPosts[currentPostIndex].large_file_url, '_blank'); } }); } if (viewButton) { viewButton.addEventListener('click', () => { if (currentPosts[currentPostIndex]) { window.open(`https://danbooru.donmai.us/posts/${currentPosts[currentPostIndex].id}`, '_blank'); } }); } } // Wiki tag navigation works regardless of posts if (wikiTags) { wikiTags.forEach(tag => { tag.addEventListener('click', () => { const tagName = tag.dataset.tag; loadWikiInfo(tagName); }); }); } } function displayPostImage(post) { const imageContainer = wikiContent.querySelector('.wiki-image-container'); if (!imageContainer) return; // Guard against missing container if (!post || (!post.preview_file_url && !post.file_url)) return; const prevButton = imageContainer.querySelector('.prev'); const nextButton = imageContainer.querySelector('.next'); const image = imageContainer.querySelector('.wiki-image'); if (!image) return; // Guard against missing image element image.src = post.preview_file_url || post.file_url; if (prevButton) prevButton.style.visibility = currentPostIndex <= 0 ? 'hidden' : 'visible'; if (nextButton) nextButton.style.visibility = currentPostIndex >= currentPosts.length - 1 ? 'hidden' : 'visible'; // Reattach event listeners if (prevButton) prevButton.addEventListener('click', () => navigateImage(-1)); if (nextButton) nextButton.addEventListener('click', () => navigateImage(1)); image.addEventListener('click', () => { window.open(post.large_file_url || post.file_url, '_blank'); }); } function navigateImage(direction) { const newIndex = currentPostIndex + direction; if (newIndex >= 0 && newIndex < currentPosts.length) { currentPostIndex = newIndex; displayPostImage(currentPosts[currentPostIndex]); } } // Add keyboard shortcut for closing with escape document.addEventListener('keydown', e => { if (e.key === 'Escape' && wikiOverlay.style.display === 'block') { hideWikiSearch(); } }); // Add new function for wiki search autocomplete function setupWikiSearchAutocomplete(searchBar) { const suggestionsBox = document.createElement('div'); suggestionsBox.className = 'wiki-search-suggestions'; suggestionsBox.style.display = 'none'; document.body.appendChild(suggestionsBox); // Append to body instead let selectedIndex = -1; // Update suggestions box position when showing function updateSuggestionsPosition() { const searchBarRect = searchBar.getBoundingClientRect(); suggestionsBox.style.top = `${searchBarRect.bottom + window.scrollY}px`; } searchBar.addEventListener('input', () => { const term = searchBar.value.replace(/\s+/g, '_').trim(); if (term) { fetchSuggestionsForWiki(term, suggestionsBox); updateSuggestionsPosition(); } else { suggestionsBox.style.display = 'none'; } }); // Update position on scroll or resize window.addEventListener('scroll', () => { if (suggestionsBox.style.display === 'block') { updateSuggestionsPosition(); } }); window.addEventListener('resize', () => { if (suggestionsBox.style.display === 'block') { updateSuggestionsPosition(); } }); searchBar.addEventListener('keydown', (e) => { const suggestions = suggestionsBox.children; if (suggestions.length === 0) return; if (e.key === 'ArrowDown') { e.preventDefault(); selectedIndex = Math.min(selectedIndex + 1, suggestions.length - 1); updateWikiSuggestionSelection(suggestions, selectedIndex); } else if (e.key === 'ArrowUp') { e.preventDefault(); selectedIndex = Math.max(selectedIndex - 1, -1); updateWikiSuggestionSelection(suggestions, selectedIndex); } else if (e.key === 'Enter' && selectedIndex !== -1) { e.preventDefault(); searchBar.value = suggestions[selectedIndex].textContent; suggestionsBox.style.display = 'none'; loadWikiInfo(searchBar.value); } }); // Close suggestions when clicking outside document.addEventListener('click', (e) => { if (!searchBar.contains(e.target) && !suggestionsBox.contains(e.target)) { suggestionsBox.style.display = 'none'; } }); } function fetchSuggestionsForWiki(term, suggestionsBox) { clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { GM.xmlHttpRequest({ method: 'GET', url: `https://gelbooru.com/index.php?page=autocomplete2&term=${encodeURIComponent(term)}&type=tag_query&limit=10`, onload: function(response) { if (response.status === 200) { try { const data = JSON.parse(response.responseText); showWikiSuggestions(data, suggestionsBox); } catch (e) { console.error("Error parsing suggestions:", e); } } } }); }, debounceDelay); } function showWikiSuggestions(suggestions, suggestionsBox) { suggestionsBox.innerHTML = ''; if (suggestions.length === 0) { suggestionsBox.style.display = 'none'; return; } suggestions.forEach(suggestion => { const div = document.createElement('div'); div.className = 'wiki-search-suggestion'; div.textContent = suggestion.label; div.addEventListener('click', () => { const searchBar = suggestionsBox.parentNode.querySelector('.wiki-search-bar'); searchBar.value = suggestion.label; suggestionsBox.style.display = 'none'; loadWikiInfo(suggestion.label); }); suggestionsBox.appendChild(div); }); suggestionsBox.style.display = 'block'; } function updateWikiSuggestionSelection(suggestions, selectedIndex) { Array.from(suggestions).forEach((suggestion, index) => { suggestion.classList.toggle('selected', index === selectedIndex); if (index === selectedIndex) { suggestion.scrollIntoView({ block: 'nearest' }); } }); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址