// ==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' });
}
});
}
})();