// ==UserScript==
// @name NHentai Search Enhancer
// @namespace http://tampermonkey.net/
// @version 2.0
// @description A single script that includes fuzzy search, library management (import/export), bulk edit, local storage, and a proper minimize fix for NHentai searches.
// @author FunkyJustin
// @match https://nhentai.net/*
// @exclude https://nhentai.net/g/*/*/
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
//----------------------------
// 1) Local Storage + Defaults
//----------------------------
const STORAGE_KEY = 'nhentai_search_enhancer_state';
const defaultState = {
library: [
'doujinshi', 'mature', 'romance', 'yaoi', 'action', 'comedy',
'schoolgirl', 'tentacles', 'yuri', 'bondage', 'big breasts',
'glasses', 'netorare', 'vanilla', 'monster girl'
],
included: [],
excluded: [],
language: 'english'
};
function loadState() {
try {
const saved = JSON.parse(localStorage.getItem(STORAGE_KEY));
if (!saved) return structuredClone(defaultState);
// Merge saved with defaults in case new fields appear
return {
library: Array.isArray(saved.library) ? saved.library : [...defaultState.library],
included: Array.isArray(saved.included) ? saved.included : [],
excluded: Array.isArray(saved.excluded) ? saved.excluded : [],
language: saved.language || 'english'
};
} catch(e) {
return structuredClone(defaultState);
}
}
function saveState() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(appState));
}
let appState = loadState();
//----------------------------
// 2) Styling Helpers
//----------------------------
function styleButton(btn) {
btn.style.border = '1px solid #555';
btn.style.borderRadius = '3px';
btn.style.backgroundColor = '#555';
btn.style.color = '#ddd';
btn.style.padding = '3px 6px';
btn.style.cursor = 'pointer';
btn.style.transition = 'background-color 0.2s, color 0.2s';
btn.addEventListener('mouseenter', () => {
btn.style.backgroundColor = '#666';
});
btn.addEventListener('mouseleave', () => {
btn.style.backgroundColor = '#555';
});
btn.addEventListener('mousedown', () => {
btn.style.backgroundColor = '#777';
});
btn.addEventListener('mouseup', () => {
btn.style.backgroundColor = '#666';
});
}
// Basic fuzzy matching: returns true if all chars in query appear in tag in order
function fuzzyMatch(query, tag) {
let q = query.toLowerCase();
let t = tag.toLowerCase();
let i = 0, j = 0;
while (i < q.length && j < t.length) {
if (q[i] === t[j]) {
i++; j++;
} else {
j++;
}
}
return i === q.length;
}
//----------------------------
// 3) Main Container & Header
//----------------------------
const container = document.createElement('div');
container.style.position = 'fixed';
container.style.top = '10px';
container.style.right = '10px';
container.style.width = '320px';
container.style.minWidth = '250px';
container.style.minHeight = '200px';
container.style.backgroundColor = '#2c2c2c';
container.style.padding = '0';
container.style.border = '1px solid #555';
container.style.borderRadius = '5px';
container.style.zIndex = '10000';
container.style.fontSize = '14px';
container.style.color = '#ddd';
container.style.boxShadow = '0 2px 6px rgba(0,0,0,0.5)';
container.style.userSelect = 'none';
container.style.overflow = 'hidden';
const header = document.createElement('div');
header.style.backgroundColor = '#3a3a3a';
header.style.padding = '8px';
header.style.cursor = 'move';
header.style.display = 'flex';
header.style.justifyContent = 'space-between';
header.style.alignItems = 'center';
header.style.borderBottom = '1px solid #555';
const title = document.createElement('span');
title.textContent = 'NHentai Search Enhancer';
title.style.fontWeight = 'bold';
header.appendChild(title);
// Minimize/Maximize button
const toggleBtn = document.createElement('button');
toggleBtn.textContent = '–';
toggleBtn.style.cursor = 'pointer';
toggleBtn.style.border = 'none';
toggleBtn.style.background = 'transparent';
toggleBtn.style.fontSize = '16px';
toggleBtn.style.lineHeight = '16px';
toggleBtn.style.padding = '0 5px';
toggleBtn.style.color = '#ddd';
header.appendChild(toggleBtn);
container.appendChild(header);
document.body.appendChild(container);
// Draggable logic
let isDragging = false;
let offsetX = 0, offsetY = 0;
header.addEventListener('mousedown', function(e) {
if (e.target !== toggleBtn) {
isDragging = true;
offsetX = e.clientX - container.getBoundingClientRect().left;
offsetY = e.clientY - container.getBoundingClientRect().top;
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', stopDrag);
}
});
function drag(e) {
if (!isDragging) return;
container.style.left = (e.clientX - offsetX) + 'px';
container.style.top = (e.clientY - offsetY) + 'px';
container.style.right = 'auto'; // remove "right"
}
function stopDrag() {
isDragging = false;
document.removeEventListener('mousemove', drag);
document.removeEventListener('mouseup', stopDrag);
}
//----------------------------
// 4) Content + Resizer
//----------------------------
const content = document.createElement('div');
content.style.padding = '10px';
content.style.position = 'relative'; // for auto-suggest alignment
container.appendChild(content);
// Resizer
const resizer = document.createElement('div');
resizer.style.width = '10px';
resizer.style.height = '10px';
resizer.style.background = 'transparent';
resizer.style.position = 'absolute';
resizer.style.right = '0';
resizer.style.bottom = '0';
resizer.style.cursor = 'se-resize';
container.appendChild(resizer);
let isResizing = false;
resizer.addEventListener('mousedown', function(e) {
isResizing = true;
e.stopPropagation();
document.addEventListener('mousemove', resize);
document.addEventListener('mouseup', stopResize);
});
function resize(e) {
if (!isResizing) return;
const newWidth = e.clientX - container.getBoundingClientRect().left;
const newHeight = e.clientY - container.getBoundingClientRect().top;
if (newWidth > 250) container.style.width = newWidth + 'px';
if (newHeight > 150) container.style.height = newHeight + 'px';
}
function stopResize() {
isResizing = false;
document.removeEventListener('mousemove', resize);
document.removeEventListener('mouseup', stopResize);
}
//----------------------------
// 5) Proper Minimize Fix
//----------------------------
let isMinimized = false;
const originalMinHeight = container.style.minHeight || '200px';
toggleBtn.addEventListener('click', function() {
isMinimized = !isMinimized;
if (isMinimized) {
// Hide content & resizer
content.style.display = 'none';
resizer.style.display = 'none';
container.style.minHeight = '0';
container.style.height = header.offsetHeight + 'px';
toggleBtn.textContent = '+';
} else {
// Show content & resizer
content.style.display = 'block';
resizer.style.display = 'block';
container.style.minHeight = originalMinHeight;
container.style.height = '';
toggleBtn.textContent = '–';
}
});
//---------------------------------------------------
// 6) createAutoSuggestField (Include/Exclude Input)
//---------------------------------------------------
function createAutoSuggestField(labelText, placeholderText, onEnterTag) {
const wrapper = document.createElement('div');
wrapper.style.marginBottom = '10px';
wrapper.style.position = 'relative';
const label = document.createElement('label');
label.textContent = labelText;
label.style.display = 'block';
label.style.marginBottom = '5px';
wrapper.appendChild(label);
const input = document.createElement('input');
input.type = 'text';
input.placeholder = placeholderText;
input.style.width = '100%';
input.style.padding = '3px';
input.style.border = '1px solid #555';
input.style.borderRadius = '3px';
input.style.backgroundColor = '#444';
input.style.color = '#ddd';
wrapper.appendChild(input);
const suggestBox = document.createElement('div');
suggestBox.style.position = 'absolute';
suggestBox.style.top = '100%';
suggestBox.style.left = '0';
suggestBox.style.width = '100%';
suggestBox.style.backgroundColor = '#444';
suggestBox.style.border = '1px solid #555';
suggestBox.style.borderTop = 'none';
suggestBox.style.display = 'none';
suggestBox.style.maxHeight = '100px';
suggestBox.style.overflowY = 'auto';
suggestBox.style.zIndex = '9999';
wrapper.appendChild(suggestBox);
// Hide if click outside
document.addEventListener('click', (e) => {
if (!wrapper.contains(e.target)) {
suggestBox.style.display = 'none';
}
});
// Press Enter
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
const val = input.value.trim();
if (val) {
onEnterTag(val);
}
suggestBox.style.display = 'none';
input.value = '';
}
});
// On input, fuzzy search
input.addEventListener('input', function() {
const query = input.value.trim().toLowerCase();
if (!query) {
suggestBox.innerHTML = '';
suggestBox.style.display = 'none';
return;
}
const filtered = appState.library.filter(t => fuzzyMatch(query, t));
if (filtered.length === 0) {
suggestBox.innerHTML = '';
suggestBox.style.display = 'none';
return;
}
suggestBox.innerHTML = '';
filtered.forEach(tag => {
const item = document.createElement('div');
item.textContent = tag;
item.style.padding = '5px';
item.style.borderBottom = '1px solid #555';
item.style.cursor = 'pointer';
item.addEventListener('mouseover', () => {
item.style.backgroundColor = '#555';
});
item.addEventListener('mouseout', () => {
item.style.backgroundColor = '#444';
});
item.addEventListener('mousedown', () => {
onEnterTag(tag);
suggestBox.style.display = 'none';
input.value = '';
});
suggestBox.appendChild(item);
});
suggestBox.style.display = 'block';
});
return { wrapper, input };
}
//----------------------------
// 7) Include & Exclude Fields
//----------------------------
const includeField = createAutoSuggestField(
'Include Tag:',
'Type to include tag...',
(tag) => {
appState.included.push(tag);
saveState();
updatePreview();
}
);
content.appendChild(includeField.wrapper);
const excludeField = createAutoSuggestField(
'Exclude Tag:',
'Type to exclude tag...',
(tag) => {
appState.excluded.push(tag);
saveState();
updatePreview();
}
);
content.appendChild(excludeField.wrapper);
//----------------------------
// 8) Language Dropdown
//----------------------------
const languageWrapper = document.createElement('div');
languageWrapper.style.marginBottom = '10px';
const languageLabel = document.createElement('label');
languageLabel.textContent = 'Language:';
languageLabel.style.display = 'block';
languageLabel.style.marginBottom = '5px';
languageWrapper.appendChild(languageLabel);
const languageSelect = document.createElement('select');
languageSelect.style.width = '100%';
languageSelect.style.padding = '3px';
languageSelect.style.border = '1px solid #555';
languageSelect.style.borderRadius = '3px';
languageSelect.style.backgroundColor = '#444';
languageSelect.style.color = '#ddd';
['none','english','japanese','chinese'].forEach(lang => {
const opt = document.createElement('option');
opt.value = lang;
opt.textContent = lang;
languageSelect.appendChild(opt);
});
languageSelect.value = appState.language || 'english';
languageSelect.addEventListener('change', () => {
appState.language = languageSelect.value;
saveState();
updatePreview();
});
languageWrapper.appendChild(languageSelect);
content.appendChild(languageWrapper);
//----------------------------
// 9) Add Tag to Library
//----------------------------
const librarySection = document.createElement('div');
librarySection.style.marginBottom = '10px';
const addTagLabel = document.createElement('label');
addTagLabel.textContent = 'Add Tag to Library:';
addTagLabel.style.display = 'block';
addTagLabel.style.marginBottom = '5px';
librarySection.appendChild(addTagLabel);
const addTagRow = document.createElement('div');
addTagRow.style.display = 'flex';
addTagRow.style.gap = '5px';
const addTagInput = document.createElement('input');
addTagInput.type = 'text';
addTagInput.placeholder = 'Enter new tag...';
addTagInput.style.flex = '1';
addTagInput.style.padding = '3px';
addTagInput.style.border = '1px solid #555';
addTagInput.style.borderRadius = '3px';
addTagInput.style.backgroundColor = '#444';
addTagInput.style.color = '#ddd';
const addTagButton = document.createElement('button');
addTagButton.textContent = 'Add';
styleButton(addTagButton);
// Press Enter to add new tag
addTagInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
addTagButton.click();
}
});
addTagButton.addEventListener('click', () => {
const newTag = addTagInput.value.trim();
if (newTag && !appState.library.includes(newTag)) {
appState.library.push(newTag);
saveState();
}
addTagInput.value = '';
});
addTagRow.appendChild(addTagInput);
addTagRow.appendChild(addTagButton);
librarySection.appendChild(addTagRow);
const btnSpacing = document.createElement('div');
btnSpacing.style.height = '10px';
librarySection.appendChild(btnSpacing);
//----------------------------
// 10) Manage Library & Bulk Edit
//----------------------------
const manageLibraryBtn = document.createElement('button');
manageLibraryBtn.textContent = 'Manage Library';
styleButton(manageLibraryBtn);
const bulkEditBtn = document.createElement('button');
bulkEditBtn.textContent = 'Bulk Edit';
styleButton(bulkEditBtn);
const libBtnRow = document.createElement('div');
libBtnRow.style.display = 'flex';
libBtnRow.style.gap = '5px';
libBtnRow.appendChild(manageLibraryBtn);
libBtnRow.appendChild(bulkEditBtn);
librarySection.appendChild(libBtnRow);
content.appendChild(librarySection);
//-------------------------------------------------
// 11) Manage Library Modal (Import/Export/Reset)
//-------------------------------------------------
const libraryModal = document.createElement('div');
libraryModal.style.position = 'fixed';
libraryModal.style.top = '0';
libraryModal.style.left = '0';
libraryModal.style.width = '100%';
libraryModal.style.height = '100%';
libraryModal.style.backgroundColor = 'rgba(0,0,0,0.5)';
libraryModal.style.display = 'none';
libraryModal.style.zIndex = '99999';
const modalContent = document.createElement('div');
modalContent.style.position = 'absolute';
modalContent.style.top = '50%';
modalContent.style.left = '50%';
modalContent.style.transform = 'translate(-50%, -50%)';
modalContent.style.backgroundColor = '#2c2c2c';
modalContent.style.padding = '10px';
modalContent.style.border = '1px solid #555';
modalContent.style.borderRadius = '5px';
modalContent.style.width = '300px';
modalContent.style.maxHeight = '450px';
modalContent.style.overflowY = 'auto';
modalContent.style.color = '#ddd';
modalContent.style.position = 'relative';
const titleRow = document.createElement('div');
titleRow.style.display = 'flex';
titleRow.style.justifyContent = 'space-between';
titleRow.style.alignItems = 'center';
titleRow.style.marginBottom = '5px';
titleRow.style.padding = '5px';
titleRow.style.borderBottom = '1px solid #555';
titleRow.style.backgroundColor = '#3a3a3a';
const modalTitle = document.createElement('span');
modalTitle.textContent = 'Manage Library';
modalTitle.style.fontSize = '16px';
modalTitle.style.fontWeight = 'bold';
titleRow.appendChild(modalTitle);
const closeModalX = document.createElement('button');
closeModalX.textContent = '×';
closeModalX.style.border = 'none';
closeModalX.style.background = 'transparent';
closeModalX.style.color = '#f66';
closeModalX.style.cursor = 'pointer';
closeModalX.style.fontSize = '24px';
closeModalX.style.fontWeight = 'bold';
closeModalX.style.padding = '0 5px';
closeModalX.addEventListener('click', () => {
libraryModal.style.display = 'none';
});
titleRow.appendChild(closeModalX);
modalContent.appendChild(titleRow);
// Buttons row: Remove All, Import, Export, Reset to Defaults
const libraryBtnRow = document.createElement('div');
libraryBtnRow.style.display = 'flex';
libraryBtnRow.style.flexWrap = 'wrap';
libraryBtnRow.style.gap = '5px';
libraryBtnRow.style.marginBottom = '5px';
// Remove All
const removeAllBtn = document.createElement('button');
removeAllBtn.textContent = 'Remove All';
styleButton(removeAllBtn);
removeAllBtn.addEventListener('click', () => {
if (confirm('Remove ALL tags from the library?')) {
appState.library = [];
saveState();
updateLibraryList(librarySearchInput.value.trim().toLowerCase());
}
});
libraryBtnRow.appendChild(removeAllBtn);
// Import
const importBtn = document.createElement('button');
importBtn.textContent = 'Import';
styleButton(importBtn);
libraryBtnRow.appendChild(importBtn);
// Export
const exportBtn = document.createElement('button');
exportBtn.textContent = 'Export';
styleButton(exportBtn);
exportBtn.addEventListener('click', () => {
const fileContent = appState.library.join('\n');
const blob = new Blob([fileContent], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'nhentai_library.txt';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
libraryBtnRow.appendChild(exportBtn);
// Reset to Defaults
const resetBtn = document.createElement('button');
resetBtn.textContent = 'Reset to Defaults';
styleButton(resetBtn);
resetBtn.addEventListener('click', () => {
if (confirm('Reset the library to default tags?')) {
appState.library = structuredClone(defaultState.library);
saveState();
updateLibraryList(librarySearchInput.value.trim().toLowerCase());
}
});
libraryBtnRow.appendChild(resetBtn);
modalContent.appendChild(libraryBtnRow);
// Import Section (hidden by default)
const importSection = document.createElement('div');
importSection.style.display = 'none';
importSection.style.marginBottom = '10px';
importSection.style.border = '1px dashed #555';
importSection.style.padding = '5px';
const importTitle = document.createElement('div');
importTitle.textContent = 'Import Tags';
importTitle.style.fontWeight = 'bold';
importTitle.style.marginBottom = '5px';
importSection.appendChild(importTitle);
// 1) File input
const fileLabel = document.createElement('label');
fileLabel.textContent = 'Select .txt file:';
fileLabel.style.display = 'block';
fileLabel.style.marginBottom = '5px';
importSection.appendChild(fileLabel);
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.txt';
fileLabel.appendChild(fileInput);
// 2) Textarea
const textLabel = document.createElement('label');
textLabel.textContent = 'Or paste tags (one per line):';
textLabel.style.display = 'block';
textLabel.style.marginTop = '10px';
importSection.appendChild(textLabel);
const importTextarea = document.createElement('textarea');
importTextarea.style.width = '100%';
importTextarea.style.height = '80px';
importTextarea.style.backgroundColor = '#444';
importTextarea.style.color = '#ddd';
importTextarea.style.border = '1px solid #555';
importTextarea.style.borderRadius = '3px';
importTextarea.style.resize = 'both';
importTextarea.style.overflow = 'auto';
importSection.appendChild(importTextarea);
const processImportBtn = document.createElement('button');
processImportBtn.textContent = 'Process Import';
styleButton(processImportBtn);
processImportBtn.style.marginTop = '10px';
importSection.appendChild(processImportBtn);
modalContent.appendChild(importSection);
importBtn.addEventListener('click', () => {
importSection.style.display =
importSection.style.display === 'none' ? 'block' : 'none';
});
processImportBtn.addEventListener('click', () => {
let linesFromFile = [];
let linesFromTextarea = [];
if (fileInput.files && fileInput.files[0]) {
const file = fileInput.files[0];
const reader = new FileReader();
reader.onload = function(e) {
const content = e.target.result;
linesFromFile = content.split('\n').map(l => l.trim()).filter(Boolean);
doImport();
};
reader.readAsText(file);
} else {
doImport();
}
function doImport() {
linesFromTextarea = importTextarea.value
.split('\n')
.map(l => l.trim())
.filter(Boolean);
const allLines = [...linesFromFile, ...linesFromTextarea];
let addedCount = 0;
allLines.forEach(line => {
if (!appState.library.includes(line)) {
appState.library.push(line);
addedCount++;
}
});
importTextarea.value = '';
fileInput.value = '';
saveState();
updateLibraryList(librarySearchInput.value.trim().toLowerCase());
alert(`Imported ${addedCount} new tags.`);
}
});
// Library Search
const librarySearchInput = document.createElement('input');
librarySearchInput.type = 'text';
librarySearchInput.placeholder = 'Search library... (fuzzy)';
librarySearchInput.style.width = '100%';
librarySearchInput.style.padding = '3px';
librarySearchInput.style.marginBottom = '10px';
librarySearchInput.style.border = '1px solid #555';
librarySearchInput.style.borderRadius = '3px';
librarySearchInput.style.backgroundColor = '#444';
librarySearchInput.style.color = '#ddd';
modalContent.appendChild(librarySearchInput);
const libraryList = document.createElement('div');
modalContent.appendChild(libraryList);
libraryModal.appendChild(modalContent);
document.body.appendChild(libraryModal);
manageLibraryBtn.addEventListener('click', () => {
librarySearchInput.value = '';
updateLibraryList('');
libraryModal.style.display = 'block';
// Hide import area
importSection.style.display = 'none';
importTextarea.value = '';
fileInput.value = '';
});
// Keyboard nav
let searchResults = [];
let selectedIndex = -1;
librarySearchInput.addEventListener('input', () => {
const query = librarySearchInput.value.trim().toLowerCase();
updateLibraryList(query);
});
librarySearchInput.addEventListener('keydown', (e) => {
if (searchResults.length === 0) return;
if (e.key === 'ArrowDown') {
e.preventDefault();
selectedIndex = Math.min(selectedIndex + 1, searchResults.length - 1);
highlightSelected();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
selectedIndex = Math.max(selectedIndex - 1, 0);
highlightSelected();
} else if (e.key === 'Enter' || e.key === 'Delete') {
if (selectedIndex >= 0 && selectedIndex < searchResults.length) {
const tagToRemove = searchResults[selectedIndex];
const idx = appState.library.indexOf(tagToRemove);
if (idx !== -1) {
appState.library.splice(idx, 1);
saveState();
}
updateLibraryList(librarySearchInput.value.trim().toLowerCase());
}
}
});
function updateLibraryList(filterQuery) {
libraryList.innerHTML = '';
let filteredLib = [...appState.library];
if (filterQuery) {
filteredLib = filteredLib.filter(t => fuzzyMatch(filterQuery, t));
}
searchResults = filteredLib;
selectedIndex = -1;
filteredLib.forEach((tag, idx) => {
const row = document.createElement('div');
row.style.display = 'flex';
row.style.justifyContent = 'space-between';
row.style.padding = '5px 0';
row.style.transition = 'background-color 0.2s';
const tagName = document.createElement('span');
tagName.textContent = tag;
row.appendChild(tagName);
const removeBtn = document.createElement('button');
removeBtn.textContent = '×';
removeBtn.style.border = 'none';
removeBtn.style.background = 'transparent';
removeBtn.style.color = '#f66';
removeBtn.style.cursor = 'pointer';
removeBtn.style.fontWeight = 'bold';
removeBtn.style.fontSize = '16px';
removeBtn.addEventListener('click', () => {
const actualIdx = appState.library.indexOf(tag);
if (actualIdx !== -1) {
appState.library.splice(actualIdx, 1);
saveState();
}
updateLibraryList(librarySearchInput.value.trim().toLowerCase());
});
row.appendChild(removeBtn);
libraryList.appendChild(row);
});
}
function highlightSelected() {
[...libraryList.children].forEach((child, idx) => {
child.style.backgroundColor = (idx === selectedIndex) ? '#666' : 'transparent';
});
}
//----------------------------------------
// 12) Bulk Edit Modal (Clear All, Save)
//----------------------------------------
const bulkEditModal = document.createElement('div');
bulkEditModal.style.position = 'fixed';
bulkEditModal.style.top = '0';
bulkEditModal.style.left = '0';
bulkEditModal.style.width = '100%';
bulkEditModal.style.height = '100%';
bulkEditModal.style.backgroundColor = 'rgba(0,0,0,0.5)';
bulkEditModal.style.display = 'none';
bulkEditModal.style.zIndex = '99999';
const bulkModalContent = document.createElement('div');
bulkModalContent.style.position = 'absolute';
bulkModalContent.style.top = '50%';
bulkModalContent.style.left = '50%';
bulkModalContent.style.transform = 'translate(-50%, -50%)';
bulkModalContent.style.backgroundColor = '#2c2c2c';
bulkModalContent.style.padding = '20px';
bulkModalContent.style.border = '1px solid #555';
bulkModalContent.style.borderRadius = '5px';
bulkModalContent.style.width = '400px';
bulkModalContent.style.maxHeight = '500px';
bulkModalContent.style.overflowY = 'auto';
bulkModalContent.style.color = '#ddd';
bulkModalContent.style.position = 'relative';
const bulkTitleRow = document.createElement('div');
bulkTitleRow.style.display = 'flex';
bulkTitleRow.style.justifyContent = 'space-between';
bulkTitleRow.style.alignItems = 'center';
bulkTitleRow.style.marginBottom = '10px';
const bulkTitle = document.createElement('h2');
bulkTitle.textContent = 'Bulk Edit Tags';
bulkTitle.style.margin = '0';
bulkTitleRow.appendChild(bulkTitle);
const bulkCloseX = document.createElement('button');
bulkCloseX.textContent = '×';
bulkCloseX.style.border = 'none';
bulkCloseX.style.background = 'transparent';
bulkCloseX.style.color = '#f66';
bulkCloseX.style.cursor = 'pointer';
bulkCloseX.style.fontSize = '20px';
bulkCloseX.style.fontWeight = 'bold';
bulkCloseX.addEventListener('click', () => {
bulkEditModal.style.display = 'none';
});
bulkTitleRow.appendChild(bulkCloseX);
bulkModalContent.appendChild(bulkTitleRow);
const incLabel = document.createElement('label');
incLabel.textContent = 'Included Tags (one per line):';
bulkModalContent.appendChild(incLabel);
const incTextarea = document.createElement('textarea');
incTextarea.style.width = '100%';
incTextarea.style.height = '80px';
incTextarea.style.marginBottom = '10px';
incTextarea.style.backgroundColor = '#444';
incTextarea.style.color = '#ddd';
incTextarea.style.border = '1px solid #555';
incTextarea.style.borderRadius = '3px';
bulkModalContent.appendChild(incTextarea);
const excLabel = document.createElement('label');
excLabel.textContent = 'Excluded Tags (one per line):';
bulkModalContent.appendChild(excLabel);
const excTextarea = document.createElement('textarea');
excTextarea.style.width = '100%';
excTextarea.style.height = '80px';
excTextarea.style.marginBottom = '10px';
excTextarea.style.backgroundColor = '#444';
excTextarea.style.color = '#ddd';
excTextarea.style.border = '1px solid #555';
excTextarea.style.borderRadius = '3px';
bulkModalContent.appendChild(excTextarea);
const bulkBtnRow = document.createElement('div');
bulkBtnRow.style.display = 'flex';
bulkBtnRow.style.gap = '5px';
bulkBtnRow.style.marginTop = '10px';
const clearAllBtn = document.createElement('button');
clearAllBtn.textContent = 'Clear All Tags';
styleButton(clearAllBtn);
clearAllBtn.addEventListener('click', () => {
appState.included = [];
appState.excluded = [];
saveState();
updateBulkModal();
updatePreview();
});
bulkBtnRow.appendChild(clearAllBtn);
const bulkSaveBtn = document.createElement('button');
bulkSaveBtn.textContent = 'Save';
styleButton(bulkSaveBtn);
bulkSaveBtn.addEventListener('click', () => {
const incLines = incTextarea.value.split('\n').map(t => t.trim()).filter(Boolean);
const excLines = excTextarea.value.split('\n').map(t => t.trim()).filter(Boolean);
appState.included = incLines;
appState.excluded = excLines;
saveState();
updatePreview();
bulkEditModal.style.display = 'none';
});
bulkBtnRow.appendChild(bulkSaveBtn);
bulkModalContent.appendChild(bulkBtnRow);
bulkEditModal.appendChild(bulkModalContent);
document.body.appendChild(bulkEditModal);
bulkEditBtn.addEventListener('click', () => {
updateBulkModal();
bulkEditModal.style.display = 'block';
});
function updateBulkModal() {
incTextarea.value = appState.included.join('\n');
excTextarea.value = appState.excluded.join('\n');
}
//----------------------------
// 13) Search Query Preview
//----------------------------
const queryPreview = document.createElement('div');
queryPreview.style.marginTop = '10px';
queryPreview.style.padding = '5px';
queryPreview.style.backgroundColor = '#333';
queryPreview.style.border = '1px dashed #555';
queryPreview.style.minHeight = '20px';
queryPreview.textContent = 'Search Query Preview: ';
content.appendChild(queryPreview);
const boxesContainer = document.createElement('div');
boxesContainer.style.marginTop = '5px';
content.appendChild(boxesContainer);
function createTagBox(tag, isExclude) {
const box = document.createElement('span');
box.style.display = 'inline-block';
box.style.backgroundColor = '#444';
box.style.padding = '3px 6px';
box.style.margin = '2px';
box.style.borderRadius = '3px';
box.style.border = '1px solid #555';
box.style.transition = 'background-color 0.2s';
const textNode = document.createElement('span');
textNode.textContent = tag;
box.appendChild(textNode);
const removeBtn = document.createElement('button');
removeBtn.textContent = ' ×';
removeBtn.style.marginLeft = '5px';
removeBtn.style.border = 'none';
removeBtn.style.background = 'transparent';
removeBtn.style.cursor = 'pointer';
removeBtn.style.fontWeight = 'bold';
removeBtn.style.color = '#f66';
removeBtn.style.transition = 'color 0.2s';
removeBtn.addEventListener('mouseenter', () => {
removeBtn.style.color = '#faa';
});
removeBtn.addEventListener('mouseleave', () => {
removeBtn.style.color = '#f66';
});
removeBtn.addEventListener('click', () => {
if (!isExclude) {
const idx = appState.included.indexOf(tag);
if (idx !== -1) appState.included.splice(idx, 1);
} else {
const idx = appState.excluded.indexOf(tag);
if (idx !== -1) appState.excluded.splice(idx, 1);
}
saveState();
updatePreview();
});
box.appendChild(removeBtn);
return box;
}
function updatePreview() {
let queryParts = [];
appState.included.forEach(t => queryParts.push(`tag:"${t}"`));
appState.excluded.forEach(t => queryParts.push(`-tag:"${t}"`));
if (appState.language !== 'none') {
queryParts.push(`language:"${appState.language}"`);
}
const finalQuery = queryParts.join(' ');
queryPreview.textContent = 'Search Query Preview: ' + finalQuery;
boxesContainer.innerHTML = '';
// Include
if (appState.included.length > 0) {
const incTitle = document.createElement('div');
incTitle.textContent = 'Include Tags:';
incTitle.style.marginTop = '5px';
incTitle.style.fontWeight = 'bold';
boxesContainer.appendChild(incTitle);
const incBoxRow = document.createElement('div');
appState.included.forEach(tag => {
incBoxRow.appendChild(createTagBox(tag, false));
});
boxesContainer.appendChild(incBoxRow);
}
// Exclude
if (appState.excluded.length > 0) {
const excTitle = document.createElement('div');
excTitle.textContent = 'Exclude Tags:';
excTitle.style.marginTop = '5px';
excTitle.style.fontWeight = 'bold';
boxesContainer.appendChild(excTitle);
const excBoxRow = document.createElement('div');
appState.excluded.forEach(tag => {
excBoxRow.appendChild(createTagBox(tag, true));
});
boxesContainer.appendChild(excBoxRow);
}
}
//----------------------------
// 14) Search Button
//----------------------------
const searchButton = document.createElement('button');
searchButton.textContent = 'Search NHentai';
styleButton(searchButton);
searchButton.style.marginTop = '10px';
searchButton.style.width = '100%';
searchButton.style.padding = '6px';
content.appendChild(searchButton);
searchButton.addEventListener('click', () => {
let queryParts = [];
appState.included.forEach(t => queryParts.push(`tag:"${t}"`));
appState.excluded.forEach(t => queryParts.push(`-tag:"${t}"`));
if (appState.language !== 'none') {
queryParts.push(`language:"${appState.language}"`);
}
const finalQuery = queryParts.join(' ');
const encodedQuery = encodeURIComponent(finalQuery);
window.location.href = `https://nhentai.net/search/?q=${encodedQuery}`;
});
//----------------------------
// 15) Final Initialization
//----------------------------
// Restore language, update preview
languageSelect.value = appState.language || 'english';
updatePreview();
// Re-check preview on blur
includeField.input.addEventListener('blur', updatePreview);
excludeField.input.addEventListener('blur', updatePreview);
})();