NHentai Search Enhancer

A single script that includes fuzzy search, library management (import/export), bulk edit, local storage, and a proper minimize fix for NHentai searches.

  1. // ==UserScript==
  2. // @name NHentai Search Enhancer
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.0
  5. // @description A single script that includes fuzzy search, library management (import/export), bulk edit, local storage, and a proper minimize fix for NHentai searches.
  6. // @author FunkyJustin
  7. // @match https://nhentai.net/*
  8. // @exclude https://nhentai.net/g/*/*/
  9. // @grant none
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. //----------------------------
  17. // 1) Local Storage + Defaults
  18. //----------------------------
  19. const STORAGE_KEY = 'nhentai_search_enhancer_state';
  20. const defaultState = {
  21. library: [
  22. 'doujinshi', 'mature', 'romance', 'yaoi', 'action', 'comedy',
  23. 'schoolgirl', 'tentacles', 'yuri', 'bondage', 'big breasts',
  24. 'glasses', 'netorare', 'vanilla', 'monster girl'
  25. ],
  26. included: [],
  27. excluded: [],
  28. language: 'english'
  29. };
  30.  
  31. function loadState() {
  32. try {
  33. const saved = JSON.parse(localStorage.getItem(STORAGE_KEY));
  34. if (!saved) return structuredClone(defaultState);
  35. // Merge saved with defaults in case new fields appear
  36. return {
  37. library: Array.isArray(saved.library) ? saved.library : [...defaultState.library],
  38. included: Array.isArray(saved.included) ? saved.included : [],
  39. excluded: Array.isArray(saved.excluded) ? saved.excluded : [],
  40. language: saved.language || 'english'
  41. };
  42. } catch(e) {
  43. return structuredClone(defaultState);
  44. }
  45. }
  46.  
  47. function saveState() {
  48. localStorage.setItem(STORAGE_KEY, JSON.stringify(appState));
  49. }
  50.  
  51. let appState = loadState();
  52.  
  53. //----------------------------
  54. // 2) Styling Helpers
  55. //----------------------------
  56. function styleButton(btn) {
  57. btn.style.border = '1px solid #555';
  58. btn.style.borderRadius = '3px';
  59. btn.style.backgroundColor = '#555';
  60. btn.style.color = '#ddd';
  61. btn.style.padding = '3px 6px';
  62. btn.style.cursor = 'pointer';
  63. btn.style.transition = 'background-color 0.2s, color 0.2s';
  64.  
  65. btn.addEventListener('mouseenter', () => {
  66. btn.style.backgroundColor = '#666';
  67. });
  68. btn.addEventListener('mouseleave', () => {
  69. btn.style.backgroundColor = '#555';
  70. });
  71. btn.addEventListener('mousedown', () => {
  72. btn.style.backgroundColor = '#777';
  73. });
  74. btn.addEventListener('mouseup', () => {
  75. btn.style.backgroundColor = '#666';
  76. });
  77. }
  78.  
  79. // Basic fuzzy matching: returns true if all chars in query appear in tag in order
  80. function fuzzyMatch(query, tag) {
  81. let q = query.toLowerCase();
  82. let t = tag.toLowerCase();
  83. let i = 0, j = 0;
  84. while (i < q.length && j < t.length) {
  85. if (q[i] === t[j]) {
  86. i++; j++;
  87. } else {
  88. j++;
  89. }
  90. }
  91. return i === q.length;
  92. }
  93.  
  94. //----------------------------
  95. // 3) Main Container & Header
  96. //----------------------------
  97. const container = document.createElement('div');
  98. container.style.position = 'fixed';
  99. container.style.top = '10px';
  100. container.style.right = '10px';
  101. container.style.width = '320px';
  102. container.style.minWidth = '250px';
  103. container.style.minHeight = '200px';
  104. container.style.backgroundColor = '#2c2c2c';
  105. container.style.padding = '0';
  106. container.style.border = '1px solid #555';
  107. container.style.borderRadius = '5px';
  108. container.style.zIndex = '10000';
  109. container.style.fontSize = '14px';
  110. container.style.color = '#ddd';
  111. container.style.boxShadow = '0 2px 6px rgba(0,0,0,0.5)';
  112. container.style.userSelect = 'none';
  113. container.style.overflow = 'hidden';
  114.  
  115. const header = document.createElement('div');
  116. header.style.backgroundColor = '#3a3a3a';
  117. header.style.padding = '8px';
  118. header.style.cursor = 'move';
  119. header.style.display = 'flex';
  120. header.style.justifyContent = 'space-between';
  121. header.style.alignItems = 'center';
  122. header.style.borderBottom = '1px solid #555';
  123.  
  124. const title = document.createElement('span');
  125. title.textContent = 'NHentai Search Enhancer';
  126. title.style.fontWeight = 'bold';
  127. header.appendChild(title);
  128.  
  129. // Minimize/Maximize button
  130. const toggleBtn = document.createElement('button');
  131. toggleBtn.textContent = '–';
  132. toggleBtn.style.cursor = 'pointer';
  133. toggleBtn.style.border = 'none';
  134. toggleBtn.style.background = 'transparent';
  135. toggleBtn.style.fontSize = '16px';
  136. toggleBtn.style.lineHeight = '16px';
  137. toggleBtn.style.padding = '0 5px';
  138. toggleBtn.style.color = '#ddd';
  139. header.appendChild(toggleBtn);
  140.  
  141. container.appendChild(header);
  142. document.body.appendChild(container);
  143.  
  144. // Draggable logic
  145. let isDragging = false;
  146. let offsetX = 0, offsetY = 0;
  147. header.addEventListener('mousedown', function(e) {
  148. if (e.target !== toggleBtn) {
  149. isDragging = true;
  150. offsetX = e.clientX - container.getBoundingClientRect().left;
  151. offsetY = e.clientY - container.getBoundingClientRect().top;
  152. document.addEventListener('mousemove', drag);
  153. document.addEventListener('mouseup', stopDrag);
  154. }
  155. });
  156.  
  157. function drag(e) {
  158. if (!isDragging) return;
  159. container.style.left = (e.clientX - offsetX) + 'px';
  160. container.style.top = (e.clientY - offsetY) + 'px';
  161. container.style.right = 'auto'; // remove "right"
  162. }
  163.  
  164. function stopDrag() {
  165. isDragging = false;
  166. document.removeEventListener('mousemove', drag);
  167. document.removeEventListener('mouseup', stopDrag);
  168. }
  169.  
  170. //----------------------------
  171. // 4) Content + Resizer
  172. //----------------------------
  173. const content = document.createElement('div');
  174. content.style.padding = '10px';
  175. content.style.position = 'relative'; // for auto-suggest alignment
  176. container.appendChild(content);
  177.  
  178. // Resizer
  179. const resizer = document.createElement('div');
  180. resizer.style.width = '10px';
  181. resizer.style.height = '10px';
  182. resizer.style.background = 'transparent';
  183. resizer.style.position = 'absolute';
  184. resizer.style.right = '0';
  185. resizer.style.bottom = '0';
  186. resizer.style.cursor = 'se-resize';
  187. container.appendChild(resizer);
  188.  
  189. let isResizing = false;
  190. resizer.addEventListener('mousedown', function(e) {
  191. isResizing = true;
  192. e.stopPropagation();
  193. document.addEventListener('mousemove', resize);
  194. document.addEventListener('mouseup', stopResize);
  195. });
  196.  
  197. function resize(e) {
  198. if (!isResizing) return;
  199. const newWidth = e.clientX - container.getBoundingClientRect().left;
  200. const newHeight = e.clientY - container.getBoundingClientRect().top;
  201. if (newWidth > 250) container.style.width = newWidth + 'px';
  202. if (newHeight > 150) container.style.height = newHeight + 'px';
  203. }
  204.  
  205. function stopResize() {
  206. isResizing = false;
  207. document.removeEventListener('mousemove', resize);
  208. document.removeEventListener('mouseup', stopResize);
  209. }
  210.  
  211. //----------------------------
  212. // 5) Proper Minimize Fix
  213. //----------------------------
  214. let isMinimized = false;
  215. const originalMinHeight = container.style.minHeight || '200px';
  216.  
  217. toggleBtn.addEventListener('click', function() {
  218. isMinimized = !isMinimized;
  219. if (isMinimized) {
  220. // Hide content & resizer
  221. content.style.display = 'none';
  222. resizer.style.display = 'none';
  223. container.style.minHeight = '0';
  224. container.style.height = header.offsetHeight + 'px';
  225. toggleBtn.textContent = '+';
  226. } else {
  227. // Show content & resizer
  228. content.style.display = 'block';
  229. resizer.style.display = 'block';
  230. container.style.minHeight = originalMinHeight;
  231. container.style.height = '';
  232. toggleBtn.textContent = '–';
  233. }
  234. });
  235.  
  236. //---------------------------------------------------
  237. // 6) createAutoSuggestField (Include/Exclude Input)
  238. //---------------------------------------------------
  239. function createAutoSuggestField(labelText, placeholderText, onEnterTag) {
  240. const wrapper = document.createElement('div');
  241. wrapper.style.marginBottom = '10px';
  242. wrapper.style.position = 'relative';
  243.  
  244. const label = document.createElement('label');
  245. label.textContent = labelText;
  246. label.style.display = 'block';
  247. label.style.marginBottom = '5px';
  248. wrapper.appendChild(label);
  249.  
  250. const input = document.createElement('input');
  251. input.type = 'text';
  252. input.placeholder = placeholderText;
  253. input.style.width = '100%';
  254. input.style.padding = '3px';
  255. input.style.border = '1px solid #555';
  256. input.style.borderRadius = '3px';
  257. input.style.backgroundColor = '#444';
  258. input.style.color = '#ddd';
  259. wrapper.appendChild(input);
  260.  
  261. const suggestBox = document.createElement('div');
  262. suggestBox.style.position = 'absolute';
  263. suggestBox.style.top = '100%';
  264. suggestBox.style.left = '0';
  265. suggestBox.style.width = '100%';
  266. suggestBox.style.backgroundColor = '#444';
  267. suggestBox.style.border = '1px solid #555';
  268. suggestBox.style.borderTop = 'none';
  269. suggestBox.style.display = 'none';
  270. suggestBox.style.maxHeight = '100px';
  271. suggestBox.style.overflowY = 'auto';
  272. suggestBox.style.zIndex = '9999';
  273. wrapper.appendChild(suggestBox);
  274.  
  275. // Hide if click outside
  276. document.addEventListener('click', (e) => {
  277. if (!wrapper.contains(e.target)) {
  278. suggestBox.style.display = 'none';
  279. }
  280. });
  281.  
  282. // Press Enter
  283. input.addEventListener('keydown', (e) => {
  284. if (e.key === 'Enter') {
  285. e.preventDefault();
  286. const val = input.value.trim();
  287. if (val) {
  288. onEnterTag(val);
  289. }
  290. suggestBox.style.display = 'none';
  291. input.value = '';
  292. }
  293. });
  294.  
  295. // On input, fuzzy search
  296. input.addEventListener('input', function() {
  297. const query = input.value.trim().toLowerCase();
  298. if (!query) {
  299. suggestBox.innerHTML = '';
  300. suggestBox.style.display = 'none';
  301. return;
  302. }
  303. const filtered = appState.library.filter(t => fuzzyMatch(query, t));
  304. if (filtered.length === 0) {
  305. suggestBox.innerHTML = '';
  306. suggestBox.style.display = 'none';
  307. return;
  308. }
  309. suggestBox.innerHTML = '';
  310. filtered.forEach(tag => {
  311. const item = document.createElement('div');
  312. item.textContent = tag;
  313. item.style.padding = '5px';
  314. item.style.borderBottom = '1px solid #555';
  315. item.style.cursor = 'pointer';
  316. item.addEventListener('mouseover', () => {
  317. item.style.backgroundColor = '#555';
  318. });
  319. item.addEventListener('mouseout', () => {
  320. item.style.backgroundColor = '#444';
  321. });
  322. item.addEventListener('mousedown', () => {
  323. onEnterTag(tag);
  324. suggestBox.style.display = 'none';
  325. input.value = '';
  326. });
  327. suggestBox.appendChild(item);
  328. });
  329. suggestBox.style.display = 'block';
  330. });
  331.  
  332. return { wrapper, input };
  333. }
  334.  
  335. //----------------------------
  336. // 7) Include & Exclude Fields
  337. //----------------------------
  338. const includeField = createAutoSuggestField(
  339. 'Include Tag:',
  340. 'Type to include tag...',
  341. (tag) => {
  342. appState.included.push(tag);
  343. saveState();
  344. updatePreview();
  345. }
  346. );
  347. content.appendChild(includeField.wrapper);
  348.  
  349. const excludeField = createAutoSuggestField(
  350. 'Exclude Tag:',
  351. 'Type to exclude tag...',
  352. (tag) => {
  353. appState.excluded.push(tag);
  354. saveState();
  355. updatePreview();
  356. }
  357. );
  358. content.appendChild(excludeField.wrapper);
  359.  
  360. //----------------------------
  361. // 8) Language Dropdown
  362. //----------------------------
  363. const languageWrapper = document.createElement('div');
  364. languageWrapper.style.marginBottom = '10px';
  365.  
  366. const languageLabel = document.createElement('label');
  367. languageLabel.textContent = 'Language:';
  368. languageLabel.style.display = 'block';
  369. languageLabel.style.marginBottom = '5px';
  370. languageWrapper.appendChild(languageLabel);
  371.  
  372. const languageSelect = document.createElement('select');
  373. languageSelect.style.width = '100%';
  374. languageSelect.style.padding = '3px';
  375. languageSelect.style.border = '1px solid #555';
  376. languageSelect.style.borderRadius = '3px';
  377. languageSelect.style.backgroundColor = '#444';
  378. languageSelect.style.color = '#ddd';
  379.  
  380. ['none','english','japanese','chinese'].forEach(lang => {
  381. const opt = document.createElement('option');
  382. opt.value = lang;
  383. opt.textContent = lang;
  384. languageSelect.appendChild(opt);
  385. });
  386. languageSelect.value = appState.language || 'english';
  387.  
  388. languageSelect.addEventListener('change', () => {
  389. appState.language = languageSelect.value;
  390. saveState();
  391. updatePreview();
  392. });
  393.  
  394. languageWrapper.appendChild(languageSelect);
  395. content.appendChild(languageWrapper);
  396.  
  397. //----------------------------
  398. // 9) Add Tag to Library
  399. //----------------------------
  400. const librarySection = document.createElement('div');
  401. librarySection.style.marginBottom = '10px';
  402.  
  403. const addTagLabel = document.createElement('label');
  404. addTagLabel.textContent = 'Add Tag to Library:';
  405. addTagLabel.style.display = 'block';
  406. addTagLabel.style.marginBottom = '5px';
  407. librarySection.appendChild(addTagLabel);
  408.  
  409. const addTagRow = document.createElement('div');
  410. addTagRow.style.display = 'flex';
  411. addTagRow.style.gap = '5px';
  412.  
  413. const addTagInput = document.createElement('input');
  414. addTagInput.type = 'text';
  415. addTagInput.placeholder = 'Enter new tag...';
  416. addTagInput.style.flex = '1';
  417. addTagInput.style.padding = '3px';
  418. addTagInput.style.border = '1px solid #555';
  419. addTagInput.style.borderRadius = '3px';
  420. addTagInput.style.backgroundColor = '#444';
  421. addTagInput.style.color = '#ddd';
  422.  
  423. const addTagButton = document.createElement('button');
  424. addTagButton.textContent = 'Add';
  425. styleButton(addTagButton);
  426.  
  427. // Press Enter to add new tag
  428. addTagInput.addEventListener('keydown', (e) => {
  429. if (e.key === 'Enter') {
  430. e.preventDefault();
  431. addTagButton.click();
  432. }
  433. });
  434.  
  435. addTagButton.addEventListener('click', () => {
  436. const newTag = addTagInput.value.trim();
  437. if (newTag && !appState.library.includes(newTag)) {
  438. appState.library.push(newTag);
  439. saveState();
  440. }
  441. addTagInput.value = '';
  442. });
  443.  
  444. addTagRow.appendChild(addTagInput);
  445. addTagRow.appendChild(addTagButton);
  446. librarySection.appendChild(addTagRow);
  447.  
  448. const btnSpacing = document.createElement('div');
  449. btnSpacing.style.height = '10px';
  450. librarySection.appendChild(btnSpacing);
  451.  
  452. //----------------------------
  453. // 10) Manage Library & Bulk Edit
  454. //----------------------------
  455. const manageLibraryBtn = document.createElement('button');
  456. manageLibraryBtn.textContent = 'Manage Library';
  457. styleButton(manageLibraryBtn);
  458.  
  459. const bulkEditBtn = document.createElement('button');
  460. bulkEditBtn.textContent = 'Bulk Edit';
  461. styleButton(bulkEditBtn);
  462.  
  463. const libBtnRow = document.createElement('div');
  464. libBtnRow.style.display = 'flex';
  465. libBtnRow.style.gap = '5px';
  466. libBtnRow.appendChild(manageLibraryBtn);
  467. libBtnRow.appendChild(bulkEditBtn);
  468.  
  469. librarySection.appendChild(libBtnRow);
  470. content.appendChild(librarySection);
  471.  
  472. //-------------------------------------------------
  473. // 11) Manage Library Modal (Import/Export/Reset)
  474. //-------------------------------------------------
  475. const libraryModal = document.createElement('div');
  476. libraryModal.style.position = 'fixed';
  477. libraryModal.style.top = '0';
  478. libraryModal.style.left = '0';
  479. libraryModal.style.width = '100%';
  480. libraryModal.style.height = '100%';
  481. libraryModal.style.backgroundColor = 'rgba(0,0,0,0.5)';
  482. libraryModal.style.display = 'none';
  483. libraryModal.style.zIndex = '99999';
  484.  
  485. const modalContent = document.createElement('div');
  486. modalContent.style.position = 'absolute';
  487. modalContent.style.top = '50%';
  488. modalContent.style.left = '50%';
  489. modalContent.style.transform = 'translate(-50%, -50%)';
  490. modalContent.style.backgroundColor = '#2c2c2c';
  491. modalContent.style.padding = '10px';
  492. modalContent.style.border = '1px solid #555';
  493. modalContent.style.borderRadius = '5px';
  494. modalContent.style.width = '300px';
  495. modalContent.style.maxHeight = '450px';
  496. modalContent.style.overflowY = 'auto';
  497. modalContent.style.color = '#ddd';
  498. modalContent.style.position = 'relative';
  499.  
  500. const titleRow = document.createElement('div');
  501. titleRow.style.display = 'flex';
  502. titleRow.style.justifyContent = 'space-between';
  503. titleRow.style.alignItems = 'center';
  504. titleRow.style.marginBottom = '5px';
  505. titleRow.style.padding = '5px';
  506. titleRow.style.borderBottom = '1px solid #555';
  507. titleRow.style.backgroundColor = '#3a3a3a';
  508.  
  509. const modalTitle = document.createElement('span');
  510. modalTitle.textContent = 'Manage Library';
  511. modalTitle.style.fontSize = '16px';
  512. modalTitle.style.fontWeight = 'bold';
  513. titleRow.appendChild(modalTitle);
  514.  
  515. const closeModalX = document.createElement('button');
  516. closeModalX.textContent = '×';
  517. closeModalX.style.border = 'none';
  518. closeModalX.style.background = 'transparent';
  519. closeModalX.style.color = '#f66';
  520. closeModalX.style.cursor = 'pointer';
  521. closeModalX.style.fontSize = '24px';
  522. closeModalX.style.fontWeight = 'bold';
  523. closeModalX.style.padding = '0 5px';
  524. closeModalX.addEventListener('click', () => {
  525. libraryModal.style.display = 'none';
  526. });
  527. titleRow.appendChild(closeModalX);
  528.  
  529. modalContent.appendChild(titleRow);
  530.  
  531. // Buttons row: Remove All, Import, Export, Reset to Defaults
  532. const libraryBtnRow = document.createElement('div');
  533. libraryBtnRow.style.display = 'flex';
  534. libraryBtnRow.style.flexWrap = 'wrap';
  535. libraryBtnRow.style.gap = '5px';
  536. libraryBtnRow.style.marginBottom = '5px';
  537.  
  538. // Remove All
  539. const removeAllBtn = document.createElement('button');
  540. removeAllBtn.textContent = 'Remove All';
  541. styleButton(removeAllBtn);
  542. removeAllBtn.addEventListener('click', () => {
  543. if (confirm('Remove ALL tags from the library?')) {
  544. appState.library = [];
  545. saveState();
  546. updateLibraryList(librarySearchInput.value.trim().toLowerCase());
  547. }
  548. });
  549. libraryBtnRow.appendChild(removeAllBtn);
  550.  
  551. // Import
  552. const importBtn = document.createElement('button');
  553. importBtn.textContent = 'Import';
  554. styleButton(importBtn);
  555. libraryBtnRow.appendChild(importBtn);
  556.  
  557. // Export
  558. const exportBtn = document.createElement('button');
  559. exportBtn.textContent = 'Export';
  560. styleButton(exportBtn);
  561. exportBtn.addEventListener('click', () => {
  562. const fileContent = appState.library.join('\n');
  563. const blob = new Blob([fileContent], { type: 'text/plain' });
  564. const url = URL.createObjectURL(blob);
  565. const a = document.createElement('a');
  566. a.href = url;
  567. a.download = 'nhentai_library.txt';
  568. document.body.appendChild(a);
  569. a.click();
  570. document.body.removeChild(a);
  571. URL.revokeObjectURL(url);
  572. });
  573. libraryBtnRow.appendChild(exportBtn);
  574.  
  575. // Reset to Defaults
  576. const resetBtn = document.createElement('button');
  577. resetBtn.textContent = 'Reset to Defaults';
  578. styleButton(resetBtn);
  579. resetBtn.addEventListener('click', () => {
  580. if (confirm('Reset the library to default tags?')) {
  581. appState.library = structuredClone(defaultState.library);
  582. saveState();
  583. updateLibraryList(librarySearchInput.value.trim().toLowerCase());
  584. }
  585. });
  586. libraryBtnRow.appendChild(resetBtn);
  587.  
  588. modalContent.appendChild(libraryBtnRow);
  589.  
  590. // Import Section (hidden by default)
  591. const importSection = document.createElement('div');
  592. importSection.style.display = 'none';
  593. importSection.style.marginBottom = '10px';
  594. importSection.style.border = '1px dashed #555';
  595. importSection.style.padding = '5px';
  596.  
  597. const importTitle = document.createElement('div');
  598. importTitle.textContent = 'Import Tags';
  599. importTitle.style.fontWeight = 'bold';
  600. importTitle.style.marginBottom = '5px';
  601. importSection.appendChild(importTitle);
  602.  
  603. // 1) File input
  604. const fileLabel = document.createElement('label');
  605. fileLabel.textContent = 'Select .txt file:';
  606. fileLabel.style.display = 'block';
  607. fileLabel.style.marginBottom = '5px';
  608. importSection.appendChild(fileLabel);
  609.  
  610. const fileInput = document.createElement('input');
  611. fileInput.type = 'file';
  612. fileInput.accept = '.txt';
  613. fileLabel.appendChild(fileInput);
  614.  
  615. // 2) Textarea
  616. const textLabel = document.createElement('label');
  617. textLabel.textContent = 'Or paste tags (one per line):';
  618. textLabel.style.display = 'block';
  619. textLabel.style.marginTop = '10px';
  620. importSection.appendChild(textLabel);
  621.  
  622. const importTextarea = document.createElement('textarea');
  623. importTextarea.style.width = '100%';
  624. importTextarea.style.height = '80px';
  625. importTextarea.style.backgroundColor = '#444';
  626. importTextarea.style.color = '#ddd';
  627. importTextarea.style.border = '1px solid #555';
  628. importTextarea.style.borderRadius = '3px';
  629. importTextarea.style.resize = 'both';
  630. importTextarea.style.overflow = 'auto';
  631. importSection.appendChild(importTextarea);
  632.  
  633. const processImportBtn = document.createElement('button');
  634. processImportBtn.textContent = 'Process Import';
  635. styleButton(processImportBtn);
  636. processImportBtn.style.marginTop = '10px';
  637. importSection.appendChild(processImportBtn);
  638.  
  639. modalContent.appendChild(importSection);
  640.  
  641. importBtn.addEventListener('click', () => {
  642. importSection.style.display =
  643. importSection.style.display === 'none' ? 'block' : 'none';
  644. });
  645.  
  646. processImportBtn.addEventListener('click', () => {
  647. let linesFromFile = [];
  648. let linesFromTextarea = [];
  649.  
  650. if (fileInput.files && fileInput.files[0]) {
  651. const file = fileInput.files[0];
  652. const reader = new FileReader();
  653. reader.onload = function(e) {
  654. const content = e.target.result;
  655. linesFromFile = content.split('\n').map(l => l.trim()).filter(Boolean);
  656. doImport();
  657. };
  658. reader.readAsText(file);
  659. } else {
  660. doImport();
  661. }
  662.  
  663. function doImport() {
  664. linesFromTextarea = importTextarea.value
  665. .split('\n')
  666. .map(l => l.trim())
  667. .filter(Boolean);
  668.  
  669. const allLines = [...linesFromFile, ...linesFromTextarea];
  670. let addedCount = 0;
  671. allLines.forEach(line => {
  672. if (!appState.library.includes(line)) {
  673. appState.library.push(line);
  674. addedCount++;
  675. }
  676. });
  677.  
  678. importTextarea.value = '';
  679. fileInput.value = '';
  680. saveState();
  681. updateLibraryList(librarySearchInput.value.trim().toLowerCase());
  682.  
  683. alert(`Imported ${addedCount} new tags.`);
  684. }
  685. });
  686.  
  687. // Library Search
  688. const librarySearchInput = document.createElement('input');
  689. librarySearchInput.type = 'text';
  690. librarySearchInput.placeholder = 'Search library... (fuzzy)';
  691. librarySearchInput.style.width = '100%';
  692. librarySearchInput.style.padding = '3px';
  693. librarySearchInput.style.marginBottom = '10px';
  694. librarySearchInput.style.border = '1px solid #555';
  695. librarySearchInput.style.borderRadius = '3px';
  696. librarySearchInput.style.backgroundColor = '#444';
  697. librarySearchInput.style.color = '#ddd';
  698. modalContent.appendChild(librarySearchInput);
  699.  
  700. const libraryList = document.createElement('div');
  701. modalContent.appendChild(libraryList);
  702.  
  703. libraryModal.appendChild(modalContent);
  704. document.body.appendChild(libraryModal);
  705.  
  706. manageLibraryBtn.addEventListener('click', () => {
  707. librarySearchInput.value = '';
  708. updateLibraryList('');
  709. libraryModal.style.display = 'block';
  710.  
  711. // Hide import area
  712. importSection.style.display = 'none';
  713. importTextarea.value = '';
  714. fileInput.value = '';
  715. });
  716.  
  717. // Keyboard nav
  718. let searchResults = [];
  719. let selectedIndex = -1;
  720.  
  721. librarySearchInput.addEventListener('input', () => {
  722. const query = librarySearchInput.value.trim().toLowerCase();
  723. updateLibraryList(query);
  724. });
  725.  
  726. librarySearchInput.addEventListener('keydown', (e) => {
  727. if (searchResults.length === 0) return;
  728. if (e.key === 'ArrowDown') {
  729. e.preventDefault();
  730. selectedIndex = Math.min(selectedIndex + 1, searchResults.length - 1);
  731. highlightSelected();
  732. } else if (e.key === 'ArrowUp') {
  733. e.preventDefault();
  734. selectedIndex = Math.max(selectedIndex - 1, 0);
  735. highlightSelected();
  736. } else if (e.key === 'Enter' || e.key === 'Delete') {
  737. if (selectedIndex >= 0 && selectedIndex < searchResults.length) {
  738. const tagToRemove = searchResults[selectedIndex];
  739. const idx = appState.library.indexOf(tagToRemove);
  740. if (idx !== -1) {
  741. appState.library.splice(idx, 1);
  742. saveState();
  743. }
  744. updateLibraryList(librarySearchInput.value.trim().toLowerCase());
  745. }
  746. }
  747. });
  748.  
  749. function updateLibraryList(filterQuery) {
  750. libraryList.innerHTML = '';
  751. let filteredLib = [...appState.library];
  752. if (filterQuery) {
  753. filteredLib = filteredLib.filter(t => fuzzyMatch(filterQuery, t));
  754. }
  755. searchResults = filteredLib;
  756. selectedIndex = -1;
  757.  
  758. filteredLib.forEach((tag, idx) => {
  759. const row = document.createElement('div');
  760. row.style.display = 'flex';
  761. row.style.justifyContent = 'space-between';
  762. row.style.padding = '5px 0';
  763. row.style.transition = 'background-color 0.2s';
  764.  
  765. const tagName = document.createElement('span');
  766. tagName.textContent = tag;
  767. row.appendChild(tagName);
  768.  
  769. const removeBtn = document.createElement('button');
  770. removeBtn.textContent = '×';
  771. removeBtn.style.border = 'none';
  772. removeBtn.style.background = 'transparent';
  773. removeBtn.style.color = '#f66';
  774. removeBtn.style.cursor = 'pointer';
  775. removeBtn.style.fontWeight = 'bold';
  776. removeBtn.style.fontSize = '16px';
  777. removeBtn.addEventListener('click', () => {
  778. const actualIdx = appState.library.indexOf(tag);
  779. if (actualIdx !== -1) {
  780. appState.library.splice(actualIdx, 1);
  781. saveState();
  782. }
  783. updateLibraryList(librarySearchInput.value.trim().toLowerCase());
  784. });
  785. row.appendChild(removeBtn);
  786.  
  787. libraryList.appendChild(row);
  788. });
  789. }
  790.  
  791. function highlightSelected() {
  792. [...libraryList.children].forEach((child, idx) => {
  793. child.style.backgroundColor = (idx === selectedIndex) ? '#666' : 'transparent';
  794. });
  795. }
  796.  
  797. //----------------------------------------
  798. // 12) Bulk Edit Modal (Clear All, Save)
  799. //----------------------------------------
  800. const bulkEditModal = document.createElement('div');
  801. bulkEditModal.style.position = 'fixed';
  802. bulkEditModal.style.top = '0';
  803. bulkEditModal.style.left = '0';
  804. bulkEditModal.style.width = '100%';
  805. bulkEditModal.style.height = '100%';
  806. bulkEditModal.style.backgroundColor = 'rgba(0,0,0,0.5)';
  807. bulkEditModal.style.display = 'none';
  808. bulkEditModal.style.zIndex = '99999';
  809.  
  810. const bulkModalContent = document.createElement('div');
  811. bulkModalContent.style.position = 'absolute';
  812. bulkModalContent.style.top = '50%';
  813. bulkModalContent.style.left = '50%';
  814. bulkModalContent.style.transform = 'translate(-50%, -50%)';
  815. bulkModalContent.style.backgroundColor = '#2c2c2c';
  816. bulkModalContent.style.padding = '20px';
  817. bulkModalContent.style.border = '1px solid #555';
  818. bulkModalContent.style.borderRadius = '5px';
  819. bulkModalContent.style.width = '400px';
  820. bulkModalContent.style.maxHeight = '500px';
  821. bulkModalContent.style.overflowY = 'auto';
  822. bulkModalContent.style.color = '#ddd';
  823. bulkModalContent.style.position = 'relative';
  824.  
  825. const bulkTitleRow = document.createElement('div');
  826. bulkTitleRow.style.display = 'flex';
  827. bulkTitleRow.style.justifyContent = 'space-between';
  828. bulkTitleRow.style.alignItems = 'center';
  829. bulkTitleRow.style.marginBottom = '10px';
  830.  
  831. const bulkTitle = document.createElement('h2');
  832. bulkTitle.textContent = 'Bulk Edit Tags';
  833. bulkTitle.style.margin = '0';
  834. bulkTitleRow.appendChild(bulkTitle);
  835.  
  836. const bulkCloseX = document.createElement('button');
  837. bulkCloseX.textContent = '×';
  838. bulkCloseX.style.border = 'none';
  839. bulkCloseX.style.background = 'transparent';
  840. bulkCloseX.style.color = '#f66';
  841. bulkCloseX.style.cursor = 'pointer';
  842. bulkCloseX.style.fontSize = '20px';
  843. bulkCloseX.style.fontWeight = 'bold';
  844. bulkCloseX.addEventListener('click', () => {
  845. bulkEditModal.style.display = 'none';
  846. });
  847. bulkTitleRow.appendChild(bulkCloseX);
  848.  
  849. bulkModalContent.appendChild(bulkTitleRow);
  850.  
  851. const incLabel = document.createElement('label');
  852. incLabel.textContent = 'Included Tags (one per line):';
  853. bulkModalContent.appendChild(incLabel);
  854.  
  855. const incTextarea = document.createElement('textarea');
  856. incTextarea.style.width = '100%';
  857. incTextarea.style.height = '80px';
  858. incTextarea.style.marginBottom = '10px';
  859. incTextarea.style.backgroundColor = '#444';
  860. incTextarea.style.color = '#ddd';
  861. incTextarea.style.border = '1px solid #555';
  862. incTextarea.style.borderRadius = '3px';
  863. bulkModalContent.appendChild(incTextarea);
  864.  
  865. const excLabel = document.createElement('label');
  866. excLabel.textContent = 'Excluded Tags (one per line):';
  867. bulkModalContent.appendChild(excLabel);
  868.  
  869. const excTextarea = document.createElement('textarea');
  870. excTextarea.style.width = '100%';
  871. excTextarea.style.height = '80px';
  872. excTextarea.style.marginBottom = '10px';
  873. excTextarea.style.backgroundColor = '#444';
  874. excTextarea.style.color = '#ddd';
  875. excTextarea.style.border = '1px solid #555';
  876. excTextarea.style.borderRadius = '3px';
  877. bulkModalContent.appendChild(excTextarea);
  878.  
  879. const bulkBtnRow = document.createElement('div');
  880. bulkBtnRow.style.display = 'flex';
  881. bulkBtnRow.style.gap = '5px';
  882. bulkBtnRow.style.marginTop = '10px';
  883.  
  884. const clearAllBtn = document.createElement('button');
  885. clearAllBtn.textContent = 'Clear All Tags';
  886. styleButton(clearAllBtn);
  887. clearAllBtn.addEventListener('click', () => {
  888. appState.included = [];
  889. appState.excluded = [];
  890. saveState();
  891. updateBulkModal();
  892. updatePreview();
  893. });
  894. bulkBtnRow.appendChild(clearAllBtn);
  895.  
  896. const bulkSaveBtn = document.createElement('button');
  897. bulkSaveBtn.textContent = 'Save';
  898. styleButton(bulkSaveBtn);
  899. bulkSaveBtn.addEventListener('click', () => {
  900. const incLines = incTextarea.value.split('\n').map(t => t.trim()).filter(Boolean);
  901. const excLines = excTextarea.value.split('\n').map(t => t.trim()).filter(Boolean);
  902. appState.included = incLines;
  903. appState.excluded = excLines;
  904. saveState();
  905. updatePreview();
  906. bulkEditModal.style.display = 'none';
  907. });
  908. bulkBtnRow.appendChild(bulkSaveBtn);
  909.  
  910. bulkModalContent.appendChild(bulkBtnRow);
  911. bulkEditModal.appendChild(bulkModalContent);
  912. document.body.appendChild(bulkEditModal);
  913.  
  914. bulkEditBtn.addEventListener('click', () => {
  915. updateBulkModal();
  916. bulkEditModal.style.display = 'block';
  917. });
  918.  
  919. function updateBulkModal() {
  920. incTextarea.value = appState.included.join('\n');
  921. excTextarea.value = appState.excluded.join('\n');
  922. }
  923.  
  924. //----------------------------
  925. // 13) Search Query Preview
  926. //----------------------------
  927. const queryPreview = document.createElement('div');
  928. queryPreview.style.marginTop = '10px';
  929. queryPreview.style.padding = '5px';
  930. queryPreview.style.backgroundColor = '#333';
  931. queryPreview.style.border = '1px dashed #555';
  932. queryPreview.style.minHeight = '20px';
  933. queryPreview.textContent = 'Search Query Preview: ';
  934. content.appendChild(queryPreview);
  935.  
  936. const boxesContainer = document.createElement('div');
  937. boxesContainer.style.marginTop = '5px';
  938. content.appendChild(boxesContainer);
  939.  
  940. function createTagBox(tag, isExclude) {
  941. const box = document.createElement('span');
  942. box.style.display = 'inline-block';
  943. box.style.backgroundColor = '#444';
  944. box.style.padding = '3px 6px';
  945. box.style.margin = '2px';
  946. box.style.borderRadius = '3px';
  947. box.style.border = '1px solid #555';
  948. box.style.transition = 'background-color 0.2s';
  949.  
  950. const textNode = document.createElement('span');
  951. textNode.textContent = tag;
  952. box.appendChild(textNode);
  953.  
  954. const removeBtn = document.createElement('button');
  955. removeBtn.textContent = ' ×';
  956. removeBtn.style.marginLeft = '5px';
  957. removeBtn.style.border = 'none';
  958. removeBtn.style.background = 'transparent';
  959. removeBtn.style.cursor = 'pointer';
  960. removeBtn.style.fontWeight = 'bold';
  961. removeBtn.style.color = '#f66';
  962. removeBtn.style.transition = 'color 0.2s';
  963. removeBtn.addEventListener('mouseenter', () => {
  964. removeBtn.style.color = '#faa';
  965. });
  966. removeBtn.addEventListener('mouseleave', () => {
  967. removeBtn.style.color = '#f66';
  968. });
  969. removeBtn.addEventListener('click', () => {
  970. if (!isExclude) {
  971. const idx = appState.included.indexOf(tag);
  972. if (idx !== -1) appState.included.splice(idx, 1);
  973. } else {
  974. const idx = appState.excluded.indexOf(tag);
  975. if (idx !== -1) appState.excluded.splice(idx, 1);
  976. }
  977. saveState();
  978. updatePreview();
  979. });
  980. box.appendChild(removeBtn);
  981.  
  982. return box;
  983. }
  984.  
  985. function updatePreview() {
  986. let queryParts = [];
  987. appState.included.forEach(t => queryParts.push(`tag:"${t}"`));
  988. appState.excluded.forEach(t => queryParts.push(`-tag:"${t}"`));
  989. if (appState.language !== 'none') {
  990. queryParts.push(`language:"${appState.language}"`);
  991. }
  992. const finalQuery = queryParts.join(' ');
  993. queryPreview.textContent = 'Search Query Preview: ' + finalQuery;
  994.  
  995. boxesContainer.innerHTML = '';
  996.  
  997. // Include
  998. if (appState.included.length > 0) {
  999. const incTitle = document.createElement('div');
  1000. incTitle.textContent = 'Include Tags:';
  1001. incTitle.style.marginTop = '5px';
  1002. incTitle.style.fontWeight = 'bold';
  1003. boxesContainer.appendChild(incTitle);
  1004.  
  1005. const incBoxRow = document.createElement('div');
  1006. appState.included.forEach(tag => {
  1007. incBoxRow.appendChild(createTagBox(tag, false));
  1008. });
  1009. boxesContainer.appendChild(incBoxRow);
  1010. }
  1011.  
  1012. // Exclude
  1013. if (appState.excluded.length > 0) {
  1014. const excTitle = document.createElement('div');
  1015. excTitle.textContent = 'Exclude Tags:';
  1016. excTitle.style.marginTop = '5px';
  1017. excTitle.style.fontWeight = 'bold';
  1018. boxesContainer.appendChild(excTitle);
  1019.  
  1020. const excBoxRow = document.createElement('div');
  1021. appState.excluded.forEach(tag => {
  1022. excBoxRow.appendChild(createTagBox(tag, true));
  1023. });
  1024. boxesContainer.appendChild(excBoxRow);
  1025. }
  1026. }
  1027.  
  1028. //----------------------------
  1029. // 14) Search Button
  1030. //----------------------------
  1031. const searchButton = document.createElement('button');
  1032. searchButton.textContent = 'Search NHentai';
  1033. styleButton(searchButton);
  1034. searchButton.style.marginTop = '10px';
  1035. searchButton.style.width = '100%';
  1036. searchButton.style.padding = '6px';
  1037. content.appendChild(searchButton);
  1038.  
  1039. searchButton.addEventListener('click', () => {
  1040. let queryParts = [];
  1041. appState.included.forEach(t => queryParts.push(`tag:"${t}"`));
  1042. appState.excluded.forEach(t => queryParts.push(`-tag:"${t}"`));
  1043. if (appState.language !== 'none') {
  1044. queryParts.push(`language:"${appState.language}"`);
  1045. }
  1046. const finalQuery = queryParts.join(' ');
  1047. const encodedQuery = encodeURIComponent(finalQuery);
  1048. window.location.href = `https://nhentai.net/search/?q=${encodedQuery}`;
  1049. });
  1050.  
  1051. //----------------------------
  1052. // 15) Final Initialization
  1053. //----------------------------
  1054. // Restore language, update preview
  1055. languageSelect.value = appState.language || 'english';
  1056. updatePreview();
  1057.  
  1058. // Re-check preview on blur
  1059. includeField.input.addEventListener('blur', updatePreview);
  1060. excludeField.input.addEventListener('blur', updatePreview);
  1061.  
  1062. })();