Nhentai Plus+

Enhances the functionality of Nhentai website.

  1. // ==UserScript==
  2. // @name Nhentai Plus+
  3. // @namespace github.com/longkidkoolstar
  4. // @version 4.10.4
  5. // @description Enhances the functionality of Nhentai website.
  6. // @author longkidkoolstar
  7. // @match https://nhentai.net/*
  8. // @require https://code.jquery.com/jquery-3.6.0.min.js
  9. // @icon https://i.imgur.com/4zMY2VD.png
  10. // @license MIT
  11. // @grant GM.setValue
  12. // @grant GM.getValue
  13. // @grant GM.addStyle
  14. // @grant GM.deleteValue
  15. // ==/UserScript==
  16.  
  17. //------------------------ **Nhentai Related Manga Button** ------------------
  18.  
  19.  
  20. // Initialize maxTagsToSelect from localStorage or default to 5
  21. let maxTagsToSelect = GM.getValue('maxTagsToSelect');
  22. if (maxTagsToSelect === undefined) {
  23. maxTagsToSelect = 5;
  24. GM.setValue('maxTagsToSelect', maxTagsToSelect);
  25. } else {
  26. maxTagsToSelect = parseInt(maxTagsToSelect); // Ensure it's parsed as an integer
  27. }
  28.  
  29. // Array to store locked tags
  30. const lockedTags = [];
  31.  
  32. // Function to create and insert 'Find Similar' button
  33. async function createFindSimilarButton() {
  34. const findSimilarEnabled = await GM.getValue('findSimilarEnabled', true);
  35. if (!findSimilarEnabled) return;
  36.  
  37. if (isNaN(maxTagsToSelect)) {
  38. maxTagsToSelect = await GM.getValue('maxTagsToSelect');
  39. if (maxTagsToSelect === undefined) {
  40. maxTagsToSelect = 5;
  41. GM.setValue('maxTagsToSelect', maxTagsToSelect);
  42. }
  43. }
  44.  
  45. const downloadButton = document.getElementById('download');
  46. if (!downloadButton) {
  47. console.log('Download button not found.');
  48. return;
  49. }
  50.  
  51. const findSimilarButtonHtml = `
  52. <a class="btn btn-primary btn-disabled tooltip find-similar">
  53. <i class="fas fa-search"></i>
  54. <span>Find Similar</span>
  55. <div class="top">Click to find similar hentai<i></i></div>
  56. <div id="lockedTagsCount">Locked tags: ${lockedTags.length}</div>
  57. </a>
  58. `;
  59. const findSimilarButton = $(findSimilarButtonHtml);
  60.  
  61. // Insert 'Find Similar' button next to the download button
  62. // Find the "Find Alt." button
  63. const findAltButton = document.querySelector('a.btn.btn-primary.btn-disabled.tooltip.find-similar');
  64.  
  65. // Insert 'Find Similar' button next to the "Find Alt." button
  66. if (findAltButton && downloadButton) {
  67. $(findAltButton).after(findSimilarButton);
  68. } else {
  69. console.log('Download button or Find Alt. button not found.');
  70. }
  71.  
  72. $('#lockedTagsCount').hide();
  73.  
  74. // Handle click event for 'Find Similar' button
  75. findSimilarButton.click(async function() {
  76. const tagsContainer = $('div.tag-container.field-name:contains("Tags:")');
  77. if (!tagsContainer.length) {
  78. console.log('Tags container not found.');
  79. return;
  80. }
  81.  
  82. // Find all tag links within the container
  83. const tagLinks = tagsContainer.find('a.tag');
  84.  
  85. // Update locked tags counter
  86. if (!tagLinks.length) {
  87. console.log('No tag links found.');
  88. return;
  89. }
  90.  
  91. // Extract tag data (name and count) and assign probabilities based on count
  92. const tagsData = Array.from(tagLinks).map(tagLink => {
  93. const tagName = $(tagLink).find('.name').text().trim();
  94. const tagCount = parseInt($(tagLink).find('.count').text().replace('K', '')) || 0;
  95. const probability = Math.sqrt(tagCount); // Adjust this formula as needed
  96. return { name: tagName, count: tagCount, probability: probability };
  97. });
  98.  
  99. // Shuffle tag data array to randomize selection
  100. shuffleArray(tagsData);
  101.  
  102. const selectedTags = [];
  103. let numTagsSelected = 0;
  104.  
  105. // Add locked tags to the selected tags array
  106. lockedTags.forEach(tag => {
  107. selectedTags.push(tag);
  108. numTagsSelected++;
  109. });
  110.  
  111. tagsData.forEach(tag => {
  112. if (numTagsSelected < maxTagsToSelect && !lockedTags.includes(tag.name) && Math.random() < tag.probability) {
  113. selectedTags.push(tag.name);
  114. numTagsSelected++;
  115. }
  116. });
  117.  
  118. // Join selected tag names into a search string
  119. const searchTags = selectedTags.join(' ');
  120.  
  121. const searchInput = $('input[name="q"]');
  122. if (searchInput.length > 0) {
  123. // Update search input value with selected tags
  124. searchInput.val(searchTags);
  125. } else {
  126. // If search input not found, create and submit a hidden form
  127. const hiddenSearchFormHtml = `
  128. <form role="search" action="/search/" method="GET" style="display: none;">
  129. <input type="hidden" name="q" value="${searchTags}" />
  130. </form>
  131. `;
  132. const hiddenSearchForm = $(hiddenSearchFormHtml);
  133. $('body').append(hiddenSearchForm);
  134. hiddenSearchForm.submit();
  135. }
  136.  
  137. // Create and display the slider (only once)
  138. if (!$('#tagSlider').length) {
  139. createSlider();
  140. }
  141. });
  142.  
  143. // Handle double-click event for 'Find Similar' button
  144. findSimilarButton.dblclick(async function() {
  145. const searchTags = lockedTags.join(' ');
  146.  
  147. const searchInput = $('input[name="q"]');
  148. if (searchInput.length > 0) {
  149. // Update search input value with locked tags only
  150. searchInput.val(searchTags);
  151. } else {
  152. // If search input not found, create and submit a hidden form with locked tags only
  153. const hiddenSearchFormHtml = `
  154. <form role="search" action="/search/" method="GET" style="display: none;">
  155. <input type="hidden" name="q" value="${searchTags}" />
  156. </form>
  157. `;
  158. const hiddenSearchForm = $(hiddenSearchFormHtml);
  159. $('body').append(hiddenSearchForm);
  160. hiddenSearchForm.submit();
  161. }
  162.  
  163. // Create and display the slider (only once)
  164. if (!$('#tagSlider').length) {
  165. createSlider();
  166. }
  167. });
  168. }
  169.  
  170. // Function to create and display the slider
  171. async function createSlider() {
  172. const sliderHtml = `
  173. <div style="position: fixed; bottom: 20px; right: 20px; z-index: 9999;">
  174. <input type="range" min="1" max="10" value="${maxTagsToSelect}" id="tagSlider">
  175. <label for="tagSlider">Max Tags to Select: <span id="tagSliderValue">${maxTagsToSelect}</span></label>
  176. </div>
  177. `;
  178. $(document.body).append(sliderHtml);
  179.  
  180. // Retrieve saved maxTagsToSelect value from GM storage (if available)
  181. const savedMaxTags = await GM.getValue('maxTagsToSelect');
  182. if (savedMaxTags !== undefined) {
  183. maxTagsToSelect = parseInt(savedMaxTags);
  184. $('#tagSlider').val(maxTagsToSelect);
  185. $('#tagSliderValue').text(maxTagsToSelect);
  186. }
  187.  
  188. // Update maxTagsToSelect based on slider value and save to GM storage
  189. $('#tagSlider').on('input', async function() {
  190. maxTagsToSelect = parseInt($(this).val());
  191. $('#tagSliderValue').text(maxTagsToSelect);
  192.  
  193. // Store the updated maxTagsToSelect value in GM storage
  194. await GM.setValue('maxTagsToSelect', maxTagsToSelect);
  195. });
  196. }
  197.  
  198. // Call the function to create 'Find Similar' button
  199. createFindSimilarButton();
  200.  
  201. function updateLockedTagsCounter() {
  202. const lockedTagsCount = lockedTags.length;
  203. const lockedTagsCounter = $('#lockedTagsCount');
  204. if (lockedTagsCount > 0) {
  205. lockedTagsCounter.text(`Locked tags: ${lockedTagsCount}`).show();
  206. if (lockedTagsCount > maxTagsToSelect) {
  207. lockedTagsCounter.css('color', 'red');
  208. } else {
  209. lockedTagsCounter.css('color', ''); // Reset color to default
  210. }
  211. } else {
  212. lockedTagsCounter.hide();
  213. }
  214. }
  215.  
  216. // Event listener for locking/unlocking tags
  217. $(document).on('click', 'span.lock-button', function(event) {
  218. event.stopPropagation(); // Prevent tag link click event from firing
  219.  
  220. const tagName = $(this).prev('a.tag').find('.name').text().trim();
  221.  
  222. if (lockedTags.includes(tagName)) {
  223. // Tag is already locked, unlock it
  224. const index = lockedTags.indexOf(tagName);
  225. if (index !== -1) {
  226. lockedTags.splice(index, 1);
  227. }
  228. $(this).html('<i class="fas fa-plus"></i>'); // Change icon to plus
  229. updateLockedTagsCounter();
  230. } else {
  231. // Lock the tag
  232. lockedTags.push(tagName);
  233. $(this).html('<i class="fas fa-minus"></i>'); // Change icon to minus
  234. updateLockedTagsCounter();
  235. }
  236. });
  237.  
  238. // Add lock button next to each tag
  239. const tagsContainer = $('div.tag-container.field-name:contains("Tags:")');
  240. if (tagsContainer.length) {
  241. const tagLinks = tagsContainer.find('a.tag');
  242. tagLinks.each(function(index, tagLink) {
  243. const lockButtonHtml = `
  244. <span class="lock-button" data-tag-index="${index}">
  245. <i class="fas fa-plus"></i>
  246. </span>
  247. `;
  248. const lockButton = $(lockButtonHtml);
  249. $(tagLink).after(lockButton);
  250. });
  251. }
  252.  
  253. console.log('Script setup complete.');
  254.  
  255. // Function to shuffle an array (Fisher-Yates shuffle algorithm)
  256. function shuffleArray(array) {
  257. for (let i = array.length - 1; i > 0; i--) {
  258. const j = Math.floor(Math.random() * (i + 1));
  259. [array[i], array[j]] = [array[j], array[i]];
  260. }
  261. }
  262.  
  263.  
  264. //------------------------ **Nhentai Related Manga Button** ------------------
  265.  
  266. //----------------------- **Find Alternative Manga Button** ------------------
  267.  
  268.  
  269. // Adds a button to the page that allows the user to find alternative manga to the current one.
  270. // Checks if the feature is enabled in the settings before appending the button.
  271.  
  272. async function addFindAltButton() {
  273. const findAltmangaEnabled = await GM.getValue('findAltmangaEnabled', true);
  274. if (!findAltmangaEnabled) return;
  275.  
  276. // Get the download button
  277. const downloadButton = document.getElementById('download');
  278. if (!downloadButton) {
  279. console.log('Download button not found.');
  280. return;
  281. }
  282.  
  283. const copyTitleButtonHtml = `
  284. <a class="btn btn-primary btn-disabled tooltip find-similar">
  285. <i class="fas fa-code-branch"></i>
  286. <span>Find Alt.</span>
  287. <div class="top">Click to find alternative manga to this one<i></i></div>
  288. </a>
  289. `;
  290. const copyTitleButton = $(copyTitleButtonHtml);
  291.  
  292. // Handle click event for the button
  293. copyTitleButton.click(function() {
  294. // Get the title element
  295. const titleElement = $('h1.title');
  296. if (!titleElement.length) {
  297. console.log('Title element not found.');
  298. return;
  299. }
  300.  
  301. // Extract the text content
  302. const titleText = titleElement.text();
  303.  
  304. // Remove text inside square brackets [] and parentheses ()
  305. const cleanedTitleText = titleText.replace(/\[.*?\]|\(.*?\)|\d+/g, '').trim();
  306.  
  307. // Find the search input
  308. const searchInput = $('input[name="q"]');
  309. if (searchInput.length > 0) {
  310. // Update search input value with cleaned title text
  311. searchInput.val(cleanedTitleText);
  312. // Click the search button
  313. const searchButton = $('button[type="submit"]');
  314. if (searchButton.length) {
  315. searchButton.click();
  316. }
  317. } else {
  318. console.log('Search input not found.');
  319. }
  320. });
  321.  
  322. // Insert 'Find Similar' button next to the download button
  323. $(downloadButton).after(copyTitleButton);
  324. }
  325. // Call the function to add the Copy Title button
  326. addFindAltButton();
  327.  
  328. //------------------------ **Find Alternative Manga Button** ------------------
  329.  
  330. //------------------------ **Find Alternative Manga Button(Thumbnail Version)** ------------------
  331.  
  332. (async function() {
  333. const findAltMangaThumbnailEnabled = await GM.getValue('findAltMangaThumbnailEnabled', true); // Default to true if not set
  334. if (!findAltMangaThumbnailEnabled) return; // Exit if the feature is not enabled
  335.  
  336. const flagEn = "https://i.imgur.com/vSnHmmi.gif";
  337. const flagJp = "https://i.imgur.com/GlArpuS.gif";
  338. const flagCh = "https://i.imgur.com/7B55DYm.gif";
  339. const non_english_fade_opacity = 0.3;
  340. const partially_fade_all_non_english = true;
  341. const mark_as_read_system_enabled = true;
  342. const marked_as_read_fade_opacity = 0.3;
  343. const auto_group_on_page_comics = true;
  344. const version_grouping_filter_brackets = false;
  345.  
  346. let MARArray = [];
  347. GM.getValue("MARArray", "[]").then((value) => {
  348. if (typeof value === 'string') {
  349. MARArray = JSON.parse(value);
  350. }
  351.  
  352. GM.addStyle(`
  353. .overlayFlag {
  354. position: absolute;
  355. display: inline-block;
  356. top: 3px;
  357. left: 3px;
  358. z-index: 3;
  359. width: 18px;
  360. height: 12px;
  361. }
  362. .numOfVersions {
  363. border-radius: 10px;
  364. padding: 5px 10px;
  365. position: absolute;
  366. background-color: rgba(0,0,0,.7);
  367. color: rgba(255,255,255,.8);
  368. top: 7.5px;
  369. left: 105px;
  370. font-size: 12px;
  371. font-weight: 900;
  372. opacity: 1;
  373. width: 40px;
  374. z-index: 2;
  375. display: none;
  376. }
  377. .findVersionButton {
  378. border-radius: 10px;
  379. padding: 5px 10px;
  380. position: absolute;
  381. background-color: rgba(0,0,0,.4);
  382. color: rgba(255,255,255,.8);
  383. bottom: 7.5px;
  384. left: 7.5px;
  385. font-size: 12px;
  386. font-weight: 900;
  387. opacity: 1;
  388. width: 125px;
  389. z-index: 2;
  390. cursor: pointer;
  391. }
  392. .versionNextButton {
  393. border-radius: 10px;
  394. padding: 5px 10px;
  395. position: absolute;
  396. background-color: rgba(0,0,0,.7);
  397. color: rgba(255,255,255,.8);
  398. top: 7.5px;
  399. right: 7.5px;
  400. font-size: 12px;
  401. font-weight: 900;
  402. opacity: 1;
  403. display: none;
  404. z-index: 2;
  405. cursor: pointer;
  406. }
  407. .versionPrevButton {
  408. border-radius: 10px;
  409. padding: 5px 10px;
  410. position: absolute;
  411. background-color: rgba(0,0,0,.7);
  412. color: rgba(255,255,255,.8);
  413. top: 7.5px;
  414. left: 7.5px;
  415. font-size: 12px;
  416. font-weight: 900;
  417. opacity: 1;
  418. z-index: 2;
  419. display: none;
  420. cursor: pointer;
  421. }
  422. `);
  423.  
  424. function IncludesAll(string, search) {
  425. string = CleanupSearchString(string);
  426. search = CleanupSearchString(search);
  427. if (string.length == 0 || search.length == 0) return false;
  428. let searches = search.split(" ");
  429. for (let i = 0; i < searches.length; i++) {
  430. if (!!searches[i] && searches[i].length > 0 && !string.includes(searches[i])) return false;
  431. }
  432. return true;
  433. }
  434.  
  435. function AddAltVersionsToThis(target) {
  436. let place = target;
  437. let title = place.parent().find(".cover:visible > .caption").text();
  438. $.get(BuildUrl(title), function(data) {
  439. let found = $(data).find(".container > .gallery");
  440. if (!found || found.length <= 0) {
  441. alert("error reading data");
  442. return;
  443. }
  444. place.parent().find(".cover").remove();
  445. try {
  446. for (let i = 0; i < found.length; i++) {
  447. if (partially_fade_all_non_english) {
  448. $(found[i]).find(".cover > img, .cover > .caption").css("opacity", non_english_fade_opacity);
  449. }
  450.  
  451. if ($(found[i]).attr("data-tags").includes("12227")) {
  452. $(found[i]).find(".caption").append(`<img class="overlayFlag" src="` + flagEn + `">`);
  453. $(found[i]).find(".cover > img, .cover > .caption").css("opacity", "1");
  454. } else {
  455. if ($(found[i]).attr("data-tags").includes("6346")) {
  456. $(found[i]).find(".caption").append(`<img class="overlayFlag" src="` + flagJp + `">`);
  457. } else if ($(found[i]).attr("data-tags").includes("29963")) {
  458. $(found[i]).find(".caption").append(`<img class="overlayFlag" src="` + flagCh + `">`);
  459. }
  460. if (!partially_fade_all_non_english) {
  461. $(found[i]).find(".cover > img, .cover > .caption").css("opacity", "1");
  462. }
  463. }
  464.  
  465. if (mark_as_read_system_enabled) {
  466. let MARArraySelector = MARArray.join("'], .cover[href='");
  467. $(found[i]).find(".cover[href='" + MARArraySelector + "']").append("<div class='readTag'>READ</div>");
  468. let readTag = $(found[i]).find(".readTag");
  469. if (!!readTag && readTag.length > 0) {
  470. readTag.parent().parent().find(".cover > img, .cover > .caption").css("opacity", marked_as_read_fade_opacity);
  471. }
  472. }
  473.  
  474. let thumbnailReplacement;
  475. if (!!$(found[i]).find(".cover > img").attr("data-src")) {
  476. thumbnailReplacement = $(found[i]).find(".cover > img").attr("data-src").replace(/\/\/.+?\.nhentai/g, "//i.nhentai").replace("thumb.jpg", "1.jpg").replace("thumb.png", "1.png");
  477. } else {
  478. thumbnailReplacement = $(found[i]).find(".cover > img").attr("src").replace(/\/\/.+?\.nhentai/g, "//i.nhentai").replace("thumb.jpg", "1.jpg").replace("thumb.png", "1.png");
  479. }
  480.  
  481. $(found[i]).find(".cover > img").attr("src", thumbnailReplacement);
  482. place.parent().append($(found[i]).find(".cover"));
  483. }
  484. } catch (er) {
  485. alert("error modifying data: " + er);
  486. return;
  487. }
  488. place.parent().find(".cover:not(:first)").css("display", "none");
  489. place.parent().find(".versionPrevButton, .versionNextButton, .numOfVersions").show(200);
  490. place.parent().find(".numOfVersions").text("1/" + (found.length));
  491. place.hide(200);
  492. }).fail(function(e) {
  493. alert("error getting data: " + e);
  494. });
  495. }
  496.  
  497. function CleanupSearchString(title) {
  498. title = title.replace(/\[.*?\]/g, "");
  499. title = title.replace(/\【.*?\】/g, "");
  500. if (version_grouping_filter_brackets) title = title.replace(/\(.*?\)/g, "");
  501. return title.trim();
  502. }
  503.  
  504. function BuildUrl(title) {
  505. let url = CleanupSearchString(title);
  506. url = url.trim();
  507. url = url.replace(/(^|\s){1}[^\w\s\d]{1}(\s|$){1}/g, " "); // remove all instances of a lone symbol character
  508. url = url.replace(/\s+/g, '" "'); // wrap all terms with ""
  509. url = '"' + url + '"';
  510. url = encodeURIComponent(url);
  511. url = "https://nhentai.net/search/?q=" + url;
  512. return url;
  513. }
  514.  
  515. function GroupAltVersionsOnPage() {
  516. let i = 0;
  517. let found = $(".container > .gallery");
  518. while (!!found && i < found.length) {
  519. AddAltVersionsToThisFromPage(found[i]);
  520. i++;
  521. found = $(".container > .gallery");
  522. }
  523. }
  524.  
  525. function AddAltVersionsToThisFromPage(target) {
  526. let place = $(target);
  527. place.addClass("ignoreThis");
  528. let title = place.find(".cover > .caption").text();
  529. if (!title || title.length <= 0) return;
  530. let found = $(".container > .gallery:not(.ignoreThis)");
  531. let numOfValid = 0;
  532. for (let i = 0; i < found.length; i++) {
  533. let cap = $(found[i]).find(".caption");
  534. if (cap.length == 1) {
  535. if (IncludesAll(cap.text(), title)) {
  536. if (partially_fade_all_non_english) {
  537. $(found[i]).find(".cover > img, .cover > .caption").css("opacity", non_english_fade_opacity);
  538. }
  539.  
  540. if ($(found[i]).attr("data-tags").includes("12227")) {
  541. $(found[i]).find(".caption").append(`<img class="overlayFlag" src="` + flagEn + `">`);
  542. $(found[i]).find(".cover > img, .cover > .caption").css("opacity", "1");
  543. } else {
  544. if ($(found[i]).attr("data-tags").includes("6346")) {
  545. $(found[i]).find(".caption").append(`<img class="overlayFlag" src="` + flagJp + `">`);
  546. } else if ($(found[i]).attr("data-tags").includes("29963")) {
  547. $(found[i]).find(".caption").append(`<img class="overlayFlag" src="` + flagCh + `">`);
  548. }
  549. if (!partially_fade_all_non_english) {
  550. $(found[i]).find(".cover > img, .cover > .caption").css("opacity", "1");
  551. }
  552. }
  553.  
  554. if (mark_as_read_system_enabled) {
  555. let MARArraySelector = MARArray.join("'], .cover[href='");
  556. $(found[i]).find(".cover[href='" + MARArraySelector + "']").append("<div class='readTag'>READ</div>");
  557. let readTag = $(found[i]).find(".readTag");
  558. if (!!readTag && readTag.length > 0) {
  559. readTag.parent().parent().find(".cover > img, .cover > .caption").css("opacity", marked_as_read_fade_opacity);
  560. }
  561. }
  562.  
  563. place.append($(found[i]).find(".cover"));
  564. $(found[i]).addClass("deleteThis");
  565. numOfValid++;
  566. }
  567. } else {
  568. let addThese = false;
  569. for (let j = 0; j < cap.length; j++) {
  570. if (IncludesAll($(cap[j]).text(), title)) {
  571. addThese = true;
  572. break;
  573. }
  574. }
  575.  
  576. if (addThese) {
  577. for (let j = 0; j < cap.length; j++) {
  578. place.append($(cap[j]).parent());
  579. }
  580. $(found[i]).addClass("deleteThis");
  581. numOfValid += cap.length;
  582. }
  583. }
  584. }
  585. numOfValid++;
  586. place.removeClass("deleteThis");
  587. place.removeClass("ignoreThis");
  588. $(".deleteThis").remove();
  589. if (numOfValid > 1) {
  590. place.find(".cover:not(:first)").css("display", "none");
  591. place.find(".versionPrevButton, .versionNextButton, .numOfVersions").show(200);
  592. place.find(".numOfVersions").text("1/" + numOfValid);
  593. }
  594. }
  595.  
  596. if ($(".container.index-container, #favcontainer.container, #recent-favorites-container, #related-container").length !== 0) {
  597. $(".cover").parent().append("<div class='findVersionButton'>Find Alt Versions</div>");
  598. $(".cover").parent().append("<div class='numOfVersions'>1/1</div>");
  599. $(".cover").parent().append("<div class='versionNextButton'>►</div>");
  600. $(".cover").parent().append("<div class='versionPrevButton'>◄</div>");
  601.  
  602. $(".findVersionButton").click(function(e) {
  603. e.preventDefault();
  604. AddAltVersionsToThis($(this));
  605. });
  606.  
  607. if (auto_group_on_page_comics) GroupAltVersionsOnPage();
  608.  
  609. $(".versionPrevButton").click(function(e) {
  610. e.preventDefault();
  611. let toHide = $(this).parent().find(".cover").filter(":visible");
  612. let toShow = toHide.prev();
  613. if (!toShow || toShow.length <= 0) return;
  614. if (!toShow.is(".cover")) toShow = toHide.prevUntil(".cover", ":last").prev();
  615. if (!toShow || toShow.length <= 0) return;
  616. toHide.hide(100);
  617. toShow.show(100);
  618. let n = $(this).parent().find(".numOfVersions");
  619. n.text((Number(n.text().split("/")[0]) - 1) + "/" + n.text().split("/")[1]);
  620. });
  621. $(".versionNextButton").click(function(e) {
  622. e.preventDefault();
  623. let toHide = $(this).parent().find(".cover").filter(":visible");
  624. let toShow = toHide.next();
  625. if (!toShow || toShow.length <= 0) return;
  626. if (!toShow.is(".cover")) toShow = toHide.nextUntil(".cover", ":last").next();
  627. if (!toShow || toShow.length <= 0) return;
  628. toHide.hide(100);
  629. toShow.show(100);
  630. let n = $(this).parent().find(".numOfVersions");
  631. n.text((Number(n.text().split("/")[0]) + 1) + "/" + n.text().split("/")[1]);
  632. });
  633. }
  634. });
  635.  
  636. })(); // Self-invoking function for the toggle check
  637.  
  638. //------------------------ **Find Alternative Manga Button(Thumbnail Version)** ------------------
  639.  
  640.  
  641. // ------------------------ *Bookmarks** ------------------
  642. function injectCSS() {
  643. const css = `
  644. /* Bookmark animation */
  645. @keyframes bookmark-animation {
  646. 0% {
  647. transform: scale(1) rotate(0deg);
  648. }
  649. 50% {
  650. transform: scale(1.2) rotate(20deg);
  651. }
  652. 100% {
  653. transform: scale(1) rotate(0deg);
  654. }
  655. }
  656.  
  657. /* Add a class for the animation */
  658. .bookmark-animating {
  659. animation: bookmark-animation 0.4s ease-in-out;
  660. }
  661. `;
  662. const style = document.createElement('style');
  663. style.type = 'text/css';
  664. style.appendChild(document.createTextNode(css));
  665. document.head.appendChild(style);
  666. }
  667.  
  668. injectCSS(); // Inject the CSS when the userscript runs
  669.  
  670. // Function to create and insert bookmark button
  671. async function createBookmarkButton() {
  672. // Check if the feature is enabled in settings
  673. const bookmarksEnabled = await GM.getValue('bookmarksEnabled', true);
  674. if (!bookmarksEnabled) {
  675. return;
  676. }
  677.  
  678. // Check if the page is already bookmarked
  679. const bookmarkedPages = await GM.getValue('bookmarkedPages', []);
  680. const currentPage = window.location.href;
  681. const isBookmarked = bookmarkedPages.includes(currentPage);
  682.  
  683. // Bookmark button HTML using Font Awesome 5.13.0
  684. const bookmarkButtonHtml = `
  685. <a class="btn btn-primary bookmark-btn" style="margin-left: 10px;">
  686. <i class="bookmark-icon ${isBookmarked ? 'fas' : 'far'} fa-bookmark"></i>
  687. </a>
  688. `;
  689. const bookmarkButton = $(bookmarkButtonHtml);
  690.  
  691. // Append the bookmark button as a child of the h1 element if it exists
  692. const h1Element = document.querySelector("#content > h1");
  693. if (h1Element) {
  694. h1Element.append(bookmarkButton[0]);
  695. }
  696.  
  697. // Handle click event for the bookmark button
  698. bookmarkButton.click(async function() {
  699. const bookmarkIcon = $(this).find('i.bookmark-icon');
  700. const bookmarkedPages = await GM.getValue('bookmarkedPages', []);
  701. const currentPage = window.location.href;
  702. const isBookmarked = bookmarkedPages.includes(currentPage);
  703.  
  704. // Add animation class
  705. bookmarkIcon.addClass('bookmark-animating');
  706.  
  707. if (isBookmarked) {
  708. // Remove the bookmark
  709. const updatedBookmarkedPages = bookmarkedPages.filter(page => page !== currentPage);
  710. await GM.setValue('bookmarkedPages', updatedBookmarkedPages);
  711. await GM.deleteValue(currentPage);
  712. bookmarkIcon.addClass('far').removeClass('fas');
  713. } else {
  714. // Add the bookmark
  715. bookmarkedPages.push(currentPage);
  716. await GM.setValue('bookmarkedPages', bookmarkedPages);
  717. bookmarkIcon.addClass('fas').removeClass('far');
  718. }
  719.  
  720. // Remove animation class after animation ends
  721. setTimeout(() => {
  722. bookmarkIcon.removeClass('bookmark-animating');
  723. }, 400); // Match the duration of the CSS animation (0.4s)
  724. });
  725. }
  726.  
  727.  
  728.  
  729.  
  730. // Only execute if not on the settings page or favorites page
  731. if (window.location.href.indexOf('nhentai.net/settings') === -1 && window.location.href.indexOf('nhentai.net/favorites') === -1) {
  732. createBookmarkButton();
  733. }
  734.  
  735.  
  736.  
  737.  
  738.  
  739.  
  740. function addBookmarkButton() {
  741. // Create the bookmark button
  742. const bookmarkButtonHtml = `
  743. <li>
  744. <a href="/bookmarks/">
  745. <i class="fa fa-bookmark"></i>
  746. Bookmarks
  747. </a>
  748. </li>
  749. `;
  750. const bookmarkButton = $(bookmarkButtonHtml);
  751.  
  752. // Append the bookmark button to the dropdown menu
  753. const dropdownMenu = $('ul.dropdown-menu');
  754. dropdownMenu.append(bookmarkButton);
  755.  
  756. // Append the bookmark button to the menu
  757. const menu = $('ul.menu.left');
  758. menu.append(bookmarkButton);
  759. }
  760.  
  761. addBookmarkButton(); // Call the function to add the bookmark button
  762.  
  763.  
  764. // Delete error message on unsupported bookmarks page
  765. (async function() {
  766. if (window.location.href.includes('/bookmarks')) {
  767. // Remove not found heading
  768. const notFoundHeading = document.querySelector('h1');
  769. if (notFoundHeading?.textContent === '404 – Not Found') {
  770. notFoundHeading.remove();
  771. }
  772.  
  773. // Remove not found message
  774. const notFoundMessage = document.querySelector('p');
  775. if (notFoundMessage?.textContent === "Looks like what you're looking for isn't here.") {
  776. notFoundMessage.remove();
  777. }
  778.  
  779. // Function to fetch the title of a webpage with caching and retries
  780. async function fetchTitleWithCacheAndRetry(url, retries = 3) {
  781. const cachedTitle = await GM.getValue(url);
  782. if (cachedTitle) {
  783. return cachedTitle;
  784. }
  785.  
  786. for (let i = 0; i < retries; i++) {
  787. try {
  788. const response = await fetch(url);
  789. if (response.status === 429) {
  790. // If we get a 429, wait for a bit before retrying
  791. await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
  792. continue;
  793. }
  794. const text = await response.text();
  795. const parser = new DOMParser();
  796. const doc = parser.parseFromString(text, 'text/html');
  797. let title = doc.querySelector('title').innerText;
  798.  
  799. // Remove "» nhentai: hentai doujinshi and manga" from the title
  800. const unwantedPart = "» nhentai: hentai doujinshi and manga";
  801. if (title.includes(unwantedPart)) {
  802. title = title.replace(unwantedPart, '').trim();
  803. }
  804.  
  805. // Cache the title
  806. await GM.setValue(url, title);
  807.  
  808. return title;
  809. } catch (error) {
  810. console.error(`Error fetching title for: ${url}. Attempt ${i + 1} of ${retries}`, error);
  811. if (i === retries - 1) {
  812. return url; // Fallback to URL if all retries fail
  813. }
  814. }
  815. }
  816. }
  817.  
  818. // Function to display bookmarked pages with active loading for unfetched bookmarks
  819. async function displayBookmarkedPages() {
  820. const bookmarkedPages = await GM.getValue('bookmarkedPages', []);
  821.  
  822. if (Array.isArray(bookmarkedPages)) {
  823. const bookmarksContainer = $('<div id="bookmarksContainer" class="container">');
  824. const bookmarksTitle = $('<h2 class="bookmarks-title">Bookmarked Pages</h2>');
  825. const bookmarksList = $('<ul class="bookmarks-list">');
  826. const searchInput = $('<input type="text" id="searchBookmarks" placeholder="Search bookmarks..." class="search-input">');
  827.  
  828. bookmarksContainer.append(bookmarksTitle);
  829. bookmarksContainer.append(searchInput);
  830. bookmarksContainer.append(bookmarksList);
  831. $('body').append(bookmarksContainer);
  832.  
  833. // Add CSS styles
  834. const styles = `
  835. #bookmarksContainer {
  836. margin: 20px auto;
  837. padding: 20px;
  838. background-color: #2c2c2c;
  839. border-radius: 8px;
  840. box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
  841. width: 80%;
  842. max-width: 600px;
  843. }
  844. .bookmarks-title {
  845. font-size: 24px;
  846. margin-bottom: 10px;
  847. color: #e63946;
  848. }
  849. .search-input {
  850. width: calc(100% - 20px);
  851. padding: 10px;
  852. margin-bottom: 20px;
  853. border-radius: 5px;
  854. border: 1px solid #ccc;
  855. font-size: 16px;
  856. }
  857. .bookmarks-list {
  858. list-style: none;
  859. padding: 0;
  860. max-height: 100%;
  861. overflow-y: hidden;
  862. }
  863. .bookmark-link {
  864. display: block;
  865. padding: 10px;
  866. font-size: 18px;
  867. color: #f1faee;
  868. text-decoration: none;
  869. transition: background-color 0.3s, color 0.3s;
  870. }
  871. .bookmark-link:hover {
  872. background-color: #e63946;
  873. color: #1d3557;
  874. }
  875. .delete-button {
  876. position: relative;
  877. top: -32px;
  878. float: right;
  879. background: none;
  880. border: none;
  881. color: #e63946;
  882. cursor: pointer;
  883. font-size: 14px;
  884. }
  885. .delete-button:hover {
  886. color: #f1faee;
  887. }
  888. .undo-popup {
  889. position: fixed;
  890. bottom: 20px;
  891. left: 50%;
  892. transform: translateX(-50%);
  893. padding: 15px;
  894. background-color: #333;
  895. color: #fff;
  896. border-radius: 5px;
  897. box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
  898. display: flex;
  899. align-items: center;
  900. gap: 10px;
  901. z-index: 1000;
  902. }
  903. .undo-button {
  904. background-color: #f1faee;
  905. color: #333;
  906. border: none;
  907. padding: 5px 10px;
  908. border-radius: 3px;
  909. cursor: pointer;
  910. }
  911. .undo-button:hover {
  912. background-color: #e63946;
  913. color: #1d3557;
  914. }
  915. @media only screen and (max-width: 600px) {
  916. #bookmarksContainer {
  917. width: 90%;
  918. margin: 10px auto;
  919. }
  920. .bookmarks-title {
  921. font-size: 20px;
  922. }
  923. .bookmark-link {
  924. font-size: 16px;
  925. }
  926. }
  927. `;
  928. const styleSheet = document.createElement("style");
  929. styleSheet.type = "text/css";
  930. styleSheet.innerText = styles;
  931. document.head.appendChild(styleSheet);
  932.  
  933. // Fetch titles for each bookmark and update dynamically
  934. for (const page of bookmarkedPages) {
  935. // Append a loading list item first
  936. const listItem = $(`<li><a href="${page}" class="bookmark-link">Loading...</a><button class="delete-button">✖</button></li>`);
  937. bookmarksList.append(listItem);
  938.  
  939. fetchTitleWithCacheAndRetry(page).then(title => {
  940. // Update the list item with the fetched title
  941. const updatedListItem = $(`<li><a href="${page}" class="bookmark-link">${title}</a><button class="delete-button">✖</button></li>`);
  942. listItem.replaceWith(updatedListItem);
  943.  
  944. // Add delete functionality
  945. updatedListItem.find('.delete-button').click(async function() {
  946. const updatedBookmarkedPages = bookmarkedPages.filter(p => p !== page);
  947. await GM.setValue('bookmarkedPages', updatedBookmarkedPages);
  948. await GM.deleteValue(page); // Remove the title from GM storage
  949. updatedListItem.remove();
  950.  
  951. const undoPopup = $(`
  952. <div class="undo-popup">
  953. <span>Bookmark deleted.</span>
  954. <button class="undo-button">Undo</button>
  955. </div>
  956. `);
  957. $('body').append(undoPopup);
  958.  
  959. const timeout = setTimeout(() => {
  960. undoPopup.remove();
  961. }, 5000);
  962.  
  963. undoPopup.find('.undo-button').click(async function() {
  964. clearTimeout(timeout);
  965. const restoredBookmarkedPages = [...updatedBookmarkedPages, page];
  966. await GM.setValue('bookmarkedPages', restoredBookmarkedPages);
  967. await fetchTitleWithCacheAndRetry(page); // Re-fetch and store the title
  968. undoPopup.remove();
  969. $('#bookmarksContainer').remove();
  970. displayBookmarkedPages();
  971. });
  972. });
  973. }).catch(error => {
  974. console.error(`Error fetching title for: ${page}`, error);
  975. listItem.text("Failed to fetch title");
  976. });
  977. }
  978.  
  979. // Implement search functionality
  980. searchInput.on('input', function() {
  981. const query = $(this).val().toLowerCase();
  982. bookmarksList.children('li').each(function() {
  983. const title = $(this).find('.bookmark-link').text().toLowerCase();
  984. $(this).toggle(title.includes(query));
  985. });
  986. });
  987. } else {
  988. console.error('Bookmarked pages is not an array');
  989. }
  990. }
  991.  
  992. // Call the function to display bookmarked pages with active loading
  993. displayBookmarkedPages();
  994.  
  995.  
  996. }
  997. })();
  998. // ------------------------ *Bookmarks** ------------------
  999.  
  1000.  
  1001.  
  1002.  
  1003.  
  1004. //------------------------ **Nhentai English Filter** ----------------------
  1005. var pathname = window.location.pathname;
  1006. var searchQuery = window.location.search.split('=')[1] || '';
  1007. var namespaceQuery = pathname.split('/')[2];
  1008. var namespaceSearchLink = '<div class="sort-type"><a href="https://nhentai.net/search/?q=' + namespaceQuery + '+English">English Only</a></div>';
  1009. var siteSearchLink = '<div class="sort-type"><a href="https://nhentai.net/search/?q=' + searchQuery + '+English">English Only</a></div>';
  1010. var favSearchBtn = '<a class="btn btn-primary" href="https://nhentai.net/favorites/?q=English+' + searchQuery + '"><i class="fa fa-flag"></i> ENG</a>';
  1011. var favPageBtn = '<a class="btn btn-primary" href="https://nhentai.net/favorites/?q=English+"><i class="fa fa-flag"></i> ENG</a>';
  1012.  
  1013. (async function() {
  1014. const englishFilterEnabled = await GM.getValue('englishFilterEnabled', true);
  1015.  
  1016. if (englishFilterEnabled) {
  1017. if (!/English/.test(searchQuery)) {
  1018. if (pathname.startsWith('/parody/')) { // parody pages
  1019. document.getElementsByClassName('sort')[0].innerHTML += namespaceSearchLink;
  1020. } else if (pathname.startsWith('/favorites/')) { // favorites pages
  1021. if (window.location.search.length) {
  1022. document.getElementById('favorites-random-button').insertAdjacentHTML('afterend', favSearchBtn);
  1023. } else {
  1024. document.getElementById('favorites-random-button').insertAdjacentHTML('afterend', favPageBtn);
  1025. }
  1026. } else if (pathname.startsWith('/artist/')) { // artist pages
  1027. document.getElementsByClassName('sort')[0].innerHTML += namespaceSearchLink;
  1028. } else if (pathname.startsWith('/tag/')) { // tag pages
  1029. document.getElementsByClassName('sort')[0].innerHTML += namespaceSearchLink;
  1030. } else if (pathname.startsWith('/group/')) { // group pages
  1031. document.getElementsByClassName('sort')[0].innerHTML += namespaceSearchLink;
  1032. } else if (pathname.startsWith('/category/')) { // category pages
  1033. document.getElementsByClassName('sort')[0].innerHTML += namespaceSearchLink;
  1034. } else if (pathname.startsWith('/search/')) { // search pages
  1035. document.getElementsByClassName('sort')[0].innerHTML += siteSearchLink;
  1036. }
  1037. }
  1038. }
  1039. })();
  1040. //------------------------ **Nhentai English Filter** ----------------------
  1041.  
  1042.  
  1043.  
  1044. //------------------------ **Nhentai Auto Login** --------------------------
  1045. (async function() {
  1046. const autoLoginEnabled = await GM.getValue('autoLoginEnabled', true);
  1047. const email = await GM.getValue('email');
  1048. const password = await GM.getValue('password');
  1049.  
  1050. // Login page
  1051. if (autoLoginEnabled && window.location.href.includes('/login/?next=/')) {
  1052. if (!email || !password) {
  1053. GM.setValue('email', prompt('Please enter your email:'));
  1054. GM.setValue('password', prompt('Please enter your password:'));
  1055. }
  1056. document.getElementById('id_username_or_email').value = email;
  1057. document.getElementById('id_password').value = password;
  1058. const errorMessage = document.querySelector('#errors');
  1059. if (!errorMessage || !errorMessage.textContent.includes('You need to solve the CAPTCHA.')) {
  1060. document.querySelector('button[type="submit"]').click();
  1061. } else {
  1062. console.log('CAPTCHA detected. Cannot auto-login.');
  1063. }
  1064. }
  1065. })();
  1066. //------------------------ **Nhentai Auto Login** --------------------------
  1067.  
  1068.  
  1069.  
  1070. //----------------------------**Settings**-----------------------------
  1071.  
  1072. // Function to add the settings button to the menu
  1073. function addSettingsButton() {
  1074. // Create the settings button
  1075. const settingsButtonHtml = `
  1076. <li>
  1077. <a href="/settings/">
  1078. <i class="fa fa-cog"></i>
  1079. Settings
  1080. </a>
  1081. </li>
  1082. `;
  1083. const settingsButton = $(settingsButtonHtml);
  1084.  
  1085. // Append the settings button to the dropdown menu and the left menu
  1086. const dropdownMenu = $('ul.dropdown-menu');
  1087. dropdownMenu.append(settingsButton);
  1088.  
  1089. const menu = $('ul.menu.left');
  1090. menu.append(settingsButton);
  1091. }
  1092.  
  1093. // Call the function to add the settings button
  1094. addSettingsButton();
  1095.  
  1096. // Handle settings page
  1097. if (window.location.href.includes('/settings')) {
  1098. // Remove 404 Not Found elements
  1099. const notFoundHeading = document.querySelector('h1');
  1100. if (notFoundHeading && notFoundHeading.textContent === '404 – Not Found') {
  1101. notFoundHeading.remove();
  1102. }
  1103.  
  1104. const notFoundMessage = document.querySelector('p');
  1105. if (notFoundMessage && notFoundMessage.textContent === "Looks like what you're looking for isn't here.") {
  1106. notFoundMessage.remove();
  1107. }
  1108.  
  1109. // Add settings form and random hentai preferences
  1110. const settingsHtml = `
  1111. <style>
  1112. #content {
  1113. padding: 20px;
  1114. background: #1a1a1a;
  1115. color: #fff;
  1116. border-radius: 5px;
  1117. }
  1118. #settingsForm {
  1119. display: flex;
  1120. flex-direction: column;
  1121. gap: 10px;
  1122. }
  1123. #settingsForm label {
  1124. display: flex;
  1125. align-items: center;
  1126. gap: 10px;
  1127. }
  1128. #settingsForm input[type="text"],
  1129. #settingsForm input[type="password"],
  1130. #settingsForm input[type="number"] {
  1131. width: calc(100% - 12px); /* Adjust for padding and borders */
  1132. padding: 5px;
  1133. border-radius: 3px;
  1134. border: 1px solid #333;
  1135. background: #333;
  1136. color: #fff;
  1137. }
  1138. #settingsForm button {
  1139. padding: 10px;
  1140. background: #2a2a2a;
  1141. border: 1px solid #333;
  1142. border-radius: 3px;
  1143. color: #fff;
  1144. cursor: pointer;
  1145. }
  1146. #settingsForm button:hover {
  1147. background: #333;
  1148. }
  1149. #autoLoginCredentials {
  1150. display: block;
  1151. margin-top: 10px;
  1152. }
  1153. #random-settings {
  1154. margin-top: 20px;
  1155. }
  1156. #random-settings label {
  1157. display: flex;
  1158. align-items: center;
  1159. gap: 10px;
  1160. }
  1161. #random-settings input[type="text"],
  1162. #random-settings input[type="number"] {
  1163. width: calc(100% - 12px); /* Adjust for padding and borders */
  1164. padding: 5px;
  1165. border-radius: 3px;
  1166. border: 1px solid #333;
  1167. background: #333;
  1168. color: #fff;
  1169. margin-bottom: 10px; /* Add spacing between fields */
  1170. }
  1171. /* Bookmark Import/Export Buttons */
  1172. .bookmark-actions {
  1173. display: flex;
  1174. gap: 10px;
  1175. margin-top: 10px;
  1176. }
  1177. .bookmark-actions button {
  1178. padding: 10px;
  1179. background-color: #007bff;
  1180. border: none;
  1181. color: white;
  1182. cursor: pointer;
  1183. }
  1184. .bookmark-actions button:hover {
  1185. background-color: #0056b3;
  1186. }
  1187. #importBookmarksFile {
  1188. display: none;
  1189. }
  1190. </style>
  1191. <div id="content">
  1192. <h1>Settings</h1>
  1193. <form id="settingsForm">
  1194. <label>
  1195. <input type="checkbox" id="findSimilarEnabled">
  1196. Enable Find Similar Button
  1197. </label>
  1198. <label>
  1199. <input type="checkbox" id="englishFilterEnabled">
  1200. Enable English Filter Button
  1201. </label>
  1202. <label>
  1203. <input type="checkbox" id="autoLoginEnabled">
  1204. Enable Auto Login
  1205. </label>
  1206. <div id="autoLoginCredentials">
  1207. <label>
  1208. Email: <input type="text" id="email">
  1209. </label>
  1210. <label>
  1211. Password: <input type="password" id="password">
  1212. </label>
  1213. </div>
  1214. <label>
  1215. <input type="checkbox" id="findAltmangaEnabled">
  1216. Enable Find Altmanga Button
  1217. </label>
  1218. <label>
  1219. <input type="checkbox" id="findAltMangaThumbnailEnabled">
  1220. Enable Find Alt Manga (Thumbnail Version)
  1221. </label>
  1222. <!-- Bookmark Section -->
  1223. <label>
  1224. <input type="checkbox" id="bookmarksEnabled">
  1225. Enable Bookmarks Button
  1226. </label>
  1227. <div class="bookmark-actions">
  1228. <button type="button" id="exportBookmarks">Export Bookmarks</button>
  1229. <button type="button" id="importBookmarks">Import Bookmarks</button>
  1230. <input type="file" id="importBookmarksFile" accept=".json">
  1231. </div>
  1232. <div id="random-settings">
  1233. <h3>Random Hentai Preferences</h3>
  1234. <label>Language: <input type="text" id="pref-language"></label>
  1235. <label>Tags: <input type="text" id="pref-tags"></label>
  1236. <label>Blacklisted Tags: <input type="text" id="blacklisted-tags"></label>
  1237. <label>Minimum Pages: <input type="number" id="pref-pages-min"></label>
  1238. <label>Maximum Pages: <input type="number" id="pref-pages-max"></label>
  1239. <label>
  1240. <input type="checkbox" id="matchAllTags">
  1241. Match All Tags (unchecked = match any)
  1242. </label>
  1243. </div>
  1244. <button type="submit">Save Settings</button>
  1245. </form>
  1246. </div>
  1247. `;
  1248. // Append settings form to the container
  1249. $('div.container').append(settingsHtml);
  1250.  
  1251. // Load settings
  1252. (async function() {
  1253. const findSimilarEnabled = await GM.getValue('findSimilarEnabled', true);
  1254. const englishFilterEnabled = await GM.getValue('englishFilterEnabled', true);
  1255. const autoLoginEnabled = await GM.getValue('autoLoginEnabled', true);
  1256. const email = await GM.getValue('email', '');
  1257. const password = await GM.getValue('password', '');
  1258. const findAltmangaEnabled = await GM.getValue('findAltmangaEnabled', true);
  1259. const bookmarksEnabled = await GM.getValue('bookmarksEnabled', true);
  1260. const language = await GM.getValue('randomPrefLanguage', '');
  1261. const tags = await GM.getValue('randomPrefTags', []);
  1262. const pagesMin = await GM.getValue('randomPrefPagesMin', '');
  1263. const pagesMax = await GM.getValue('randomPrefPagesMax', '');
  1264. const matchAllTags = await GM.getValue('matchAllTags', true);
  1265. const blacklistedTags = await GM.getValue('blacklistedTags', []);
  1266. const findAltMangaThumbnailEnabled = await GM.getValue('findAltMangaThumbnailEnabled', true);
  1267.  
  1268. $('#findSimilarEnabled').prop('checked', findSimilarEnabled);
  1269. $('#englishFilterEnabled').prop('checked', englishFilterEnabled);
  1270. $('#autoLoginEnabled').prop('checked', autoLoginEnabled);
  1271. $('#email').val(email);
  1272. $('#password').val(password);
  1273. $('#findAltmangaEnabled').prop('checked', findAltmangaEnabled);
  1274. $('#bookmarksEnabled').prop('checked', bookmarksEnabled);
  1275. $('#pref-language').val(language);
  1276. $('#pref-tags').val(tags.join(', '));
  1277. $('#pref-pages-min').val(pagesMin);
  1278. $('#pref-pages-max').val(pagesMax);
  1279. $('#autoLoginCredentials').toggle(autoLoginEnabled);
  1280. $('#matchAllTags').prop('checked', matchAllTags);
  1281. $('#blacklisted-tags').val(blacklistedTags.join(', '));
  1282. $('#findAltMangaThumbnailEnabled').prop('checked', findAltMangaThumbnailEnabled);
  1283. })();
  1284.  
  1285. // Save settings
  1286. $('#settingsForm').on('submit', async function(event) {
  1287. event.preventDefault();
  1288.  
  1289. const findSimilarEnabled = $('#findSimilarEnabled').prop('checked');
  1290. const englishFilterEnabled = $('#englishFilterEnabled').prop('checked');
  1291. const autoLoginEnabled = $('#autoLoginEnabled').prop('checked');
  1292. const email = $('#email').val();
  1293. const password = $('#password').val();
  1294. const findAltmangaEnabled = $('#findAltmangaEnabled').prop('checked');
  1295. const bookmarksEnabled = $('#bookmarksEnabled').prop('checked');
  1296. const language = $('#pref-language').val();
  1297. let tags = $('#pref-tags').val().split(',').map(tag => tag.trim());
  1298. tags = tags.map(tag => tag.replace(/-/g, ' ')); // Replace hyphens with spaces
  1299. let blacklistedTags = $('#blacklisted-tags').val().split(',').map(tag => tag.trim());
  1300. blacklistedTags = blacklistedTags.map(tag => tag.replace(/-/g, ' ')); // Replace hyphens with spaces
  1301. const pagesMin = $('#pref-pages-min').val();
  1302. const pagesMax = $('#pref-pages-max').val();
  1303. const matchAllTags = $('#matchAllTags').prop('checked');
  1304. const findAltMangaThumbnailEnabled = $('#findAltMangaThumbnailEnabled').prop('checked');
  1305.  
  1306. await GM.setValue('findSimilarEnabled', findSimilarEnabled);
  1307. await GM.setValue('englishFilterEnabled', englishFilterEnabled);
  1308. await GM.setValue('autoLoginEnabled', autoLoginEnabled);
  1309. await GM.setValue('email', email);
  1310. await GM.setValue('password', password);
  1311. await GM.setValue('findAltmangaEnabled', findAltmangaEnabled);
  1312. await GM.setValue('bookmarksEnabled', bookmarksEnabled);
  1313. await GM.setValue('randomPrefLanguage', language);
  1314. await GM.setValue('blacklistedTags', blacklistedTags);
  1315. await GM.setValue('randomPrefTags', tags);
  1316. await GM.setValue('randomPrefPagesMin', pagesMin);
  1317. await GM.setValue('randomPrefPagesMax', pagesMax);
  1318. await GM.setValue('matchAllTags', matchAllTags);
  1319. await GM.setValue('findAltMangaThumbnailEnabled', findAltMangaThumbnailEnabled);
  1320.  
  1321. // Show custom popup instead of alert
  1322. showPopup('Settings saved!');
  1323. });
  1324.  
  1325. // Toggle auto login credentials
  1326. $('#autoLoginEnabled').on('change', function() {
  1327. $('#autoLoginCredentials').toggle(this.checked);
  1328. });
  1329.  
  1330.  
  1331. // Import Bookmarked Pages
  1332. async function importBookmarkedPages(file) {
  1333. try {
  1334. const reader = new FileReader();
  1335. const fileContent = await new Promise((resolve, reject) => {
  1336. reader.onload = () => resolve(reader.result);
  1337. reader.onerror = () => reject(reader.error);
  1338. reader.readAsText(file);
  1339. });
  1340.  
  1341. const importedBookmarks = JSON.parse(fileContent);
  1342. if (!Array.isArray(importedBookmarks)) {
  1343. throw new Error('Invalid file format');
  1344. }
  1345.  
  1346. const existingBookmarks = await GM.getValue('bookmarkedPages', []);
  1347. const mergedBookmarks = [...new Set([...existingBookmarks, ...importedBookmarks])]; // Merge without duplicates
  1348. await GM.setValue('bookmarkedPages', mergedBookmarks);
  1349. alert('Bookmarks imported successfully!');
  1350. } catch (error) {
  1351. alert(`Failed to import bookmarks: ${error.message}`);
  1352. }
  1353. }
  1354. // Add event listeners to buttons on the settings page
  1355. function setupBookmarkButtons() {
  1356. // Export Button
  1357. document.getElementById('exportBookmarks').addEventListener('click', exportBookmarkedPages);
  1358.  
  1359. // Import Button
  1360. document.getElementById('importBookmarks').addEventListener('click', () => {
  1361. document.getElementById('importBookmarksFile').click();
  1362. });
  1363.  
  1364. // Handle file selection for import
  1365. document.getElementById('importBookmarksFile').addEventListener('change', (event) => {
  1366. const file = event.target.files[0];
  1367. if (file) {
  1368. importBookmarkedPages(file);
  1369. }
  1370. });
  1371. }
  1372. // Call this function after settings form is rendered
  1373. setupBookmarkButtons();
  1374.  
  1375. }
  1376.  
  1377. function showPopup(message) {
  1378. const popup = document.createElement('div');
  1379. popup.id = 'popup';
  1380. popup.innerHTML = `
  1381. <div class="popup-content">
  1382. <button class="close-btn">&times;</button>
  1383. <p>${message}</p>
  1384. </div>
  1385. `;
  1386. document.body.appendChild(popup);
  1387. // Add CSS styling for the popup
  1388. const style = document.createElement('style');
  1389. style.textContent = `
  1390. #popup {
  1391. position: fixed;
  1392. top: 50%;
  1393. left: 50%;
  1394. transform: translate(-50%, -50%);
  1395. background: rgba(0, 0, 0, 0.9);
  1396. color: #fff;
  1397. border-radius: 5px;
  1398. z-index: 9999;
  1399. padding: 15px;
  1400. width: 250px; /* Make the popup smaller */
  1401. text-align: center;
  1402. }
  1403. .popup-content {
  1404. position: relative;
  1405. padding: 10px; /* Adjust padding for a smaller popup */
  1406. }
  1407. .close-btn {
  1408. position: absolute;
  1409. top: 5px; /* Position closer to the top */
  1410. right: 10px; /* Position closer to the right */
  1411. background: none;
  1412. border: none;
  1413. color: #fff;
  1414. font-size: 18px; /* Adjust font size for a smaller popup */
  1415. cursor: pointer;
  1416. transition: color 0.3s, opacity 0.3s;
  1417. }
  1418. .close-btn:hover {
  1419. color: #ff0000;
  1420. opacity: 0.7;
  1421. }
  1422. `;
  1423. document.head.appendChild(style);
  1424. // Close the popup when the close button is clicked
  1425. document.querySelector('.close-btn').addEventListener('click', function() {
  1426. document.body.removeChild(popup);
  1427. document.head.removeChild(style); // Remove the styling
  1428. });
  1429.  
  1430. // Optionally remove the popup after a few seconds
  1431. setTimeout(() => {
  1432. if (document.body.contains(popup)) {
  1433. document.body.removeChild(popup);
  1434. document.head.removeChild(style); // Remove the styling
  1435. }
  1436. }, 3000); // Adjust the time as needed
  1437. }
  1438.  
  1439. function exportBookmarkedPages() {
  1440.     GM.getValue('bookmarkedPages', []).then(bookmarkedPages => {
  1441.         const blob = new Blob([JSON.stringify(bookmarkedPages, null, 2)], { type: 'application/json' });
  1442.         const link = document.createElement('a');
  1443.         link.href = URL.createObjectURL(blob);
  1444.         link.download = 'bookmarked_pages.json';
  1445.         document.body.appendChild(link);
  1446.         link.click();
  1447.         document.body.removeChild(link);
  1448.     });
  1449. }
  1450.  
  1451. //----------------------------**Settings**--------------------------------------------
  1452.  
  1453.  
  1454.  
  1455.  
  1456.  
  1457. //----------------------------**Random Hentai Preferences**----------------------------
  1458. // Intercept random button clicks only if preferences are set
  1459. document.addEventListener('click', async function(event) {
  1460. const target = event.target;
  1461. if (target.tagName === 'A' && target.getAttribute('href') === '/random/') {
  1462. event.preventDefault(); // Prevent the default navigation
  1463.  
  1464. // Show the loading popup immediately
  1465. showLoadingPopup();
  1466.  
  1467. // Check if user preferences are set
  1468. const preferencesSet = await arePreferencesSet();
  1469.  
  1470. if (preferencesSet) {
  1471. // Set a flag to stop the search if needed
  1472. window.searchInProgress = true;
  1473. fetchRandomHentai();
  1474. } else {
  1475. // Close the popup and proceed with the default action
  1476. hideLoadingPopup();
  1477. window.location.href = '/random/';
  1478. }
  1479. }
  1480. });
  1481.  
  1482. async function arePreferencesSet() {
  1483. try {
  1484. const language = await GM.getValue('randomPrefLanguage', '');
  1485. const tags = await GM.getValue('randomPrefTags', []);
  1486. const pagesMin = parseInt(await GM.getValue('randomPrefPagesMin', ''), 10);
  1487. const pagesMax = parseInt(await GM.getValue('randomPrefPagesMax', ''), 10);
  1488.  
  1489. return language || tags.length > 0 || !isNaN(pagesMin) || !isNaN(pagesMax);
  1490. } catch (error) {
  1491. console.error('Error checking preferences:', error);
  1492. return false;
  1493. }
  1494. }
  1495.  
  1496. function showLoadingPopup() {
  1497. if (window.searchInProgress) {
  1498. showPopup('Already searching for random content!');
  1499. return;
  1500. }
  1501.  
  1502. // Create and display the popup
  1503. const popup = document.createElement('div');
  1504. popup.id = 'loading-popup';
  1505. popup.style.position = 'fixed';
  1506. popup.style.top = '50%';
  1507. popup.style.left = '50%';
  1508. popup.style.transform = 'translate(-50%, -50%)';
  1509. popup.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
  1510. popup.style.color = 'white';
  1511. popup.style.padding = '20px';
  1512. popup.style.borderRadius = '8px';
  1513. popup.style.zIndex = '9999';
  1514. popup.style.display = 'flex';
  1515. popup.style.flexDirection = 'column';
  1516. popup.style.alignItems = 'center';
  1517. popup.style.justifyContent = 'center';
  1518.  
  1519. // Popup content with image container and buttons
  1520. popup.innerHTML = `
  1521. <span>Searching for random content...</span>
  1522. <div id="cover-preview-container" style="margin-top: 10px; width: 350px; height: 192px; display: flex; align-items: center; justify-content: center; overflow: hidden; border-radius: 8px;">
  1523. <a id="cover-preview-link" href="#" style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; text-decoration: none;">
  1524. <img id="cover-preview" style="max-width: 100%; max-height: 100%; object-fit: contain; display: none; cursor: pointer;" />
  1525. </a>
  1526. </div>
  1527. <div id="preview-notes" style="margin-top: 10px; color: white; text-align: center;">
  1528. <!-- Notes will be inserted here -->
  1529. </div>
  1530. <div style="margin-top: 20px; display: flex; gap: 15px;">
  1531. <button id="previous-image" class="control-button" style="background: none; border: none; color: white; cursor: pointer; font-size: 20px; transition: color 0.3s ease, transform 0.3s ease;">
  1532. <i class="fas fa-arrow-left"></i>
  1533. </button>
  1534. <button id="pause-search" class="control-button" style="background: none; border: none; color: white; cursor: pointer; font-size: 20px; transition: color 0.3s ease, transform 0.3s ease;">
  1535. <i class="fas fa-pause"></i>
  1536. </button>
  1537. <button id="next-image" class="control-button" style="background: none; border: none; color: white; cursor: pointer; font-size: 20px; transition: color 0.3s ease, transform 0.3s ease;">
  1538. <i class="fas fa-arrow-right"></i>
  1539. </button>
  1540. </div>
  1541. <button class="close" style="margin-top: 20px; background: none; border: none; font-size: 24px; color: white; cursor: pointer;">&times;</button>
  1542. `;
  1543.  
  1544. document.body.appendChild(popup);
  1545.  
  1546. // Add event listener to close button
  1547. const closeButton = popup.querySelector('.close');
  1548. closeButton.addEventListener('click', function() {
  1549. hideLoadingPopup();
  1550. window.searchInProgress = false; // Stop the search
  1551. });
  1552.  
  1553. // Add hover effect for the close button
  1554. closeButton.addEventListener('mouseenter', function() {
  1555. closeButton.style.color = 'red';
  1556. closeButton.style.opacity = '0.7';
  1557. });
  1558.  
  1559. closeButton.addEventListener('mouseleave', function() {
  1560. closeButton.style.color = 'white';
  1561. closeButton.style.opacity = '1';
  1562. });
  1563.  
  1564. // Add hover effect for control buttons
  1565. const controlButtons = document.querySelectorAll('.control-button');
  1566. controlButtons.forEach(button => {
  1567. button.addEventListener('mouseenter', function() {
  1568. button.style.color = '#ddd'; // Light color on hover
  1569. button.style.transform = 'scale(1.1)'; // Slightly enlarge button
  1570. });
  1571.  
  1572. button.addEventListener('mouseleave', function() {
  1573. button.style.color = 'white'; // Original color
  1574. button.style.transform = 'scale(1)'; // Return to original size
  1575. });
  1576. });
  1577.  
  1578. // Add event listeners for control buttons
  1579. document.getElementById('previous-image').addEventListener('click', showPreviousImage);
  1580. document.getElementById('pause-search').addEventListener('click', togglePause);
  1581. document.getElementById('next-image').addEventListener('click', showNextImage);
  1582.  
  1583. // Add click event listener to the preview image to navigate to the content URL
  1584. document.getElementById('cover-preview').addEventListener('click', function() {
  1585. const currentImageIndex = parseInt(localStorage.getItem('currentImageIndex') || '0', 10);
  1586. const images = getImagesFromLocalStorage();
  1587. if (images[currentImageIndex] && images[currentImageIndex].url) {
  1588. window.location.href = images[currentImageIndex].url;
  1589. }
  1590. });
  1591. }
  1592.  
  1593.  
  1594.  
  1595. function hideLoadingPopup() {
  1596. const popup = document.getElementById('loading-popup');
  1597. if (popup) {
  1598. document.body.removeChild(popup);
  1599. }
  1600. }
  1601.  
  1602. async function fetchRandomHentai() {
  1603. try {
  1604. if (!window.searchInProgress) return; // Stop if search was canceled
  1605. const response = await fetch('https://nhentai.net/random/', { method: 'HEAD' });
  1606. await analyzeURL(response.url);
  1607. } catch (error) {
  1608. console.error('Error fetching random URL:', error);
  1609. }
  1610. }
  1611.  
  1612. async function analyzeURL(url) {
  1613. try {
  1614. if (!window.searchInProgress) return; // Stop if search was canceled
  1615. const response = await fetch(url);
  1616. const html = await response.text();
  1617. const parser = new DOMParser();
  1618. const doc = parser.parseFromString(html, 'text/html');
  1619.  
  1620. const coverImage = doc.querySelector('#cover img.lazyload');
  1621. const coverImageUrl = coverImage ? (coverImage.getAttribute('data-src') || coverImage.src) : null;
  1622.  
  1623. const title = doc.querySelector('#info h1')?.textContent.trim();
  1624. const tags = Array.from(doc.querySelectorAll('#tags .tag')).map(tag => tag.textContent.trim());
  1625. const pages = parseInt(doc.querySelector('#tags .tag-container:nth-last-child(2) .name')?.textContent.trim(), 10);
  1626. const uploadDate = doc.querySelector('#tags .tag-container:last-child time')?.getAttribute('datetime');
  1627.  
  1628. // Extract and handle languages
  1629. let languages = [];
  1630. const tagContainers = doc.querySelectorAll('.tag-container.field-name');
  1631. tagContainers.forEach(container => {
  1632. if (container.textContent.includes('Languages:')) {
  1633. const languageElements = container.querySelectorAll('.tags .tag .name');
  1634. languageElements.forEach(languageElement => {
  1635. let language = languageElement.textContent.trim().toLowerCase();
  1636. languages.push(language);
  1637. });
  1638. }
  1639. });
  1640.  
  1641. // Determine which language to display
  1642. let languageDisplay = 'Unknown';
  1643.  
  1644. if (languages.includes('english')) {
  1645. languageDisplay = 'English';
  1646. } else if (languages.includes('translated') && languages.length === 1) {
  1647. languageDisplay = 'English';
  1648. } else if (languages.includes('translated') && languages.length > 1) {
  1649. // Exclude 'translated' and show other language(s)
  1650. const otherLanguages = languages.filter(lang => lang !== 'translated');
  1651. languageDisplay = otherLanguages.length > 0 ? otherLanguages.map(lang => lang.charAt(0).toUpperCase() + lang.slice(1)).join(', ') : 'Unknown';
  1652. } else {
  1653. languageDisplay = languages.map(lang => lang.charAt(0).toUpperCase() + lang.slice(1)).join(', ');
  1654. }
  1655.  
  1656. if (coverImageUrl) {
  1657. saveImageToLocalStorage(coverImageUrl, url, languageDisplay, pages, title);
  1658. showPreviousImage(); // Automatically show the next image if not paused. Says the showPreviousImage because I flipped the way images are saved in local storage so I had to flip everything
  1659. }
  1660.  
  1661. if (await meetsUserPreferences(tags, pages)) {
  1662. hideLoadingPopup();
  1663. window.location.href = url;
  1664. } else {
  1665. console.log('Does not meet user preferences, fetching another random hentai.');
  1666. fetchRandomHentai();
  1667. }
  1668. } catch (error) {
  1669. console.error('Error analyzing page:', error);
  1670. }
  1671. }
  1672.  
  1673.  
  1674.  
  1675.  
  1676.  
  1677. async function meetsUserPreferences(tags, pages) {
  1678. try {
  1679. const preferredLanguage = (await GM.getValue('randomPrefLanguage', '')).toLowerCase();
  1680. const preferredTags = (await GM.getValue('randomPrefTags', [])).map(tag => tag.toLowerCase());
  1681. const blacklistedTags = (await GM.getValue('blacklistedTags', [])).map(tag => tag.toLowerCase());
  1682. const preferredPagesMin = parseInt(await GM.getValue('randomPrefPagesMin', ''), 10);
  1683. const preferredPagesMax = parseInt(await GM.getValue('randomPrefPagesMax', ''), 10);
  1684. const matchAllTags = await GM.getValue('matchAllTags', true);
  1685.  
  1686. // Strip tag counts and only keep the tag names
  1687. const cleanedTags = tags.map(tag => tag.replace(/\d+K?$/, '').trim().toLowerCase());
  1688.  
  1689. const hasPreferredLanguage = preferredLanguage ? cleanedTags.includes(preferredLanguage) : true;
  1690. let hasPreferredTags;
  1691. if (preferredTags.length > 0) {
  1692. if (matchAllTags) {
  1693. hasPreferredTags = preferredTags.every(tag => cleanedTags.includes(tag));
  1694. } else {
  1695. hasPreferredTags = preferredTags.some(tag => cleanedTags.includes(tag));
  1696. }
  1697. } else {
  1698. hasPreferredTags = true;
  1699. }
  1700.  
  1701. const withinPageRange = (!isNaN(preferredPagesMin) ? pages >= preferredPagesMin : true) &&
  1702. (!isNaN(preferredPagesMax) ? pages <= preferredPagesMax : true);
  1703.  
  1704. const hasBlacklistedTags = blacklistedTags.some(tag => cleanedTags.includes(tag));
  1705.  
  1706. return hasPreferredLanguage && hasPreferredTags && withinPageRange && !hasBlacklistedTags;
  1707. } catch (error) {
  1708. console.error('Error checking user preferences:', error);
  1709. return false;
  1710. }
  1711. }
  1712.  
  1713.  
  1714. function saveImageToLocalStorage(imageUrl, hentaiUrl, language, pages, title) {
  1715. let images = JSON.parse(localStorage.getItem('hentaiImages') || '[]');
  1716. images.unshift({ imageUrl, url: hentaiUrl, language, pages, title }); // Add title to stored data
  1717.  
  1718. if (images.length > 10) {
  1719. images.pop();
  1720. }
  1721.  
  1722. localStorage.setItem('hentaiImages', JSON.stringify(images));
  1723. localStorage.setItem('currentImageIndex', '0');
  1724. updatePreviewImage(imageUrl, language, pages, title);
  1725. }
  1726.  
  1727.  
  1728. function getImagesFromLocalStorage() {
  1729. return JSON.parse(localStorage.getItem('hentaiImages') || '[]');
  1730. }
  1731.  
  1732. function showNextImage() {
  1733. const images = getImagesFromLocalStorage();
  1734. if (images.length === 0) return;
  1735.  
  1736. let currentIndex = parseInt(localStorage.getItem('currentImageIndex') || '0', 10);
  1737. currentIndex = (currentIndex - 1 + images.length) % images.length;
  1738. localStorage.setItem('currentImageIndex', currentIndex.toString());
  1739.  
  1740. const currentImage = images[currentIndex];
  1741. updatePreviewImage(currentImage.imageUrl, currentImage.language, currentImage.pages, currentImage.title);
  1742. }
  1743.  
  1744. function showPreviousImage() {
  1745. const images = getImagesFromLocalStorage();
  1746. if (images.length === 0) return;
  1747.  
  1748. let currentIndex = parseInt(localStorage.getItem('currentImageIndex') || '0', 10);
  1749. currentIndex = (currentIndex + 1) % images.length;
  1750. localStorage.setItem('currentImageIndex', currentIndex.toString());
  1751.  
  1752. const currentImage = images[currentIndex];
  1753. updatePreviewImage(currentImage.imageUrl, currentImage.language, currentImage.pages, currentImage.title);
  1754. }
  1755.  
  1756.  
  1757. function updatePreviewImage(imageUrl, language = '', pages = '', title = '') {
  1758. const coverPreview = document.getElementById('cover-preview');
  1759. const coverPreviewLink = document.getElementById('cover-preview-link');
  1760. const notesContainer = document.getElementById('preview-notes');
  1761. const isPaused = !window.searchInProgress;
  1762.  
  1763. if (coverPreview) {
  1764. coverPreview.src = imageUrl;
  1765. coverPreview.style.display = 'block';
  1766. }
  1767.  
  1768. // Update the link URL
  1769. if (coverPreviewLink) {
  1770. const images = getImagesFromLocalStorage();
  1771. const currentIndex = parseInt(localStorage.getItem('currentImageIndex') || '0', 10);
  1772. if (images[currentIndex] && images[currentIndex].url) {
  1773. coverPreviewLink.href = images[currentIndex].url;
  1774. }
  1775. }
  1776.  
  1777. if (notesContainer) {
  1778. notesContainer.innerHTML = `
  1779. ${isPaused ? `<div style="margin-bottom: 5px;"><span style="font-weight: bold;">Title:</span> ${title || 'Title Not Available'}</div>` : ''}
  1780. <div>Language: ${language || 'N/A'}</div>
  1781. <div>Pages: ${pages || 'N/A'}</div>
  1782. `;
  1783. }
  1784. }
  1785.  
  1786. // Remove the old click event listener from the image and add it to the link instead (Not necessary may remove later)
  1787. document.addEventListener('DOMContentLoaded', function() {
  1788. const coverPreviewLink = document.getElementById('cover-preview-link');
  1789. if (coverPreviewLink) {
  1790. coverPreviewLink.addEventListener('click', function(event) {
  1791. event.preventDefault();
  1792. const currentImageIndex = parseInt(localStorage.getItem('currentImageIndex') || '0', 10);
  1793. const images = getImagesFromLocalStorage();
  1794. if (images[currentImageIndex] && images[currentImageIndex].url) {
  1795. window.location.href = images[currentImageIndex].url;
  1796. }
  1797. });
  1798. }
  1799. });
  1800.  
  1801. function togglePause() {
  1802. window.searchInProgress = !window.searchInProgress;
  1803. const pauseButtonIcon = document.querySelector('#pause-search i');
  1804. pauseButtonIcon.className = window.searchInProgress ? 'fas fa-pause' : 'fas fa-play';
  1805.  
  1806. // Update the current image display with the new pause state
  1807. const images = getImagesFromLocalStorage();
  1808. const currentIndex = parseInt(localStorage.getItem('currentImageIndex') || '0', 10);
  1809. if (images[currentIndex]) {
  1810. const currentImage = images[currentIndex];
  1811. updatePreviewImage(currentImage.imageUrl, currentImage.language, currentImage.pages, currentImage.title);
  1812. }
  1813.  
  1814. if (window.searchInProgress) {
  1815. fetchRandomHentai();
  1816. }
  1817. }
  1818.  
  1819. // Initialize the current image index
  1820. localStorage.setItem('currentImageIndex', '0');
  1821.  
  1822.  
  1823. //----------------------------**Random Hentai Preferences**----------------------------