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