Civitai Prompt Autocomplete & Tag Wiki

Adds tag autocomplete and wiki lookup features

目前為 2025-02-16 提交的版本,檢視 最新版本

// ==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或关注我们的公众号极客氢云获取最新地址