// ==UserScript==
// @name Nhentai Plus+
// @namespace github.com/longkidkoolstar
// @version 9.4.1
// @description Enhances the functionality of Nhentai website.
// @author longkidkoolstar
// @match https://nhentai.net/*
// @require https://code.jquery.com/jquery-3.6.0.min.js
// @require https://cdn.jsdelivr.net/npm/[email protected]/Sortable.min.js
// @icon https://i.imgur.com/AOs1HMS.png
// @license MIT
// @grant GM.setValue
// @grant GM.getValue
// @grant GM.addStyle
// @grant GM.deleteValue
// @grant GM.openInTab
// @grant GM.listValues
// @grant GM.xmlHttpRequest
// ==/UserScript==
//----------------------- **Change Log** ------------------------------------------
const CURRENT_VERSION = "9.4.1";
const CHANGELOG_URL = "https://raw.githubusercontent.com/longkidkoolstar/Nhentai-Plus/refs/heads/main/changelog.json";
(async () => {
const lastSeenVersion = await GM.getValue("lastSeenVersion", "0.0.0");
if (CURRENT_VERSION !== lastSeenVersion) {
try {
const res = await fetch(CHANGELOG_URL);
const changelogData = await res.json();
const log = changelogData[CURRENT_VERSION];
if (log) {
const msg = `🆕 Version ${CURRENT_VERSION} (${log.date})\n\n` +
log.changes.map(line => `• ${line}`).join("\n");
showChangelogPopup(msg);
}
await GM.setValue("lastSeenVersion", CURRENT_VERSION);
} catch (err) {
console.error("Error fetching or displaying changelog:", err);
}
}
})();
function showChangelogPopup(message) {
const popup = document.createElement("div");
popup.style.position = "fixed";
popup.style.bottom = "20px";
popup.style.right = "20px";
popup.style.maxWidth = "350px";
popup.style.backgroundColor = "#1e1e1e";
popup.style.color = "#fff";
popup.style.padding = "16px";
popup.style.borderRadius = "8px";
popup.style.boxShadow = "0 4px 12px rgba(0,0,0,0.5)";
popup.style.zIndex = 99999;
popup.style.fontFamily = "'Segoe UI', Arial, sans-serif";
popup.style.opacity = "0";
popup.style.transform = "translateY(20px)";
popup.style.transition = "opacity 0.3s ease, transform 0.3s ease";
popup.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<strong style="font-size: 16px;">Changelog</strong>
<button style="background: transparent; color: #aaa; border: none; cursor: pointer; font-size: 18px; line-height: 1; padding: 0;">×</button>
</div>
<pre style="white-space: pre-wrap; font-size: 13px; line-height: 1.4; margin: 0; color: #ddd;">${message}</pre>
`;
popup.querySelector("button").addEventListener("click", () => {
popup.style.opacity = "0";
popup.style.transform = "translateY(20px)";
setTimeout(() => popup.remove(), 300);
});
document.body.appendChild(popup);
// Trigger animation
setTimeout(() => {
popup.style.opacity = "1";
popup.style.transform = "translateY(0)";
}, 10);
// Auto-dismiss after 15 seconds
setTimeout(() => {
if (document.body.contains(popup)) {
popup.style.opacity = "0";
popup.style.transform = "translateY(20px)";
setTimeout(() => popup.remove(), 300);
}
}, 15000);
}
//----------------------- **Change Log** ------------------------------------------
//----------------------- **Fix Menu OverFlow**----------------------------------
// Nhentai Plus+.user.js
$(document).ready(async function() {
// Remove the 'required' attribute from the search input when must-add tags are enabled
const searchInput = document.querySelector('form.search input[name="q"]');
if (searchInput) {
const mustAddTagsEnabled = await GM.getValue('mustAddTagsEnabled', false);
const mustAddTags = (await GM.getValue('mustAddTags', [])).map(tag => tag.toLowerCase());
if (mustAddTagsEnabled && mustAddTags.length > 0) {
searchInput.removeAttribute('required');
}
}
var styles = `
@media (max-width: 644px) {
nav .collapse.open {
max-height: 600px;
}
}
`;
$("<style>").html(styles).appendTo("head");
});
//--------------------------**Fix Menu OverFlow**------------------------------------
/**
* Detects and removes old-format cache entries while preserving important data
*/
async function cleanupOldData() {
console.log("Starting cleanup of old format entries...");
const allKeys = await GM.listValues();
let removedCount = 0;
// Find and delete old manga_URL_ID format keys
const oldMangaKeys = allKeys.filter(key => key.startsWith('manga_http'));
for (const key of oldMangaKeys) {
await GM.deleteValue(key);
removedCount++;
}
// Find and handle URL to title mappings (old format bookmarks)
for (const key of allKeys) {
// Skip keys that are part of the new format or important lists
if (key === 'bookmarkedPages' ||
key === 'bookmarkedMangas' ||
key.startsWith('manga_') ||
key.startsWith('bookmark_manga_ids_')) {
continue;
}
// Check if it's an old-style URL to title mapping
const value = await GM.getValue(key);
if (typeof value === 'string' &&
(value.startsWith('Tag: ') ||
value.startsWith('Search: ') ||
value.startsWith('Artist: ') ||
value.startsWith('Character: ') ||
value.startsWith('Group: ') ||
value.startsWith('Parody: '))) {
// This is an old-style bookmark title, safe to remove
await GM.deleteValue(key);
removedCount++;
}
}
// Get all stored keys
const storedKeys = await GM.listValues();
// Filter keys that match the old title storage format
const oldTitleKeys = storedKeys.filter(key => key.startsWith('title_'));
// Delete each old title key
for (const key of oldTitleKeys) {
await GM.deleteValue(key);
console.log(`Deleted old title storage key: ${key}`);
removedCount++;
}
console.log(`Cleanup complete! Removed ${removedCount} old format entries.`);
return removedCount;
}
cleanupOldData();
/**
* Detects and removes old-format cache entries while preserving important data
*/
//------------------------ **Find Similar Button** ------------------
// Initialize maxTagsToSelect from localStorage or default to 5
let maxTagsToSelect = GM.getValue('maxTagsToSelect');
if (maxTagsToSelect === undefined) {
maxTagsToSelect = 5;
GM.setValue('maxTagsToSelect', maxTagsToSelect);
} else {
maxTagsToSelect = parseInt(maxTagsToSelect); // Ensure it's parsed as an integer
}
// Array to store locked tags
const lockedTags = [];
// Function to create and insert 'Find Similar' button
async function createFindSimilarButton() {
const findSimilarEnabled = await GM.getValue('findSimilarEnabled', true);
if (!findSimilarEnabled) return;
if (isNaN(maxTagsToSelect)) {
maxTagsToSelect = await GM.getValue('maxTagsToSelect');
if (maxTagsToSelect === undefined) {
maxTagsToSelect = 5;
GM.setValue('maxTagsToSelect', maxTagsToSelect);
}
}
const downloadButton = document.getElementById('download');
if (!downloadButton) {
console.log('Download button not found.');
return;
}
const findSimilarButtonHtml = `
<a class="btn btn-primary btn-disabled tooltip find-similar">
<i class="fas fa-search"></i>
<span>Find Similar</span>
<div class="top">Click to find similar hentai<i></i></div>
<div id="lockedTagsCount">Locked tags: ${lockedTags.length}</div>
</a>
`;
const findSimilarButton = $(findSimilarButtonHtml);
// Insert 'Find Similar' button next to the download button
// Find the "Find Alt." button
const findAltButton = document.querySelector('a.btn.btn-primary.btn-disabled.tooltip.find-similar');
// Insert 'Find Similar' button next to the "Find Alt." button
if (findAltButton && downloadButton) {
$(findAltButton).after(findSimilarButton);
} else {
console.log('Download button or Find Alt. button not found.');
}
$('#lockedTagsCount').hide();
// Handle click event for 'Find Similar' button
findSimilarButton.click(async function() {
const tagsContainer = $('div.tag-container.field-name:contains("Tags:")');
if (!tagsContainer.length) {
console.log('Tags container not found.');
return;
}
// Find all tag links within the container
const tagLinks = tagsContainer.find('a.tag');
// Update locked tags counter
if (!tagLinks.length) {
console.log('No tag links found.');
return;
}
// Extract tag data (name and count) and assign probabilities based on count
const tagsData = Array.from(tagLinks).map(tagLink => {
const tagName = $(tagLink).find('.name').text().trim();
const tagCount = parseInt($(tagLink).find('.count').text().replace('K', '')) || 0;
const probability = Math.sqrt(tagCount); // Adjust this formula as needed
return { name: tagName, count: tagCount, probability: probability };
});
// Shuffle tag data array to randomize selection
shuffleArray(tagsData);
const selectedTags = [];
let numTagsSelected = 0;
// Add locked tags to the selected tags array
lockedTags.forEach(tag => {
selectedTags.push(tag);
numTagsSelected++;
});
tagsData.forEach(tag => {
if (numTagsSelected < maxTagsToSelect && !lockedTags.includes(tag.name) && Math.random() < tag.probability) {
selectedTags.push(tag.name);
numTagsSelected++;
}
});
// Join selected tag names into a search string
const searchTags = selectedTags.map(tag => 'tag:"' + tag + '"').join(' ');
const findSimilarType = await GM.getValue('findSimilarType', 'immediately');
const searchInput = $('input[name="q"]');
if (findSimilarType === 'immediately') {
if (searchInput.length > 0) {
// Update search input value with selected tags
searchInput.val(searchTags);
} else {
// If search input not found, create and submit a hidden form
const hiddenSearchFormHtml = `
<form role="search" action="/search/" method="GET" style="display: none;">
<input type="hidden" name="q" value="${searchTags}" />
</form>
`;
const hiddenSearchForm = $(hiddenSearchFormHtml);
$('body').append(hiddenSearchForm);
hiddenSearchForm.submit();
}
// Submit the form
$('button[type="submit"]').click();
} else if (findSimilarType === 'input-tags') {
if (searchInput.length > 0) {
// Update search input value with selected tags
searchInput.val(searchTags);
} else {
// If search input not found, create a hidden input
const hiddenSearchInputHtml = `
<input type="hidden" name="q" value="${searchTags}" />
`;
const hiddenSearchInput = $(hiddenSearchInputHtml);
$('body').append(hiddenSearchInput);
}
}
// Create and display the slider (only once)
if (!$('#tagSlider').length) {
createSlider();
}
});
// Handle double-click event for 'Find Similar' button
findSimilarButton.dblclick(async function() {
const searchTags = lockedTags.join(' ');
const searchInput = $('input[name="q"]');
if (searchInput.length > 0) {
// Update search input value with locked tags only
searchInput.val(searchTags);
} else {
// If search input not found, create and submit a hidden form with locked tags only
const hiddenSearchFormHtml = `
<form role="search" action="/search/" method="GET" style="display: none;">
<input type="hidden" name="q" value="${searchTags}" />
</form>
`;
const hiddenSearchForm = $(hiddenSearchFormHtml);
$('body').append(hiddenSearchForm);
hiddenSearchForm.submit();
}
// Create and display the slider (only once)
if (!$('#tagSlider').length) {
createSlider();
}
});
}
// Function to create and display the slider
async function createSlider() {
const sliderHtml = `
<div style="position: fixed; bottom: 20px; right: 20px; z-index: 9999;">
<input type="range" min="1" max="10" value="${maxTagsToSelect}" id="tagSlider">
<label for="tagSlider">Max Tags to Select: <span id="tagSliderValue">${maxTagsToSelect}</span></label>
</div>
`;
$(document.body).append(sliderHtml);
// Retrieve saved maxTagsToSelect value from GM storage (if available)
const savedMaxTags = await GM.getValue('maxTagsToSelect');
if (savedMaxTags !== undefined) {
maxTagsToSelect = parseInt(savedMaxTags);
$('#tagSlider').val(maxTagsToSelect);
$('#tagSliderValue').text(maxTagsToSelect);
}
// Update maxTagsToSelect based on slider value and save to GM storage
$('#tagSlider').on('input', async function() {
maxTagsToSelect = parseInt($(this).val());
$('#tagSliderValue').text(maxTagsToSelect);
// Store the updated maxTagsToSelect value in GM storage
await GM.setValue('maxTagsToSelect', maxTagsToSelect);
});
}
// Call the function to create 'Find Similar' button
createFindSimilarButton();
function updateLockedTagsCounter() {
const lockedTagsCount = lockedTags.length;
const lockedTagsCounter = $('#lockedTagsCount');
if (lockedTagsCount > 0) {
lockedTagsCounter.text(`Locked tags: ${lockedTagsCount}`).show();
if (lockedTagsCount > maxTagsToSelect) {
lockedTagsCounter.css('color', 'red');
} else {
lockedTagsCounter.css('color', ''); // Reset color to default
}
} else {
lockedTagsCounter.hide();
}
}
// Function to toggle lock buttons based on findSimilarEnabled
async function toggleLockButtons() {
const findSimilarEnabled = await GM.getValue('findSimilarEnabled', true);
if (findSimilarEnabled) {
$('span.lock-button').show();
} else {
$('span.lock-button').hide();
}
}
// Event listener for locking/unlocking tags
$(document).on('click', 'span.lock-button', function(event) {
event.stopPropagation(); // Prevent tag link click event from firing
const tagName = $(this).prev('a.tag').find('.name').text().trim();
if (lockedTags.includes(tagName)) {
// Tag is already locked, unlock it
const index = lockedTags.indexOf(tagName);
if (index !== -1) {
lockedTags.splice(index, 1);
}
$(this).html('<i class="fas fa-plus"></i>'); // Change icon to plus
updateLockedTagsCounter();
} else {
// Lock the tag
lockedTags.push(tagName);
$(this).html('<i class="fas fa-minus"></i>'); // Change icon to minus
updateLockedTagsCounter();
}
});
// Add lock button next to each tag
const tagsContainer = $('div.tag-container.field-name:contains("Tags:")');
if (tagsContainer.length) {
const tagLinks = tagsContainer.find('a.tag');
tagLinks.each(function(index, tagLink) {
const lockButtonHtml = `
<span class="lock-button" data-tag-index="${index}">
<i class="fas fa-plus"></i>
</span>
`;
const lockButton = $(lockButtonHtml);
$(tagLink).after(lockButton);
});
}
// Initialize lock buttons visibility based on findSimilarEnabled
toggleLockButtons();
console.log('Script setup complete.');
// Function to shuffle an array (Fisher-Yates shuffle algorithm)
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
//------------------------ **Find Similar Button** ------------------
//----------------------- **Find Alternative Manga Button** ------------------
// Adds a button to the page that allows the user to find alternative manga to the current one.
// Checks if the feature is enabled in the settings before appending the button.
async function addFindAltButton() {
const findAltmangaEnabled = await GM.getValue('findAltmangaEnabled', true);
if (!findAltmangaEnabled) return;
// Get the download button
const downloadButton = document.getElementById('download');
if (!downloadButton) {
console.log('Download button not found.');
return;
}
const copyTitleButtonHtml = `
<a class="btn btn-primary btn-disabled tooltip find-similar">
<i class="fas fa-code-branch"></i>
<span>Find Alt.</span>
<div class="top">Click to find alternative manga to this one<i></i></div>
</a>
`;
const copyTitleButton = $(copyTitleButtonHtml);
// Handle click event for the button
copyTitleButton.click(function() {
// Get the title element
const titleElement = $('h1.title');
if (!titleElement.length) {
console.log('Title element not found.');
return;
}
// Extract the text content from the pretty class if it exists
let titleText;
const prettyElement = titleElement.find('.pretty');
if (prettyElement.length) {
titleText = prettyElement.text();
} else {
titleText = titleElement.text();
}
// Remove text inside square brackets [], parentheses (), 'Ch.', 'ch.', 'Vol.', 'vol.', and all Chinese and Japanese characters
const cleanedTitleText = titleText.replace(/[\[\]\(\)]|Ch\.|ch\.|Vol\.|vol\.|Ep\.|Ep|ep\.|ep|\|[\u3002\uFF01-\uFF5E\u4E00-\u9FFF\u3040-\u309F\u30A0-\u30FF]|(?<!\w)-(?!\w)|\d+/g, '').trim();
// Find the search input
const searchInput = $('input[name="q"]');
if (searchInput.length > 0) {
// Update search input value with cleaned title text
searchInput.val(cleanedTitleText);
// Click the search button
const searchButton = $('button[type="submit"]');
if (searchButton.length) {
searchButton.click();
}
} else {
console.log('Search input not found.');
}
});
// Insert 'Find Similar' button next to the download button
$(downloadButton).after(copyTitleButton);
}
// Call the function to add the Copy Title button
addFindAltButton();
//------------------------ **Find Alternative Manga Button** ------------------
//------------------------ **Find Alternative Manga Button(Thumbnail Version)** ------------------
(async function() {
const findAltMangaThumbnailEnabled = await GM.getValue('findAltMangaThumbnailEnabled', true); // Default to true if not set
if (!findAltMangaThumbnailEnabled) return; // Exit if the feature is not enabled
const flagEn = "https://i.imgur.com/vSnHmmi.gif";
const flagJp = "https://i.imgur.com/GlArpuS.gif";
const flagCh = "https://i.imgur.com/7B55DYm.gif";
const non_english_fade_opacity = 0.3;
const partially_fade_all_non_english = true;
const mark_as_read_system_enabled = true;
const marked_as_read_fade_opacity = 0.3;
const auto_group_on_page_comics = true;
const version_grouping_filter_brackets = false;
let MARArray = [];
GM.getValue("MARArray", "[]").then((value) => {
if (typeof value === 'string') {
MARArray = JSON.parse(value);
}
GM.addStyle(`
.overlayFlag {
position: absolute;
display: inline-block;
top: 3px;
left: 3px;
z-index: 3;
width: 18px;
height: 12px;
}
.numOfVersions {
border-radius: 10px;
padding: 5px 10px;
position: absolute;
background-color: rgba(0,0,0,.7);
color: rgba(255,255,255,.8);
top: 7.5px;
left: 105px;
font-size: 12px;
font-weight: 900;
opacity: 1;
width: 40px;
z-index: 2;
display: none;
}
.findVersionButton {
border-radius: 10px;
padding: 5px 10px;
position: absolute;
background-color: rgba(0,0,0,.4);
color: rgba(255,255,255,.8);
bottom: 7.5px;
left: 7.5px;
font-size: 12px;
font-weight: 900;
opacity: 1;
width: 125px;
z-index: 2;
cursor: pointer;
}
.versionNextButton {
border-radius: 10px;
padding: 5px 10px;
position: absolute;
background-color: rgba(0,0,0,.7);
color: rgba(255,255,255,.8);
top: 7.5px;
right: 7.5px;
font-size: 12px;
font-weight: 900;
opacity: 1;
display: none;
z-index: 2;
cursor: pointer;
}
.versionPrevButton {
border-radius: 10px;
padding: 5px 10px;
position: absolute;
background-color: rgba(0,0,0,.7);
color: rgba(255,255,255,.8);
top: 7.5px;
left: 7.5px;
font-size: 12px;
font-weight: 900;
opacity: 1;
z-index: 2;
display: none;
cursor: pointer;
}
.newTabButton {
border-radius: 10px;
padding: 5px 10px;
position: absolute;
background-color: rgba(0,0,0,.4);
color: rgba(255,255,255,.8);
bottom: 7.5px;
right: 7.5px; /* Position on the right side */
font-size: 12px;
font-weight: 900;
opacity: 1;
width: auto; /* Smaller width since text is shorter */
z-index: 4;
cursor: pointer;
text-align: center;
}
/* Add hover effect */
.newTabButton:hover {
background-color: rgba(0,0,0,.7);
}
`);
function IncludesAll(string, search) {
string = CleanupSearchString(string);
search = CleanupSearchString(search);
if (string.length == 0 || search.length == 0) return false;
let searches = search.split(" ");
for (let i = 0; i < searches.length; i++) {
if (!!searches[i] && searches[i].length > 0 && !string.includes(searches[i])) return false;
}
return true;
}
async function AddAltVersionsToThis(target) {
let place = target;
const coverElement = place.parent().find(".cover:visible");
const href = coverElement.attr('href');
const captionTitle = place.parent().find(".cover:visible > .caption").text();
try {
let titles = [captionTitle]; // Start with the caption title
// Try to get the title from the manga page if href exists
if (href) {
try {
const response = await fetch(`https://nhentai.net${href}`);
if (response.ok) {
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const titleElement = doc.querySelector('.title');
if (titleElement) {
const prettySpan = titleElement.querySelector('.pretty');
let titleText = prettySpan ? prettySpan.textContent.trim() : titleElement.textContent.trim();
const cleanedTitleText = titleText.replace(/[\[\]\(\)]|Ch\.|ch\.|Vol\.|vol\.|Ep\.|Ep|ep\.|ep|\|[\u3002\uFF01-\uFF5E\u4E00-\u9FFF\u3040-\u309F\u30A0-\u30FF]|(?<!\w)-(?!\w)|\d+/g, '').trim();
// Add the cleaned title if it's different from the caption title
if (cleanedTitleText && cleanedTitleText !== captionTitle) {
titles.push(cleanedTitleText);
}
}
}
} catch (error) {
console.error("Error fetching title from manga page:", error);
}
}
// Process search with all collected titles
await processSearchWithMultipleTitles(titles);
} catch (error) {
console.error("Error in AddAltVersionsToThis:", error);
// Fallback to just the caption title if there's an error
processSearch(captionTitle);
}
// Function to process search with multiple titles and combine results
async function processSearchWithMultipleTitles(titles) {
let allResults = [];
let processedHrefs = new Set(); // To track unique results
for (const title of titles) {
if (!title || title.trim() === '') continue;
try {
// Use fetch API instead of jQuery's $.get to avoid XHR interception issues
const response = await fetch(BuildUrl(title));
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const text = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'text/html');
const found = doc.querySelectorAll(".container > .gallery");
if (found && found.length > 0) {
// Add unique results to allResults
for (let i = 0; i < found.length; i++) {
const resultHref = found[i].querySelector(".cover")?.getAttribute('href');
if (resultHref && !processedHrefs.has(resultHref)) {
processedHrefs.add(resultHref);
allResults.push(found[i]);
}
}
}
} catch (error) {
console.error(`Error searching for title "${title}":`, error);
}
}
if (allResults.length === 0) {
alert("No results found for any of the search terms");
return;
}
// Process the combined results
place.parent().find(".cover").remove();
try {
for (let i = 0; i < allResults.length; i++) {
// Convert DOM element to jQuery object for consistent handling
const $result = $(allResults[i]);
if (partially_fade_all_non_english) {
$result.find(".cover > img, .cover > .caption").css("opacity", non_english_fade_opacity);
}
const dataTags = $result.attr("data-tags") || "";
if (dataTags.includes("12227")) {
$result.find(".caption").append(`<img class="overlayFlag" src="` + flagEn + `">`);
$result.find(".cover > img, .cover > .caption").css("opacity", "1");
} else {
if (dataTags.includes("6346")) {
$result.find(".caption").append(`<img class="overlayFlag" src="` + flagJp + `">`);
} else if (dataTags.includes("29963")) {
$result.find(".caption").append(`<img class="overlayFlag" src="` + flagCh + `">`);
}
if (!partially_fade_all_non_english) {
$result.find(".cover > img, .cover > .caption").css("opacity", "1");
}
}
if (mark_as_read_system_enabled) {
let MARArraySelector = MARArray.join("'], .cover[href='");
$result.find(".cover[href='" + MARArraySelector + "']").append("<div class='readTag'>READ</div>");
let readTag = $result.find(".readTag");
if (!!readTag && readTag.length > 0) {
readTag.parent().parent().find(".cover > img, .cover > .caption").css("opacity", marked_as_read_fade_opacity);
}
}
let thumbnailReplacement;
const $img = $result.find(".cover > img");
const dataSrc = $img.attr("data-src");
if (dataSrc) {
thumbnailReplacement = dataSrc
.replace(/\/\/.+?\.nhentai/g, "//i1.nhentai") // Fixed CDN path
.replace("thumb.", "1."); // Generic replacement for all extensions
} else {
thumbnailReplacement = $img.attr("src")
.replace(/\/\/.+?\.nhentai/g, "//i1.nhentai") // Fixed CDN path
.replace("thumb.", "1."); // Generic replacement for all extensions
}
$img.attr("src", thumbnailReplacement);
place.parent().append($result.find(".cover"));
}
} catch (er) {
alert("Error modifying data: " + er);
return;
}
place.parent().find(".cover:not(:first)").css("display", "none");
place.parent().find(".versionPrevButton, .versionNextButton, .numOfVersions").show(200);
place.parent().find(".numOfVersions").text("1/" + (allResults.length));
place.hide(200);
}
// Original search function as fallback
async function processSearch(title) {
try {
// Use fetch API instead of jQuery's $.get to avoid XHR interception issues
const response = await fetch(BuildUrl(title));
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const text = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'text/html');
const foundElements = doc.querySelectorAll(".container > .gallery");
if (!foundElements || foundElements.length <= 0) {
alert("Error reading data");
return;
}
// Convert NodeList to jQuery collection for easier manipulation
const found = $(foundElements);
place.parent().find(".cover").remove();
try {
for (let i = 0; i < found.length; i++) {
const $item = $(found[i]);
if (partially_fade_all_non_english) {
$item.find(".cover > img, .cover > .caption").css("opacity", non_english_fade_opacity);
}
const dataTags = $item.attr("data-tags") || "";
if (dataTags.includes("12227")) {
$item.find(".caption").append(`<img class="overlayFlag" src="` + flagEn + `">`);
$item.find(".cover > img, .cover > .caption").css("opacity", "1");
} else {
if (dataTags.includes("6346")) {
$item.find(".caption").append(`<img class="overlayFlag" src="` + flagJp + `">`);
} else if (dataTags.includes("29963")) {
$item.find(".caption").append(`<img class="overlayFlag" src="` + flagCh + `">`);
}
if (!partially_fade_all_non_english) {
$item.find(".cover > img, .cover > .caption").css("opacity", "1");
}
}
if (mark_as_read_system_enabled) {
let MARArraySelector = MARArray.join("'], .cover[href='");
$item.find(".cover[href='" + MARArraySelector + "']").append("<div class='readTag'>READ</div>");
let readTag = $item.find(".readTag");
if (!!readTag && readTag.length > 0) {
readTag.parent().parent().find(".cover > img, .cover > .caption").css("opacity", marked_as_read_fade_opacity);
}
}
let thumbnailReplacement;
const $img = $item.find(".cover > img");
const dataSrc = $img.attr("data-src");
if (dataSrc) {
thumbnailReplacement = dataSrc
.replace(/\/\/.+?\.nhentai/g, "//i1.nhentai") // Fixed CDN path
.replace("thumb.", "1."); // Generic replacement for all extensions
} else {
thumbnailReplacement = $img.attr("src")
.replace(/\/\/.+?\.nhentai/g, "//i1.nhentai") // Fixed CDN path
.replace("thumb.", "1."); // Generic replacement for all extensions
}
$img.attr("src", thumbnailReplacement);
place.parent().append($item.find(".cover"));
}
} catch (er) {
alert("Error modifying data: " + er);
return;
}
place.parent().find(".cover:not(:first)").css("display", "none");
place.parent().find(".versionPrevButton, .versionNextButton, .numOfVersions").show(200);
place.parent().find(".numOfVersions").text("1/" + (found.length));
place.hide(200);
} catch (e) {
alert("Error getting data: " + e);
}
}
}
function CleanupSearchString(title) {
title = title.replace(/\[.*?\]/g, "");
title = title.replace(/\【.*?\】/g, "");
if (version_grouping_filter_brackets) title = title.replace(/\(.*?\)/g, "");
return title.trim();
}
function BuildUrl(title) {
let url = CleanupSearchString(title);
url = url.trim();
url = url.replace(/(^|\s){1}[^\w\s\d]{1}(\s|$){1}/g, " "); // remove all instances of a lone symbol character
url = url.replace(/\s+/g, '" "'); // wrap all terms with ""
url = '"' + url + '"';
url = encodeURIComponent(url);
url = "https://nhentai.net/search/?q=" + url;
return url;
}
async function GroupAltVersionsOnPage() {
// Check if the feature is enabled
const mangagroupingenabled = await GM.getValue('mangagroupingenabled', true);
if (!mangagroupingenabled) return;
let i = 0;
let found = $(".container > .gallery");
while (!!found && i < found.length) {
AddAltVersionsToThisFromPage(found[i]);
i++;
found = $(".container > .gallery");
}
}
function AddAltVersionsToThisFromPage(target) {
let place = $(target);
place.addClass("ignoreThis");
let title = place.find(".cover > .caption").text();
if (!title || title.length <= 0) return;
let found = $(".container > .gallery:not(.ignoreThis)");
let numOfValid = 0;
for (let i = 0; i < found.length; i++) {
let cap = $(found[i]).find(".caption");
if (cap.length == 1) {
if (IncludesAll(cap.text(), title)) {
if (partially_fade_all_non_english) {
$(found[i]).find(".cover > img, .cover > .caption").css("opacity", non_english_fade_opacity);
}
if ($(found[i]).attr("data-tags").includes("12227")) {
$(found[i]).find(".caption").append(`<img class="overlayFlag" src="` + flagEn + `">`);
$(found[i]).find(".cover > img, .cover > .caption").css("opacity", "1");
} else {
if ($(found[i]).attr("data-tags").includes("6346")) {
$(found[i]).find(".caption").append(`<img class="overlayFlag" src="` + flagJp + `">`);
} else if ($(found[i]).attr("data-tags").includes("29963")) {
$(found[i]).find(".caption").append(`<img class="overlayFlag" src="` + flagCh + `">`);
}
if (!partially_fade_all_non_english) {
$(found[i]).find(".cover > img, .cover > .caption").css("opacity", "1");
}
}
if (mark_as_read_system_enabled) {
let MARArraySelector = MARArray.join("'], .cover[href='");
$(found[i]).find(".cover[href='" + MARArraySelector + "']").append("<div class='readTag'>READ</div>");
let readTag = $(found[i]).find(".readTag");
if (!!readTag && readTag.length > 0) {
readTag.parent().parent().find(".cover > img, .cover > .caption").css("opacity", marked_as_read_fade_opacity);
}
}
place.append($(found[i]).find(".cover"));
$(found[i]).addClass("deleteThis");
numOfValid++;
}
} else {
let addThese = false;
for (let j = 0; j < cap.length; j++) {
if (IncludesAll($(cap[j]).text(), title)) {
addThese = true;
break;
}
}
if (addThese) {
for (let j = 0; j < cap.length; j++) {
place.append($(cap[j]).parent());
}
$(found[i]).addClass("deleteThis");
numOfValid += cap.length;
}
}
}
numOfValid++;
place.removeClass("deleteThis");
place.removeClass("ignoreThis");
$(".deleteThis").remove();
if (numOfValid > 1) {
place.find(".cover:not(:first)").css("display", "none");
place.find(".versionPrevButton, .versionNextButton, .numOfVersions").show(200);
place.find(".numOfVersions").text("1/" + numOfValid);
}
}
if ($(".container.index-container, #favcontainer.container, #recent-favorites-container, #related-container").length !== 0) {
$(".cover").parent().append("<div class='findVersionButton'>Find Alt Versions</div>");
$(".cover").parent().append("<div class='numOfVersions'>1/1</div>");
$(".cover").parent().append("<div class='versionNextButton'>►</div>");
$(".cover").parent().append("<div class='versionPrevButton'>◄</div>");
$(".findVersionButton").click(function(e) {
e.preventDefault();
AddAltVersionsToThis($(this));
updateMarkAsReadButtonPosition($(this).closest('.gallery')); // Pass the gallery context
});
//------------- UPD CSS BASED ON SETTINGS RQ -----------------------------
// Function to update the position of the mark-as-read button
async function updateMarkAsReadButtonPosition(galleryContext) {
const markAsReadButton = galleryContext.find('.mark-as-read-btn');
const showPageNumbersEnabled = await GM.getValue('showPageNumbersEnabled', true);
console.log(showPageNumbersEnabled);
if (showPageNumbersEnabled || $('.findVersionButton').length > 0) { // Check if find alt version button exists
markAsReadButton.css('top', '40px');
} else {
markAsReadButton.css('top', '5px');
}
}
// Function to adjust the position of a specific element
setInterval(function() {
const pageNumberDisplay = $('.page-number-display');
if (pageNumberDisplay.length > 0) {
$('.mark-as-read-btn').css('top', '40px');
}
}, 1000);
// Function to adjust the position of tag warning badges
setInterval(function() {
if ($('.findVersionButton').length > 0) {
$('.tag-warning-badge').css({
'bottom': '40px',
'left': '10px'
});
}
}, 100);
//------------- UPD CSS BASED ON SETTINGS RQ -----------------------------
if (auto_group_on_page_comics) GroupAltVersionsOnPage();
$(".versionPrevButton").click(function(e) {
e.preventDefault();
let toHide = $(this).parent().find(".cover").filter(":visible");
let toShow = toHide.prev();
if (!toShow || toShow.length <= 0) return;
if (!toShow.is(".cover")) toShow = toHide.prevUntil(".cover", ":last").prev();
if (!toShow || toShow.length <= 0) return;
toHide.hide(100);
toShow.show(100);
let n = $(this).parent().find(".numOfVersions");
n.text((Number(n.text().split("/")[0]) - 1) + "/" + n.text().split("/")[1]);
});
$(".versionNextButton").click(function(e) {
e.preventDefault();
let toHide = $(this).parent().find(".cover").filter(":visible");
let toShow = toHide.next();
if (!toShow || toShow.length <= 0) return;
if (!toShow.is(".cover")) toShow = toHide.nextUntil(".cover", ":last").next();
if (!toShow || toShow.length <= 0) return;
toHide.hide(100);
toShow.show(100);
let n = $(this).parent().find(".numOfVersions");
n.text((Number(n.text().split("/")[0]) + 1) + "/" + n.text().split("/")[1]);
});
}
});
})(); // Self-invoking function for the toggle check
//------------------------ **Find Alternative Manga Button(Thumbnail Version)** ------------------
// ------------------------ *Bookmarks** ------------------
function injectCSS() {
const css = `
/* Bookmark animation */
@keyframes bookmark-animation {
0% {
transform: scale(1) rotate(0deg);
}
50% {
transform: scale(1.2) rotate(20deg);
}
100% {
transform: scale(1) rotate(0deg);
}
}
/* Add a class for the animation */
.bookmark-animating {
animation: bookmark-animation 0.4s ease-in-out;
}
`;
const style = document.createElement('style');
style.type = 'text/css';
style.appendChild(document.createTextNode(css));
document.head.appendChild(style);
}
injectCSS(); // Inject the CSS when the userscript runs
// Function to create and insert bookmark button
async function createBookmarkButton() {
// Check if the feature is enabled in settings
const bookmarksEnabled = await GM.getValue('bookmarksEnabled', true);
if (!bookmarksEnabled) {
return;
}
// Check if the page is already bookmarked
const bookmarkedPages = await GM.getValue('bookmarkedPages', []);
const currentPage = window.location.href;
const isBookmarked = bookmarkedPages.includes(currentPage);
// Bookmark button HTML using Font Awesome 5.13.0
const bookmarkButtonHtml = `
<a class="btn btn-primary bookmark-btn" style="margin-left: 10px;">
<i class="bookmark-icon ${isBookmarked ? 'fas' : 'far'} fa-bookmark"></i>
</a>
`;
const bookmarkButton = $(bookmarkButtonHtml);
// Append the bookmark button as a child of the h1 element if it exists
const h1Element = document.querySelector("#content > h1");
if (h1Element) {
h1Element.append(bookmarkButton[0]);
}
// Handle click event for the bookmark button
bookmarkButton.click(async function() {
const bookmarkIcon = $(this).find('i.bookmark-icon');
const bookmarkedPages = await GM.getValue('bookmarkedPages', []);
const currentPage = window.location.href;
const isBookmarked = bookmarkedPages.includes(currentPage);
// Add animation class
bookmarkIcon.addClass('bookmark-animating');
if (isBookmarked) {
// Remove the bookmark
const updatedBookmarkedPages = bookmarkedPages.filter(page => page !== currentPage);
await GM.setValue('bookmarkedPages', updatedBookmarkedPages);
await GM.deleteValue(currentPage);
// Get the list of manga IDs for this bookmark
const bookmarkMangaIds = await GM.getValue(`bookmark_manga_ids_${currentPage}`, []);
// Delete the bookmark's manga ID list
await GM.deleteValue(`bookmark_manga_ids_${currentPage}`);
// For each manga associated with this bookmark
const allKeys = await GM.listValues();
const mangaKeys = allKeys.filter(key => key.startsWith('manga_'));
for (const key of mangaKeys) {
const mangaInfo = await GM.getValue(key);
// If this manga is associated with the deleted bookmark
if (mangaInfo && mangaInfo.bookmarks && mangaInfo.bookmarks.includes(currentPage)) {
// Remove this bookmark from the manga's bookmarks list
mangaInfo.bookmarks = mangaInfo.bookmarks.filter(b => b !== currentPage);
// If this manga is no longer in any bookmarks, delete it entirely
if (mangaInfo.bookmarks.length === 0) {
await GM.deleteValue(key);
console.log(`Deleted orphaned manga: ${key}`);
} else {
// Otherwise, update the manga info with the bookmark removed
await GM.setValue(key, mangaInfo);
console.log(`Updated manga ${key}: removed bookmark reference`);
}
}
}
// Switch icon class to 'far' when unbookmarking
bookmarkIcon.addClass('far').removeClass('fas');
} else {
// Add the bookmark
bookmarkedPages.push(currentPage);
await GM.setValue('bookmarkedPages', bookmarkedPages);
// Switch icon class to 'fas' when bookmarking
bookmarkIcon.addClass('fas').removeClass('far');
}
// Remove animation class after animation ends
setTimeout(() => {
bookmarkIcon.removeClass('bookmark-animating');
}, 400); // Match the duration of the CSS animation (0.4s)
});
}
// Only execute if not on the settings page or favorites page
if (window.location.href.indexOf('nhentai.net/settings') === -1 && window.location.href.indexOf('nhentai.net/favorites') === -1) {
createBookmarkButton();
}
async function addBookmarkButton() {
const bookmarksPageEnabled = await GM.getValue('bookmarksPageEnabled', true);
if (!bookmarksPageEnabled) return;
// Create the bookmark button
const bookmarkButtonHtml = `
<li>
<a href="/bookmarks/">
<i class="fa fa-bookmark"></i>
Bookmarks
</a>
</li>
`;
const bookmarkButton = $(bookmarkButtonHtml);
// Append the bookmark button to the dropdown menu
const dropdownMenu = $('ul.dropdown-menu');
dropdownMenu.append(bookmarkButton);
// Append the bookmark button to the menu
const menu = $('ul.menu.left');
menu.append(bookmarkButton);
// Call updateMenuOrder to ensure proper tab order
setTimeout(updateMenuOrder, 100);
}
async function addOfflineFavoritesButton() {
const offlineFavoritingEnabled = await GM.getValue('offlineFavoritingEnabled', true);
const offlineFavoritesPageEnabled = await GM.getValue('offlineFavoritesPageEnabled', true);
const isLoggedIn = !document.querySelector('.menu-sign-in');
if (offlineFavoritingEnabled && offlineFavoritesPageEnabled && !isLoggedIn) {
// Create the offline favorites button
const offlineFavoritesButtonHtml = `
<li>
<a href="/favorite/">
<i class="fa fa-heart"></i>
Offline Favorites
</a>
</li>
`;
const offlineFavoritesButton = $(offlineFavoritesButtonHtml);
// Append to dropdown menu
const dropdownMenu = $('ul.dropdown-menu');
dropdownMenu.append(offlineFavoritesButton);
// Append to main menu
const menu = $('ul.menu.left');
menu.append(offlineFavoritesButton);
// Call updateMenuOrder to ensure proper tab order
setTimeout(updateMenuOrder, 100);
}
}
addBookmarkButton(); // Call the function to add the bookmark button
addOfflineFavoritesButton(); // Call the function to add the offline favorites button
// Add Read Manga button function
async function addReadMangaButton() {
const markAsReadEnabled = await GM.getValue('markAsReadEnabled', true);
const readMangaPageEnabled = await GM.getValue('readMangaPageEnabled', true);
if (markAsReadEnabled && readMangaPageEnabled) {
// Check if link already exists
if (document.querySelector('a[href="/read-manga/"]')) return;
// Create the read manga button
const readMangaButtonHtml = `
<li>
<a href="/read-manga/">
<i class="fas fa-book-open"></i> Read Manga
</a>
</li>
`;
const readMangaButton = $(readMangaButtonHtml);
// Append to dropdown menu
const dropdownMenu = $('ul.dropdown-menu');
dropdownMenu.append(readMangaButton);
// Append to main menu
const menu = $('ul.menu.left');
menu.append(readMangaButton);
// Add click handler for navigation
readMangaButton.find('a').on('click', (e) => {
e.preventDefault();
if (window.readMangaPageSystem) {
window.readMangaPageSystem.navigateToReadMangaPage();
}
});
// Call updateMenuOrder to ensure proper tab order
setTimeout(updateMenuOrder, 100);
}
}
addReadMangaButton(); // Call the function to add the read manga button
// Delete error message on unsupported bookmarks page
(async function() {
if (window.location.href.includes('/bookmarks')) {
// Remove not found heading
const notFoundHeading = document.querySelector('h1');
if (notFoundHeading?.textContent === '404 – Not Found') {
notFoundHeading.remove();
}
// Remove not found message
const notFoundMessage = document.querySelector('p');
if (notFoundMessage?.textContent === "Looks like what you're looking for isn't here.") {
notFoundMessage.remove();
}
// Function to fetch the title of a webpage with caching and retries
async function fetchTitleWithCacheAndRetry(url, retries = 3) {
// Check if we have cached manga IDs for this bookmark
const mangaIds = await GM.getValue(`bookmark_manga_ids_${url}`, []);
// If we have cached manga data, use it to construct the title
if (mangaIds.length > 0) {
// For bookmarks with multiple manga, we'll show a count
if (mangaIds.length > 1) {
let itemCount = mangaIds.length;
let itemSuffix = itemCount > 25 ? `+` : ``;
return `${url} (${itemCount}${itemSuffix} items)`;
}
// For a single manga, fetch its details
else {
const mangaId = mangaIds[0];
const mangaInfo = await GM.getValue(`manga_${mangaId}`);
if (mangaInfo && mangaInfo.title) {
return mangaInfo.title;
}
}
}
// If no cached data found, fetch the title directly
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url);
if (response.status === 429) {
// If we get a 429, wait for a bit before retrying
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
continue;
}
const text = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'text/html');
let title = doc.querySelector('title').innerText;
// Remove "» nhentai: hentai doujinshi and manga" from the title
const unwantedPart = "» nhentai: hentai doujinshi and manga";
if (title.includes(unwantedPart)) {
title = title.replace(unwantedPart, '').trim();
}
// We no longer cache the title directly with the URL as the key
// Instead, we'll create proper relationships when manga data is saved
return title;
} catch (error) {
console.error(`Error fetching title for: ${url}. Attempt ${i + 1} of ${retries}`, error);
if (i === retries - 1) {
return url; // Fallback to URL if all retries fail
}
}
}
}
// Function to display bookmarked pages with active loading for unfetched bookmarks
async function displayBookmarkedPages() {
let bookmarkedPages = await GM.getValue('bookmarkedPages', []);
let bookmarkedMangas = await GM.getValue('bookmarkedMangas', []);
const bookmarkArrangementType = await GM.getValue('bookmarkArrangementType', 'default');
if (Array.isArray(bookmarkedPages) && Array.isArray(bookmarkedMangas)) {
// Sort bookmarked mangas based on arrangement type
if (bookmarkArrangementType === 'alphabetical') {
bookmarkedMangas.sort((a, b) => {
const titleA = a.title ? a.title.toLowerCase() : '';
const titleB = b.title ? b.title.toLowerCase() : '';
return titleA.localeCompare(titleB);
});
} else if (bookmarkArrangementType === 'reverse-alphabetical') {
bookmarkedMangas.sort((a, b) => {
const titleA = a.title ? a.title.toLowerCase() : '';
const titleB = b.title ? b.title.toLowerCase() : '';
return titleB.localeCompare(titleA);
});
}
// Get manga IDs for each bookmark page to sort by item count
const bookmarkItemCounts = {};
if (bookmarkArrangementType === 'most-items' || bookmarkArrangementType === 'least-items') {
// We'll need to fetch the manga counts for each bookmark
for (const page of bookmarkedPages) {
const mangaIds = await GM.getValue(`bookmark_manga_ids_${page}`, []);
bookmarkItemCounts[page] = mangaIds.length;
}
// Sort bookmarked pages based on item count
if (bookmarkArrangementType === 'most-items') {
bookmarkedPages.sort((a, b) => bookmarkItemCounts[b] - bookmarkItemCounts[a]);
} else if (bookmarkArrangementType === 'least-items') {
bookmarkedPages.sort((a, b) => bookmarkItemCounts[a] - bookmarkItemCounts[b]);
}
}
// Note: For default arrangement, we keep the original order (most recent first)
const bookmarksContainer = $('<div id="bookmarksContainer" class="container">');
const bookmarksTitle = $('<h2 class="bookmarks-title">Bookmarked Pages</h2>');
const bookmarksList = $('<ul class="bookmarks-list">');
const searchInput = $('<input type="text" id="searchBookmarks" placeholder="Search bookmarks..." class="search-input">');
const mangaBookmarksTitle = $('<h2 class="bookmarks-title">Bookmarked Mangas</h2>');
const mangaBookmarksList = $('<ul class="bookmarks-grid">');
const tagSearchInput = $('<input type="text" id="searchMangaTags" placeholder="Search manga tags..." class="search-input">');
// Get the bookmarks page order from storage or use default order
const defaultOrder = ['bookmarksTitle', 'searchInput', 'tagSearchInput', 'bookmarksList', 'mangaBookmarksTitle', 'mangaBookmarksList'];
const bookmarksOrder = await GM.getValue('bookmarksContainerOrder', defaultOrder);
// Create a map of element names to their actual elements
const elementsMap = {
'bookmarksTitle': bookmarksTitle,
'searchInput': searchInput,
'tagSearchInput': tagSearchInput,
'bookmarksList': bookmarksList,
'mangaBookmarksTitle': mangaBookmarksTitle,
'mangaBookmarksList': mangaBookmarksList
};
// Append elements in the order specified by bookmarksOrder
bookmarksOrder.forEach(elementName => {
if (elementsMap[elementName]) {
bookmarksContainer.append(elementsMap[elementName]);
}
});
$('body').append(bookmarksContainer);
// Add CSS styles
const styles = `
#bookmarksContainer {
margin: 20px auto;
padding: 20px;
background-color: #2c2c2c;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
width: 80%;
max-width: 600px;
}
.bookmarks-title {
font-size: 24px;
margin-bottom: 10px;
color: #e63946;
}
.search-input {
width: calc(100% - 20px);
padding: 10px;
margin-bottom: 20px;
border-radius: 5px;
border: 1px solid #ccc;
font-size: 16px;
}
.bookmarks-list {
list-style: none;
padding: 0;
max-height: 100%;
overflow-y: hidden;
}
.bookmark-link {
display: block;
padding: 10px;
font-size: 18px;
color: #f1faee;
text-decoration: none;
transition: background-color 0.3s, color 0.3s;
}
.bookmark-link:hover {
background-color: #e63946;
color: #1d3557;
}
.delete-button:hover {
color: #f1faee;
}
.delete-button-pages {
position: relative;
top: -32px;
float: right;
background: none;
border: none;
color: #e63946;
cursor: pointer;
font-size: 14px;
}
.delete-button-pages:hover {
color: #f1faee;
}
.undo-popup {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
padding: 15px;
background-color: #333;
color: #fff;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
gap: 10px;
z-index: 1000;
}
.undo-button {
background-color: #f1faee;
color: #333;
border: none;
padding: 5px 10px;
border-radius: 3px;
cursor: pointer;
}
.undo-button:hover {
background-color: #e63946;
color: #1d3557;
}
@media only screen and (max-width: 600px) {
#bookmarksContainer {
width: 90%;
margin: 10px auto;
}
.bookmarks-title {
font-size: 20px;
}
.bookmark-link {
font-size: 16px;
}
}
`;
const styleSheet = document.createElement("style");
styleSheet.type = "text/css";
styleSheet.innerText = styles;
document.head.appendChild(styleSheet);
// Fetch titles for each bookmark and update dynamically
for (const page of bookmarkedPages) {
// Append a loading list item first
const listItem = $(`<li><a href="${page}" class="bookmark-link">Loading...</a><button class="delete-button-pages">✖</button></li>`);
bookmarksList.append(listItem);
// Using async IIFE to handle async operations in the loop
(async () => {
try {
// Get manga IDs associated with this bookmark
const mangaIds = await GM.getValue(`bookmark_manga_ids_${page}`, []);
// Determine what to display based on manga IDs
let displayText;
if (mangaIds.length > 0) {
// For single or multiple manga
const urlObj = new URL(page);
const pathName = urlObj.pathname;
const searchParams = urlObj.searchParams.get('q');
let itemCount = mangaIds.length;
let itemSuffix = itemCount == 1 ? ' item' : ` items`;
let itemPlusSuffix = itemCount == 25 ? `+` : ``;
if (pathName.includes('/tag/')) {
// For tag pages, extract the tag name
const tagName = pathName.split('/tag/')[1].replace('/', '');
displayText = `Tag: ${tagName} (${itemCount}${itemPlusSuffix}${itemSuffix})`;
} else if (pathName.includes('/artist/')) {
// For artist pages, extract the artist name
const artistName = pathName.split('/artist/')[1].replace('/', '');
displayText = `Artist: ${artistName} (${itemCount}${itemPlusSuffix}${itemSuffix})`;
} else if (pathName.includes('/character/')) {
// For character pages, extract the character name
const characterName = pathName.split('/character/')[1].replace('/', '');
displayText = `Character: ${characterName} (${itemCount}${itemPlusSuffix}${itemSuffix})`;
} else if (pathName.includes('/parody/')) {
// For parody pages, extract the parody name
const parodyName = pathName.split('/parody/')[1].replace('/', '');
displayText = `Parody: ${parodyName} (${itemCount}${itemPlusSuffix}${itemSuffix})`;
} else if (pathName.includes('/group/')) {
// For group pages, extract the group name
const groupName = pathName.split('/group/')[1].replace('/', '');
displayText = `Group: ${groupName} (${itemCount}${itemPlusSuffix}${itemSuffix})`;
} else if (searchParams) {
// For search results
displayText = `Search: ${searchParams} (${itemCount}${itemPlusSuffix}${itemSuffix})`;
} else {
// Default display for other pages with manga
displayText = `${page} (${itemCount}${itemPlusSuffix}${itemSuffix})`;
}
} else {
// If no manga IDs found, fetch title directly
displayText = await fetchTitleWithCacheAndRetry(page);
}
// Update the list item with the fetched title/display text
const updatedListItem = $(`<li><a href="${page}" class="bookmark-link">${displayText}</a><button class="delete-button-pages">✖</button></li>`);
listItem.replaceWith(updatedListItem);
// Add delete functionality
updatedListItem.find('.delete-button-pages').click(async function() {
const updatedBookmarkedPages = bookmarkedPages.filter(p => p !== page);
await GM.setValue('bookmarkedPages', updatedBookmarkedPages);
// Get the list of manga IDs for this bookmark
const bookmarkMangaIds = await GM.getValue(`bookmark_manga_ids_${page}`, []);
// Delete the bookmark's manga ID list
await GM.deleteValue(`bookmark_manga_ids_${page}`);
// For each manga associated with this bookmark
const allKeys = await GM.listValues();
const mangaKeys = allKeys.filter(key => key.startsWith('manga_'));
for (const key of mangaKeys) {
const mangaInfo = await GM.getValue(key);
// If this manga is associated with the deleted bookmark
if (mangaInfo && mangaInfo.bookmarks && mangaInfo.bookmarks.includes(page)) {
// Remove this bookmark from the manga's bookmarks list
mangaInfo.bookmarks = mangaInfo.bookmarks.filter(b => b !== page);
// If this manga is no longer in any bookmarks, delete it entirely
if (mangaInfo.bookmarks.length === 0) {
await GM.deleteValue(key);
console.log(`Deleted orphaned manga: ${key}`);
} else {
// Otherwise, update the manga info with the bookmark removed
await GM.setValue(key, mangaInfo);
console.log(`Updated manga ${key}: removed bookmark reference`);
}
}
}
updatedListItem.remove();
console.log(`Deleted bookmark: ${page} and cleaned up related manga data`);
const undoPopup = $(`
<div class="undo-popup">
<span>Bookmark deleted.</span>
<button class="undo-button">Undo</button>
</div>
`);
$('body').append(undoPopup);
const timeout = setTimeout(() => {
undoPopup.remove();
}, 5000);
undoPopup.find('.undo-button').click(async function() {
clearTimeout(timeout);
const restoredBookmarkedPages = [...updatedBookmarkedPages, page];
await GM.setValue('bookmarkedPages', restoredBookmarkedPages);
undoPopup.remove();
$('#bookmarksContainer').remove();
displayBookmarkedPages();
});
});
} catch (error) {
console.error(`Error processing bookmark: ${page}`, error);
listItem.html(`<a href="${page}" class="bookmark-link">Failed to load</a><button class="delete-button-pages">✖</button>`);
}
})();
}
// Modified version with better cover organization
for (const manga of bookmarkedMangas) {
const listItem = $(`<li class="bookmark-item"><a href="${manga.url}" class="bookmark-link">Loading...</a><button class="delete-button">✖</button></li>`);
mangaBookmarksList.append(listItem);
(async () => { // Immediately invoked async function
const mangaBookMarkingType = await GM.getValue('mangaBookMarkingType', 'cover');
let title = manga.title;
let coverImage = manga.coverImageUrl;
if (!title || !coverImage) {
try {
const info = await fetchMangaInfoWithCacheAndRetry(manga.url);
title = info.title;
} catch (error) {
console.error(`Error fetching info for: ${manga.url}`, error);
listItem.html(`<span class="error-text">Failed to fetch data</span>`);
return; // Stop processing this item if fetching fails
}
}
// Fetch and store tags
let tags = await GM.getValue(`tags_${manga.url}`, null);
if (!tags) {
try {
const response = await fetch(manga.url);
const html = await response.text();
const doc = new DOMParser().parseFromString(html, 'text/html');
tags = Array.from(doc.querySelectorAll('#tags .tag')).map(tag => {
// Remove popularity numbers and format the tag
return tag.textContent.replace(/\d+K?$/, '').trim().replace(/\b\w/g, char => char.toUpperCase());
});
console.log(`Fetched tags for ${manga.url}:`, tags); // Log the fetched tags
await GM.setValue(`tags_${manga.url}`, tags); // Save tags for future use
} catch (error) {
console.error(`Error fetching tags for: ${manga.url}`, error);
tags = []; // Default to empty if fetch fails
}
} else {
console.log(`Retrieved cached tags for ${manga.url}:`, tags); // Log cached tags
}
let content = "";
if (mangaBookMarkingType === 'cover') {
content = `
<div class="cover-container">
<img src="${coverImage}" alt="${title}" class="cover-image">
<div class="title-overlay">${title}</div>
</div>`;
} else if (mangaBookMarkingType === 'title') {
content = `<span class="title-only">${title}</span>`;
} else if (mangaBookMarkingType === 'both') {
content = `
<div class="cover-with-title">
<img src="${coverImage}" alt="${title}" class="cover-image-small">
<span class="title-text">${title}</span>
</div>`;
}
const updatedListItem = $(`<li class="bookmark-item ${mangaBookMarkingType}-mode"><a href="${manga.url}" class="bookmark-link">${content}</a><button class="delete-button">✖</button></li>`);
listItem.replaceWith(updatedListItem);
// Add title attribute with tags for hover tooltip
const tooltipText = `${title}\n\nTags: ${tags.join(', ')}`;
const galleryCaptionTooltipsEnabled = await GM.getValue('galleryCaptionTooltipsEnabled', true);
if (galleryCaptionTooltipsEnabled) {
updatedListItem.find('.bookmark-link').attr('title', tooltipText);
}
// Add delete functionality
updatedListItem.find('.delete-button').click(async function() {
const updatedBookmarkedMangas = bookmarkedMangas.filter(m => m.url !== manga.url);
await GM.setValue('bookmarkedMangas', updatedBookmarkedMangas);
updatedListItem.remove();
const undoPopup = $(`
<div class="undo-popup">
<span>Bookmark deleted.</span>
<button class="undo-button">Undo</button>
</div>
`);
$('body').append(undoPopup);
const timeout = setTimeout(() => {
undoPopup.remove();
}, 5000);
undoPopup.find('.undo-button').click(async function() {
clearTimeout(timeout);
const restoredBookmarkedMangas = [...updatedBookmarkedMangas, manga];
await GM.setValue('bookmarkedMangas', restoredBookmarkedMangas);
undoPopup.remove();
$('#bookmarksContainer').remove();
displayBookmarkedPages();
});
});
})(); // Execute the async function immediately
}
// Add this CSS to your styles
const additionalStyles = `
#mangaBookmarksList {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 15px;
list-style-type: none;
padding: 0;
}
.bookmark-item {
position: relative;
}
.bookmark-item.cover-mode {
text-align: center;
}
.cover-container {
position: relative;
width: 100%;
height: 0;
padding-bottom: 140%; /* Aspect ratio for typical manga covers */
overflow: hidden;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
.cover-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.cover-container:hover .cover-image {
transform: scale(1.05);
}
.title-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0,0,0,0.7);
color: white;
padding: 5px;
font-size: 12px;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.delete-button {
position: absolute;
top: 5px;
right: 5px;
background: rgba(0,0,0,0.5);
color: #ffffff;
border: none;
border-radius: 50%;
width: 20px;
height: 20px;
font-size: 12px;
cursor: pointer;
opacity: 0;
transition: opacity 0.2s ease;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
}
@media only screen and (max-width: 768px) {
.delete-button {
font-size: 10px;
}
}
.bookmark-item:hover .delete-button {
opacity: 1;
}
/* Hide default browser tooltip */
.bookmark-link[title] {
position: relative;
}
/* Gallery caption hover tooltip styling */
.gallery .caption[title].tooltip-enabled:hover::after {
content: attr(title);
position: fixed;
background: rgba(0, 0, 0, 0.9);
color: white;
padding: 8px 12px;
border-radius: 6px;
font-size: 12px;
line-height: 1.4;
white-space: pre-line;
max-width: 300px;
word-wrap: break-word;
z-index: 999999;
pointer-events: none;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.2);
backdrop-filter: blur(4px);
top: 20px;
left: 20px;
}
/* General tooltip styling for any element with title attribute */
[title]:hover::after {
content: attr(title);
position: fixed !important;
background: rgba(0, 0, 0, 0.9) !important;
color: white !important;
padding: 8px 12px !important;
border-radius: 6px !important;
font-size: 12px !important;
line-height: 1.4 !important;
white-space: pre-line !important;
max-width: 300px !important;
word-wrap: break-word !important;
z-index: 999999 !important;
pointer-events: none !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
border: 1px solid rgba(255, 255, 255, 0.2) !important;
backdrop-filter: blur(4px) !important;
top: 20px !important;
left: 20px !important;
}
.title-only {
display: block;
padding: 5px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cover-with-title {
display: flex;
align-items: center;
}
.cover-image-small {
width: 50px;
height: 70px;
object-fit: cover;
margin-right: 10px;
border-radius: 3px;
}
/* Default styles for desktop */
.bookmarks-grid {
list-style: none;
padding: 0;
max-height: 100%;
overflow-y: hidden;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); /* adjust the min and max widths as needed */
gap: 10px; /* adjust the gap between grid items as needed */
}
/* Styles for mobile devices */
@media only screen and (max-width: 768px) {
.bookmarks-grid {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); /* adjust the grid item width for mobile */
gap: 5px; /* adjust the gap between grid items for mobile */
}
}
.title-text {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
/* Modified search to work with new layout */
.bookmark-item.hidden {
display: none;
}
.random-button {
background-color: #e63946;
color: #ffffff;
border: none;
padding: 5px 10px;
font-size: 14px;
cursor: pointer;
border-radius: 5px;
transition: background-color 0.2s ease;
}
.random-button:hover {
background-color:rgb(255, 255, 255);
color: #e63946;
}
.random-button:active {
transform: translateY(2px);
}
.random-button i {
margin-right: 10px;
}
`;
// Add the CSS to the page
$('<style>').text(additionalStyles).appendTo('head');
// Modified search functionality to work with the new layout
searchInput.on('input', filterBookmarks);
tagSearchInput.on('input', filterBookmarks);
function filterBookmarks() {
const searchQuery = searchInput.val().toLowerCase();
const tagQueries = tagSearchInput.val().toLowerCase().trim().split(/,\s*|\s+/);
mangaBookmarksList.children('li').each(async function () {
const $li = $(this);
const mangaUrl = $li.find('.bookmark-link').attr('href');
const tags = await GM.getValue(`tags_${mangaUrl}`, []);
const cleanedTags = tags.map(tag =>
tag.replace(/\d+K?$/, '').trim().toLowerCase()
);
const textContent = $li.find('.bookmark-link').text().toLowerCase();
const imageSrc = $li.find('.bookmark-link img').attr('src') || '';
const searchMatch = textContent.includes(searchQuery) || imageSrc.toLowerCase().includes(searchQuery);
const tagMatch = tagQueries.every(query => {
const queryWords = query.split(/\s+/);
return cleanedTags.some(tag =>
queryWords.every(word => tag.includes(word))
);
});
$li.toggleClass('hidden', !(searchMatch && tagMatch));
});
$('.bookmarks-list li').each(async function () {
const $li = $(this);
const bookmarkUrl = $li.find('.bookmark-link').attr('href');
let matchFound = false;
// Get all manga IDs associated with this bookmark
const mangaIds = await GM.getValue(`bookmark_manga_ids_${bookmarkUrl}`, []);
if (!mangaIds || mangaIds.length === 0) {
// If we don't have any manga IDs for this bookmark, hide it
$li.toggleClass('hidden', true);
return;
}
// Check each manga in this bookmark for matching tags
for (const mangaId of mangaIds) {
const mangaData = await GM.getValue(`manga_${mangaId}`, null);
if (!mangaData || !mangaData.tags) continue;
const cleanedTags = mangaData.tags.map(tag =>
tag.replace(/\d+K?$/, '').trim().toLowerCase()
);
const searchContent = $li.find('.bookmark-link').text().toLowerCase();
const searchImageSrc = $li.find('.bookmark-link img').attr('src') || '';
const searchMatch = searchContent.includes(searchQuery) || searchImageSrc.toLowerCase().includes(searchQuery);
const tagMatch = tagQueries.every(query => {
const queryWords = query.split(/\s+/);
return cleanedTags.some(tag =>
queryWords.every(word => tag.includes(word))
);
});
if (searchMatch && tagMatch) {
matchFound = true;
break;
}
}
$li.toggleClass('hidden', !matchFound);
});
}
} else {
console.error('Bookmarked pages or mangas is not an array');
}
}
// Function to fetch manga info (title and cover image) with cache and retry
async function fetchMangaInfoWithCacheAndRetry(manga) {
const cacheKey = `manga-info-${manga}`;
const cachedInfo = await GM.getValue(cacheKey);
if (cachedInfo) {
return cachedInfo;
}
try {
const response = await fetch(manga);
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const title = doc.querySelector('h1.title').textContent;
const coverImage = doc.querySelector('#cover img').src;
const info = { title, coverImage };
await GM.setValue(cacheKey, info);
return info;
} catch (error) {
console.error(`Error fetching manga info for: ${manga}`, error);
throw error;
}
}
// Call the function to display bookmarked pages with active loading
displayBookmarkedPages();
}
})();
// ------------------------ *Bookmarks** ------------------
//------------------------ **Nhentai English Filter** ----------------------
var pathname = window.location.pathname;
var searchQuery = window.location.search.split('=')[1] || '';
var namespaceType = pathname.split('/')[1];
var namespaceQuery = pathname.split('/')[2];
var namespaceSearchLink = '<div class="sort-type"><a href="https://nhentai.net/search/?q=' + namespaceType + '%3A%22' + namespaceQuery + '%22+language%3A%22english%22">English Only</a></div>';
var siteSearchLink = '<div class="sort-type"><a href="https://nhentai.net/search/?q=' + searchQuery + '+language%3A%22english%22">English Only</a></div>';
var favSearchBtn = '<a class="btn btn-primary" href="https://nhentai.net/favorites/?q=language%3A%22english%22+' + searchQuery + '"><i class="fa fa-flag"></i> ENG</a>';
var favPageBtn = '<a class="btn btn-primary" href="https://nhentai.net/favorites/?q=language%3A%22english%22+"><i class="fa fa-flag"></i> ENG</a>';
(async function() {
const englishFilterEnabled = await GM.getValue('englishFilterEnabled', true);
if (englishFilterEnabled) {
// Check if the search query contains 'English' or 'english'
if (!/(English|language%3A%22english%22)/i.test(searchQuery)) {
if (pathname.startsWith('/parody/')) { // parody pages
document.getElementsByClassName('sort')[0].innerHTML += namespaceSearchLink;
} else if (pathname.startsWith('/favorites/')) { // favorites pages
if (window.location.search.length) {
document.getElementById('favorites-random-button').insertAdjacentHTML('afterend', favSearchBtn);
} else {
document.getElementById('favorites-random-button').insertAdjacentHTML('afterend', favPageBtn);
}
} else if (pathname.startsWith('/artist/')) { // artist pages
document.getElementsByClassName('sort')[0].innerHTML += namespaceSearchLink;
} else if (pathname.startsWith('/tag/')) { // tag pages
document.getElementsByClassName('sort')[0].innerHTML += namespaceSearchLink;
} else if (pathname.startsWith('/group/')) { // group pages
document.getElementsByClassName('sort')[0].innerHTML += namespaceSearchLink;
} else if (pathname.startsWith('/category/')) { // category pages
document.getElementsByClassName('sort')[0].innerHTML += namespaceSearchLink;
} else if (pathname.startsWith('/character/')) { // character pages
document.getElementsByClassName('sort')[0].innerHTML += namespaceSearchLink;
} else if (pathname.startsWith('/search/')) { // search pages
document.getElementsByClassName('sort')[0].innerHTML += siteSearchLink;
}
}
}
})();
//------------------------ **Nhentai English Filter** ----------------------
//------------------------ **Nhentai Auto Login** --------------------------
(async function() {
const autoLoginEnabled = await GM.getValue('autoLoginEnabled', true);
const email = await GM.getValue('email');
const password = await GM.getValue('password');
// Login page
if (autoLoginEnabled && window.location.href.includes('/login/?next=/')) {
if (!email || !password) {
GM.setValue('email', prompt('Please enter your email:'));
GM.setValue('password', prompt('Please enter your password:'));
}
document.getElementById('id_username_or_email').value = email;
document.getElementById('id_password').value = password;
const errorMessage = document.querySelector('#errors');
if (!errorMessage || !errorMessage.textContent.includes('You need to solve the CAPTCHA.')) {
document.querySelector('button[type="submit"]').click();
} else {
console.log('CAPTCHA detected. Cannot auto-login.');
}
}
})();
//------------------------ **Nhentai Auto Login** --------------------------
//----------------------------**Settings**-----------------------------
// Function to add the settings button to the menu
function addSettingsButton() {
// Create the settings button
const settingsButtonHtml = `
<li>
<a href="/settings/">
<i class="fa fa-cog"></i>
Settings
</a>
</li>
`;
const settingsButton = $(settingsButtonHtml);
// Append the settings button to the dropdown menu and the left menu
const dropdownMenu = $('ul.dropdown-menu');
dropdownMenu.append(settingsButton);
const menu = $('ul.menu.left');
menu.append(settingsButton);
}
// Call the function to add the settings button
addSettingsButton();
// Handle settings page
if (window.location.href.includes('/settings')) {
// Remove 404 Not Found elements
const notFoundHeading = document.querySelector('h1');
if (notFoundHeading && notFoundHeading.textContent === '404 – Not Found') {
notFoundHeading.remove();
}
const notFoundMessage = document.querySelector('p');
if (notFoundMessage && notFoundMessage.textContent === "Looks like what you're looking for isn't here.") {
notFoundMessage.remove();
}
// Add settings form and random hentai preferences
const settingsHtml = `
<style>
#content {
padding: 20px;
background: #1a1a1a;
color: #fff;
border-radius: 5px;
}
#settingsForm {
display: flex;
flex-direction: column;
gap: 10px;
}
.tooltip {
display: inline-block;
position: relative;
cursor: pointer;
font-size: 14px;
background: #444;
color: #fff;
border-radius: 50%;
width: 18px;
height: 18px;
text-align: center;
line-height: 18px;
font-weight: bold;
}
.tooltip:hover::after {
content: attr(data-tooltip);
position: absolute;
left: 50%;
bottom: 100%;
transform: translateX(-50%);
background: #666;
color: #fff;
padding: 5px;
border-radius: 3px;
white-space: nowrap;
font-size: 12px;
}
#settingsForm label {
display: flex;
align-items: center;
gap: 10px;
}
#settingsForm input[type="text"],
#settingsForm input[type="password"],
#settingsForm input[type="number"] {
width: calc(100% - 12px); /* Adjust for padding and borders */
padding: 5px;
border-radius: 3px;
border: 1px solid #333;
background: #333;
color: #fff;
}
#settingsForm button {
padding: 10px;
background: #2a2a2a;
border: 1px solid #333;
border-radius: 3px;
color: #fff;
cursor: pointer;
}
#settingsForm button:hover {
background: #333;
}
#autoLoginCredentials {
display: block;
margin-top: 10px;
}
#random-settings {
margin-top: 20px;
}
#random-settings label {
display: flex;
align-items: center;
gap: 10px;
}
#random-settings input[type="text"],
#random-settings input[type="number"] {
width: calc(100% - 12px); /* Adjust for padding and borders */
padding: 5px;
border-radius: 3px;
border: 1px solid #333;
background: #333;
color: #fff;
margin-bottom: 10px; /* Add spacing between fields */
}
/* Bookmark Import/Export Buttons */
.bookmark-actions {
display: flex;
gap: 10px;
margin-top: 10px;
}
.bookmark-actions button {
padding: 10px;
background-color: #007bff;
border: none;
color: white;
cursor: pointer;
}
.bookmark-actions button:hover {
background-color: #0056b3;
}
#importBookmarksFile {
display: none;
}
/* Advanced Settings Section */
#advanced-settings {
margin-top: 30px;
border-top: 1px solid #333;
padding-top: 20px;
}
#advanced-settings h3 {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
}
/* Tab Arrangement Styles */
.sortable-list {
list-style: none;
padding: 0;
margin: 10px 0;
touch-action: pan-y;
}
.tab-item {
display: flex;
align-items: center;
padding: 10px;
margin: 5px 0;
background: #2a2a2a;
border: 1px solid #333;
border-radius: 3px;
user-select: none;
transition: background 0.2s, transform 0.2s;
touch-action: none;
}
.handle {
cursor: grab;
margin-right: 8px;
touch-action: none;
}
.tab-item.sortable-ghost {
opacity: 0.5;
}
.tab-item.sortable-drag,
.tab-item.dragging {
cursor: grabbing !important;
background: #333;
transform: scale(1.02);
z-index: 1000;
}
.tab-item:hover {
background: #333;
}
.tab-item .handle:hover {
opacity: 0.8;
}
}
.tab-item:hover {
background: #333;
}
.tab-item .handle {
margin-right: 10px;
color: #666;
}
.btn-secondary {
background: #444;
color: #fff;
border: none;
padding: 8px 15px;
border-radius: 3px;
cursor: pointer;
margin-top: 10px;
}
.btn-secondary:hover {
background: #555;
}
#advanced-settings-content {
display: none;
margin-top: 15px;
}
/* Fade & Read Settings */
#fade-read-settings {
margin-top: 20px;
border-top: 1px solid #333;
padding-top: 20px;
}
#fade-read-settings-content {
display: none;
margin-top: 15px;
}
#fade-read-settings-content input[type="range"] {
width: 200px;
margin: 0 10px;
}
/* Tag Management Settings */
#tag-management-settings {
margin-top: 20px;
border-top: 1px solid #333;
padding-top: 20px;
}
#tag-management-settings-content {
display: none;
margin-top: 15px;
}
.tag-lists-container {
margin: 15px 0;
}
.tag-list-section {
margin-bottom: 15px;
}
.tag-list-section h4 {
color: #e63946;
margin-bottom: 5px;
font-size: 14px;
}
.tag-list-section textarea {
width: calc(100% - 12px);
padding: 8px;
border-radius: 3px;
border: 1px solid #333;
background: #2a2a2a;
color: #fff;
font-family: inherit;
resize: vertical;
min-height: 60px;
}
.btn-secondary {
padding: 8px 16px;
background: #444;
border: 1px solid #555;
border-radius: 3px;
color: #fff;
cursor: pointer;
margin-top: 10px;
margin-right: 10px;
}
.btn-secondary:hover {
background: #555;
}
#storage-data {
width: 100%;
height: 200px;
background: #333;
color: #fff;
border: 1px solid #444;
padding: 10px;
font-family: monospace;
margin-bottom: 10px;
white-space: pre;
overflow: auto;
}
.storage-key-item {
display: flex;
align-items: center;
margin-bottom: 5px;
background: #2a2a2a;
padding: 5px;
border-radius: 3px;
}
.storage-key {
flex: 1;
padding: 5px;
overflow: hidden;
text-overflow: ellipsis;
}
.storage-actions {
display: flex;
gap: 5px;
}
.storage-actions button {
background: #444;
border: none;
color: white;
padding: 3px 8px;
border-radius: 2px;
cursor: pointer;
}
.storage-actions button:hover {
background: #555;
}
.action-btn-danger {
background: #d9534f !important;
}
.action-btn-danger:hover {
background: #c9302c !important;
}
#edit-value-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
z-index: 999;
}
#edit-value-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #222;
padding: 20px;
border-radius: 5px;
width: 80%;
max-width: 600px;
}
#edit-value-textarea {
width: 100%;
height: 200px;
background: #333;
color: #fff;
border: 1px solid #444;
padding: 10px;
font-family: monospace;
margin-bottom: 15px;
}
.modal-buttons {
display: flex;
gap: 10px;
justify-content: flex-end;
}
/* Page Management Section */
#page-management {
margin-top: 20px;
border-top: 1px solid #333;
border-bottom: 1px solid #333;
padding-top: 20px;
padding-bottom: 30px;
}
#page-management h3 {
display: flex;
align-items: center;
gap: 10px;
}
.section-header {
font-weight: bold;
margin: 10px 0 5px 0;
color: #ccc;
}
.expand-icon::after {
content: "❯"; /* Chevron Right */
margin-left: 5px;
font-size: 14px;
display: inline-block;
transition: transform 0.2s ease;
}
.expand-icon.expanded::after {
content: "❯"; /* Keep the same content */
transform: rotate(90deg); /* Rotate to mimic Chevron Down */
font-size: 14px;
}
/* Style for the Show Non-English dropdown to match NHentai theme */
#showNonEnglishSelect {
/* Basic styling */
padding: 6px 10px;
margin: 0 5px;
min-width: 110px;
/* Colors */
background-color: #2b2b2b;
color: #e6e6e6;
border: 1px solid #3d3d3d;
/* Typography */
font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
font-size: 14px;
font-weight: 400;
/* Effects */
border-radius: 4px;
cursor: pointer;
transition: all 0.15s ease;
}
/* Hover state */
#showNonEnglishSelect:hover {
border-color: #4e4e4e;
background-color: #323232;
}
/* Focus state */
#showNonEnglishSelect:focus {
outline: none;
border-color: #616161;
box-shadow: 0 0 0 2px rgba(82, 82, 82, 0.35);
}
/* Dropdown options */
#showNonEnglishSelect option {
padding: 8px 12px;
background-color: #2b2b2b;
color: #e6e6e6;
}
/* Tooltip integration */
label:hover .tooltip {
opacity: 1;
visibility: visible;
}
/* Online Data Sync Styles */
.sync-section {
margin: 15px 0;
padding: 10px;
border: 1px solid #444;
border-radius: 5px;
background: #2a2a2a;
}
.sync-section h4 {
margin: 0 0 10px 0;
color: #fff;
font-size: 14px;
}
.sync-controls {
margin: 10px 0;
display: flex;
gap: 10px;
align-items: center;
}
.sync-controls button {
padding: 5px 10px;
background: #444;
border: 1px solid #666;
border-radius: 3px;
color: #fff;
cursor: pointer;
font-size: 12px;
}
.sync-controls button:hover {
background: #555;
}
.sync-controls button:disabled {
background: #333;
color: #666;
cursor: not-allowed;
}
.sync-status {
font-size: 12px;
padding: 2px 6px;
border-radius: 3px;
margin-left: 10px;
}
.sync-status.success {
background: #2d5a2d;
color: #90ee90;
}
.sync-status.error {
background: #5a2d2d;
color: #ff6b6b;
}
.sync-status.loading {
background: #5a5a2d;
color: #ffeb3b;
}
.sync-info {
margin-top: 5px;
font-size: 11px;
color: #aaa;
}
#userUUID {
font-family: monospace;
letter-spacing: 2px;
text-align: center;
width: 80px !important;
}
#edit-uuid, #regenerate-uuid, #browse-users {
margin-left: 10px;
padding: 2px 8px;
font-size: 11px;
}
#edit-uuid {
background: #444;
border: 1px solid #666;
}
#edit-uuid:hover {
background: #555;
}
#uuid-edit-warning {
background: rgba(255, 107, 107, 0.1);
border: 1px solid #ff6b6b;
border-radius: 3px;
padding: 8px;
}
</style>
<div id="content">
<h1>Settings</h1>
<form id="settingsForm">
<label>
<input type="checkbox" id="mustAddTagsEnabled">
Enable Must Add Tags <span class="tooltip" data-tooltip="Enable or disable the 'Must Add Tags' feature.">?</span>
</label>
<label>Must Add Tags: <input type="text" id="must-add-tags"> <span class="tooltip" data-tooltip="Tags that must be included in search. Separate with commas.">?</span></label>
<label>
Show Non-English:
<select id="showNonEnglishSelect">
<option value="show">Show</option>
<option value="hide">Hide</option>
<option value="fade">Fade</option>
</select>
<span class="tooltip" data-tooltip="Control the visibility of non-English manga.">?</span>
</label>
<label>
<input type="checkbox" id="showPageNumbersEnabled">
Show Page Numbers <span class="tooltip" data-tooltip="Displays the page count for each manga thumbnail.">?</span>
</label>
<!-- Fade & Read Settings Section -->
<div id="fade-read-settings">
<h3 class="expand-icon">Fade & Read Settings <span class="tooltip" data-tooltip="Configure opacity settings and mark as read functionality">?</span></h3>
<div id="fade-read-settings-content">
<label>
<input type="checkbox" id="markAsReadEnabled">
Enable Mark as Read System <span class="tooltip" data-tooltip="Allows marking galleries as read with visual feedback">?</span>
</label>
<label>
<input type="checkbox" id="autoMarkReadEnabled">
Auto-mark as Read on Last Page <span class="tooltip" data-tooltip="Automatically marks galleries as read when reaching the last page">?</span>
</label>
<div>
<label for="nonEnglishOpacity">Non-English Galleries Opacity:</label>
<input type="range" id="nonEnglishOpacity" min="0.1" max="1.0" step="0.1" value="0.2">
<span id="nonEnglishOpacityValue">0.2</span>
<span class="tooltip" data-tooltip="Opacity level for non-English galleries (0.1 = very faded, 1.0 = normal)">?</span>
</div>
<div>
<label for="readGalleriesOpacity">Read Galleries Opacity:</label>
<input type="range" id="readGalleriesOpacity" min="0.1" max="1.0" step="0.1" value="0.6">
<span id="readGalleriesOpacityValue">0.6</span>
<span class="tooltip" data-tooltip="Opacity level for galleries marked as read (0.1 = very faded, 1.0 = normal)">?</span>
</div>
<button type="button" id="resetFadeSettings" class="btn-secondary">Reset to Defaults</button>
</div>
</div>
<!-- Tag Management Settings Section -->
<div id="tag-management-settings">
<h3 class="expand-icon">Tag Management <span class="tooltip" data-tooltip="Configure tag warnings, blacklists, and favorites">?</span></h3>
<div id="tag-management-settings-content">
<label>
<input type="checkbox" id="tagWarningEnabled">
Enable Tag Warning System <span class="tooltip" data-tooltip="Shows warning badges for problematic tags">?</span>
</label>
<div class="tag-lists-container">
<div class="tag-list-section">
<h4>Blacklist Tags (Red Badges)</h4>
<textarea id="blacklistTags" placeholder="Enter tags separated by commas (e.g., scat, guro, vore)" rows="3"></textarea>
<span class="tooltip" data-tooltip="Tags that will show red warning badges. Separate with commas.">?</span>
</div>
<div class="tag-list-section">
<h4>Warning Tags (Orange Badges)</h4>
<textarea id="warningTags" placeholder="Enter tags separated by commas (e.g., ntr, cheating, netorare)" rows="3"></textarea>
<span class="tooltip" data-tooltip="Tags that will show orange warning badges. Separate with commas.">?</span>
</div>
<div class="tag-list-section">
<h4>Favorite Tags</h4>
<textarea id="favoriteTags" placeholder="Enter favorite tags separated by commas" rows="3" readonly></textarea>
<span class="tooltip" data-tooltip="Your favorite tags (managed by starring tags in gallery view). Shows blue badges.">?</span>
<button type="button" id="clearFavoriteTags" class="btn-secondary">Clear All Favorites</button>
</div>
</div>
<button type="button" id="resetTagSettings" class="btn-secondary">Reset to Defaults</button>
</div>
</div>
<label>
<input type="checkbox" id="offlineFavoritingEnabled">
Enable Offline Favoriting <span class="tooltip" data-tooltip="Allows favoriting manga even without being logged in.">?</span>
</label>
<label>
<input type="checkbox" id="tooltipsEnabled">
Enable Tooltips <span class="tooltip" data-tooltip="Enables or disables tooltips.">?</span>
</label>
<label>
<input type="checkbox" id="findSimilarEnabled">
Enable Find Similar Button <span class="tooltip" data-tooltip="Finds similar manga based on the current one.">?</span>
</label>
<div id="find-similar-options" style="display: none;">
<label>
<input type="radio" id="open-immediately" name="find-similar-type" value="immediately">
Open Immediately <span class="tooltip" data-tooltip="Opens the similar manga immediately.">?</span>
</label>
<label>
<input type="radio" id="input-tags" name="find-similar-type" value="input-tags">
Input Tags <span class="tooltip" data-tooltip="Allows inputting tags to find similar manga.">?</span>
</label>
</div>
<label>
<input type="checkbox" id="englishFilterEnabled">
Enable English Filter Button <span class="tooltip" data-tooltip="Filters manga to show only English translations.">?</span>
</label>
<label>
<input type="checkbox" id="autoLoginEnabled">
Enable Auto Login <span class="tooltip" data-tooltip="Automatically logs in with saved credentials.">?</span>
</label>
<div id="autoLoginCredentials">
<label>
Email: <input type="text" id="email">
</label>
<label>
Password: <input type="password" id="password">
</label>
</div>
<label>
<input type="checkbox" id="bookmarkLinkEnabled">
Enable Bookmark Link <span class="tooltip" data-tooltip="Adds a link to your bookmark in the manga title.">?</span>
</label>
<label>
<input type="checkbox" id="findAltmangaEnabled">
Enable Find Altmanga Button <span class="tooltip" data-tooltip="Finds alternative sources for the manga.">?</span>
</label>
<label>
<input type="checkbox" id="findAltMangaThumbnailEnabled">
Enable Find Alt Manga (Thumbnail Version) <span class="tooltip" data-tooltip="Displays alternative manga sources as thumbnails.">?</span>
</label>
<div id="find-Alt-Manga-Thumbnail-options" style="display: none;">
<label>
<input type="checkbox" id="mangagroupingenabled" name="manga-grouping-type" value="grouping">
Find Alt Manga Grouping <span class="tooltip" data-tooltip="Groups alternative versions of manga together on the page.">?</span>
</label>
</div>
<label>
<input type="checkbox" id="openInNewTabEnabled">
Enable Open in New Tab Button <span class="tooltip" data-tooltip="Opens manga links in a new tab.">?</span>
</label>
<div id="open-in-New-Tab-options" style="display: none;">
<label>
<input type="radio" id="open-in-new-tab-background" name="open-in-new-tab" value="background">
Open in New Tab (Background) <span class="tooltip" data-tooltip="Opens the link in a new tab without focusing on it.">?</span>
</label>
<label>
<input type="radio" id="open-in-new-tab-foreground" name="open-in-new-tab" value="foreground">
Open in New Tab (Foreground) <span class="tooltip" data-tooltip="Opens the link in a new tab and focuses on it.">?</span>
</label>
</div>
<label>
<input type="checkbox" id="monthFilterEnabled">
Enable Month Filter Button <span class="tooltip" data-tooltip="Filters manga by publication month.">?</span>
</label>
<label>
<input type="checkbox" id="mangaBookMarkingButtonEnabled">
Enable Manga Bookmarking Button <span class="tooltip" data-tooltip="Allows bookmarking manga for quick access.">?</span>
</label>
<div id="manga-bookmarking-options" style="display: none;">
<label>
<input type="radio" id="manga-bookmarking-cover" name="manga-bookmarking-type" value="cover">
Show Cover <span class="tooltip" data-tooltip="Displays the cover image for bookmarks.">?</span>
</label>
<label>
<input type="radio" id="manga-bookmarking-title" name="manga-bookmarking-type" value="title">
Show Title <span class="tooltip" data-tooltip="Displays the title only for bookmarks.">?</span>
</label>
<label>
<input type="radio" id="manga-bookmarking-both" name="manga-bookmarking-type" value="both">
Show Both <span class="tooltip" data-tooltip="Displays both the cover and title for bookmarks.">?</span>
</label>
</div>
<label>
<input type="checkbox" id="bookmarksEnabled">
Enable Bookmarks Button <span class="tooltip" data-tooltip="Enables the bookmarks feature.">?</span>
</label>
<div class="bookmark-actions">
<button type="button" id="exportBookmarks">Export Bookmarks</button>
<button type="button" id="importBookmarks">Import Bookmarks</button>
<input type="file" id="importBookmarksFile" accept=".json">
</div>
<div>
<label for="max-manga-per-bookmark-slider">Max Manga per Bookmark:</label>
<input type="range" id="max-manga-per-bookmark-slider" min="1" max="25" value="5">
<span id="max-manga-per-bookmark-on-mobile-value">5</span>
<span class="tooltip" data-tooltip="Sets the maximum number of manga fetched per bookmarked page.">?</span>
</div>
<!-- Page Management Section -->
<div id="page-management">
<h3 class="expand-icon">Page Management <span class="tooltip" data-tooltip="Enable or disable custom pages and features.">?</span></h3>
<div id="page-management-content">
<p>Control which custom pages and navigation elements are enabled:</p>
<div class="section-header">Feature Pages</div>
<label>
<input type="checkbox" id="offlineFavoritesPageEnabled">
Enable Offline Favorites Page <span class="tooltip" data-tooltip="Adds a tab to view all your offline favorites.">?</span>
</label>
<label>
<input type="checkbox" id="readMangaPageEnabled">
Enable Read Manga Page <span class="tooltip" data-tooltip="Adds a tab to view all your read manga with management options.">?</span>
</label>
<div id="read-manga-page-options" style="display: none; margin-left: 20px; margin-bottom: 15px;">
<div>
<label for="max-read-manga-display-slider">Max Read Manga to Display:</label>
<input type="range" id="max-read-manga-display-slider" min="10" max="500" step="10" value="50">
<span id="max-read-manga-display-value">50</span>
<span class="tooltip" data-tooltip="Sets the maximum number of manga displayed on the Read Manga page. Higher values may affect performance.">?</span>
</div>
</div>
<label>
<input type="checkbox" id="nfmPageEnabled">
Enable NFM (Nhentai Favorite Manager) Page <span class="tooltip" data-tooltip="Enables the Nhentai Favorite Manager page for favorite management.">?</span>
</label>
<label>
<input type="checkbox" id="replaceRelatedWithBookmarks">
Replace Related Manga with Bookmarks <span class="tooltip" data-tooltip="Replaces the Related Manga section with content from your bookmarks.">?</span>
</label>
<label>
<input type="checkbox" id="enableRelatedFlipButton">
Enable Related Flip Button <span class="tooltip" data-tooltip="Shows a Flip button to toggle between bookmarked and original related manga. Only works when 'Replace Related Manga with Bookmarks' is enabled.">?</span>
</label>
<label>
<input type="checkbox" id="bookmarksPageEnabled">
Enable Bookmarks Page <span class="tooltip" data-tooltip="Enables the dedicated Bookmarks page for managing saved bookmarks.">?</span>
</label>
<div id="bookmark-page-options" style="display: none;">
<label>
Bookmark Arrangement Type:
<select id="bookmark-arrangement-type">
<option value="default">Default (Most Recent)</option>
<option value="alphabetical">Alphabetical (A-Z)</option>
<option value="reverse-alphabetical">Reverse Alphabetical (Z-A)</option>
<option value="most-items">Most Items</option>
<option value="least-items">Least Items</option>
</select>
<span class="tooltip" data-tooltip="Choose how bookmarks are sorted on the bookmarks page.">?</span>
</label>
<label>
<input type="checkbox" id="enableRandomButton">
Enable Random Button <span class="tooltip" data-tooltip="Randomly selects a bookmarked manga for reading.">?</span>
</label>
<div id="random-options" style="display: none;">
<label>
<input type="radio" id="random-open-in-new-tab" name="random-open-type" value="new-tab">
Open Random Manga in New Tab <span class="tooltip" data-tooltip="Opens the randomly selected manga in a new tab.">?</span>
</label>
<label>
<input type="radio" id="random-open-in-current-tab" name="random-open-type" value="current-tab">
Open Random Manga in Current Tab <span class="tooltip" data-tooltip="Opens the randomly selected manga in the current tab.">?</span>
</label>
</div>
<label>
<input type="checkbox" id="galleryCaptionTooltipsEnabled">
Enable Gallery Caption Tooltips <span class="tooltip" data-tooltip="Enables or disables tooltips for gallery captions showing tags.">?</span>
</label>
</div>
<div class="section-header">Navigation</div>
<label>
<input type="checkbox" id="twitterButtonEnabled">
Delete Twitter Button <span class="tooltip" data-tooltip="Deletes the Twitter button.">?</span>
</label>
<label>
<input type="checkbox" id="profileButtonEnabled">
Delete Profile Button <span class="tooltip" data-tooltip="Deletes the Profile button.">?</span>
</label>
<label>
<input type="checkbox" id="infoButtonEnabled">
Delete Info Button <span class="tooltip" data-tooltip="Deletes the Info button.">?</span>
</label>
<label>
<input type="checkbox" id="logoutButtonEnabled">
Delete Logout Button <span class="tooltip" data-tooltip="Deletes the Logout button.">?</span>
</label>
<div class="section-header">Tab Arrangement</div>
<div id="tab-arrangement">
<p>Drag and drop tabs to rearrange their order:</p>
<ul id="tab-list" class="sortable-list">
<li data-tab="random" class="tab-item"><i class="fa fa-bars handle"></i> Random</li>
<li data-tab="tags" class="tab-item"><i class="fa fa-bars handle"></i> Tags</li>
<li data-tab="artists" class="tab-item"><i class="fa fa-bars handle"></i> Artists</li>
<li data-tab="characters" class="tab-item"><i class="fa fa-bars handle"></i> Characters</li>
<li data-tab="parodies" class="tab-item"><i class="fa fa-bars handle"></i> Parodies</li>
<li data-tab="groups" class="tab-item"><i class="fa fa-bars handle"></i> Groups</li>
<li data-tab="info" class="tab-item"><i class="fa fa-bars handle"></i> Info</li>
<li data-tab="twitter" class="tab-item"><i class="fa fa-bars handle"></i> Twitter</li>
<li data-tab="read_manga" class="tab-item"><i class="fa fa-bars handle"></i> Read Manga</li>
<!-- Offline Favorites tab will be added dynamically if user is not logged in -->
</ul>
<button type="button" id="resetTabOrder" class="btn-secondary">Reset to Default Order</button>
</div>
<div class="section-header">Bookmarks Page Arrangement</div>
<div id="bookmarks-arrangement">
<p>Drag and drop elements to rearrange their order in the bookmarks page:</p>
<ul id="bookmarks-list" class="sortable-list">
<li data-element="bookmarksTitle" class="tab-item"><i class="fa fa-bars handle"></i> Bookmarked Pages Title</li>
<li data-element="searchInput" class="tab-item"><i class="fa fa-bars handle"></i> Search Input</li>
<li data-element="tagSearchInput" class="tab-item"><i class="fa fa-bars handle"></i> Tag Search Input</li>
<li data-element="bookmarksList" class="tab-item"><i class="fa fa-bars handle"></i> Bookmarks List</li>
<li data-element="mangaBookmarksTitle" class="tab-item"><i class="fa fa-bars handle"></i> Manga Bookmarks Title</li>
<li data-element="mangaBookmarksList" class="tab-item"><i class="fa fa-bars handle"></i> Manga Bookmarks List</li>
</ul>
<button type="button" id="resetBookmarksOrder" class="btn-secondary">Reset to Default Order</button>
</div>
</label>
</div>
</div>
<div id="random-settings">
<h3 class="expand-icon">Random Hentai Preferences <span class="tooltip" data-tooltip="Configure preferences for random hentai.">?</span></h3>
<div id="random-settings-content">
<label>Language: <input type="text" id="pref-language"> <span class="tooltip" data-tooltip="Preferred language for random hentai.">?</span></label>
<label>Tags: <input type="text" id="pref-tags"> <span class="tooltip" data-tooltip="Preferred tags for filtering hentai.">?</span></label>
<label>Blacklisted Tags: <input type="text" id="blacklisted-tags"> <span class="tooltip" data-tooltip="Tags to exclude from search results.">?</span></label>
<label>Minimum Pages: <input type="number" id="pref-pages-min"> <span class="tooltip" data-tooltip="Minimum number of pages for random hentai.">?</span></label>
<label>Maximum Pages: <input type="number" id="pref-pages-max"> <span class="tooltip" data-tooltip="Maximum number of pages for random hentai.">?</span></label>
<label>
<input type="checkbox" id="matchAllTags">
Match All Tags (unchecked = match any) <span class="tooltip" data-tooltip="If enabled, all tags must match instead of any.">?</span>
</label>
</div>
</div>
<!-- Separator Line -->
<hr style="margin: 20px 0; border: none; border-top: 1px solid #444;">
<!-- Online Data Sync Section -->
<div id="online-sync-settings">
<h3 class="expand-icon">Online Data Sync <span class="tooltip" data-tooltip="Sync your userscript data with cloud storage providers">?</span></h3>
<div id="online-sync-settings-content">
<p>Sync your bookmarks, favorites, and settings across devices using cloud storage.</p>
<!-- User UUID -->
<div class="sync-section">
<h4>User Identification</h4>
<label>
Your UUID: <div class="uuid-controls">
<input type="text" id="userUUID" readonly style="background: #222; color: #ccc;">
<button type="button" id="edit-uuid">Edit</button>
<button type="button" id="regenerate-uuid">Regenerate</button>
<button type="button" id="browse-users">Browse Users</button>
</div>
<span class="tooltip" data-tooltip="Unique identifier for your data. Keep this safe!">?</span>
</label>
<style>
@media (max-width: 768px) {
.uuid-controls {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.uuid-controls input[type="text"] {
flex-grow: 1;
min-width: 150px;
}
.uuid-controls button {
flex-grow: 1;
}
}
</style>
<div id="uuid-edit-warning" style="display: none; color: #ff6b6b; font-size: 12px; margin-top: 5px;">
⚠️ Warning: Changing your UUID will affect which data you can access. Make sure you have the correct UUID for your data.
</div>
<div id="available-users" style="display: none; margin-top: 10px; padding: 10px; background: #333; border-radius: 3px;">
<h5 style="margin: 0 0 10px 0; color: #fff;">Available Users in Cloud Storage:</h5>
<div id="users-list" style="font-family: monospace; font-size: 12px;"></div>
<button type="button" id="close-users-list" style="margin-top: 10px; padding: 2px 8px; font-size: 11px;">Close</button>
</div>
</div>
<!-- Public Section -->
<div class="sync-section">
<h4>Public Sync (Standard Security)</h4>
<label>
<input type="checkbox" id="publicSyncEnabled">
Enable Public Sync <span class="tooltip" data-tooltip="Use predefined JSONStorage.net endpoint with standard security">?</span>
</label>
<div id="public-sync-options" style="margin-left: 20px; display: none;">
<p style="font-size: 12px; color: #aaa;">Uses predefined JSONStorage.net API with standard security level.</p>
<div class="sync-controls">
<button type="button" id="public-sync-upload">Upload Data</button>
<button type="button" id="public-sync-download">Download Data</button>
<span id="public-sync-status" class="sync-status"></span>
</div>
<div class="sync-info">
<small>Last sync: <span id="public-last-sync">Never</span></small>
</div>
</div>
</div>
<!-- Private Section -->
<div class="sync-section">
<h4>Private Sync (Enhanced Security)</h4>
<label>
<input type="checkbox" id="privateSyncEnabled">
Enable Private Sync <span class="tooltip" data-tooltip="Use your own JSONStorage.net credentials for enhanced security">?</span>
</label>
<div id="private-sync-options" style="margin-left: 20px; margin-top: 10px; display: none;">
<label>
Storage URL: <input type="text" id="privateStorageUrl" placeholder="https://api.jsonstorage.net/v1/json/your-endpoint">
<span class="tooltip" data-tooltip="Your custom JSONStorage.net endpoint URL">?</span>
</label>
<label>
API Key: <input type="password" id="privateApiKey" placeholder="Your API key">
<span class="tooltip" data-tooltip="Your private JSONStorage.net API key">?</span>
</label>
<div class="sync-controls">
<button type="button" id="private-sync-upload">Upload Data</button>
<button type="button" id="private-sync-download">Download Data</button>
<span id="private-sync-status" class="sync-status"></span>
</div>
<div class="sync-info">
<small>Last sync: <span id="private-last-sync">Never</span></small>
</div>
</div>
</div>
<!-- Sync Settings -->
<div class="sync-section">
<h4>Sync Options</h4>
<label>
<input type="checkbox" id="autoSyncEnabled">
Enable Auto Sync <span class="tooltip" data-tooltip="Automatically sync data when changes are made">?</span>
</label>
<label>
Sync Interval (minutes): <input type="number" id="syncInterval" min="5" max="1440" value="30">
<span class="tooltip" data-tooltip="How often to automatically sync (5-1440 minutes)">?</span>
</label>
<div id="auto-sync-status" style="font-size: 12px; color: #666; margin-top: 5px;">
No automatic syncs yet
</div>
<button id="trigger-auto-sync" style="margin-top: 10px; padding: 5px 10px; font-size: 12px;">
Trigger Sync Now
</button>
</div>
</div>
</div>
<!-- Advanced Storage Section -->
<div id="advanced-settings">
<h3 class="expand-icon">Advanced Storage Management <span class="tooltip" data-tooltip="View and modify all data stored in GM.getValue">?</span></h3>
<div id="advanced-settings-content">
<p>This section allows you to view and modify all data stored by this userscript.</p>
<button type="button" id="refresh-storage">Refresh Storage Data</button>
<div id="storage-keys-list"></div>
<div id="edit-value-modal">
<div id="edit-value-content">
<h3>Edit Storage Value</h3>
<p id="editing-key-name">Key: </p>
<textarea id="edit-value-textarea"></textarea>
<div class="modal-buttons">
<button type="button" id="cancel-edit">Cancel</button>
<button type="button" id="save-edit">Save Changes</button>
</div>
</div>
</div>
</div>
</div>
<button type="submit">Save Settings</button>
</form>
</div>
`;
// Append settings form to the container
$('div.container').append(settingsHtml);
// Nhentai Plus+.user.js (2441-2516)
// Load settings
(async function() {
const findSimilarEnabled = await GM.getValue('findSimilarEnabled', true);
const englishFilterEnabled = await GM.getValue('englishFilterEnabled', true);
const autoLoginEnabled = await GM.getValue('autoLoginEnabled', true);
const email = await GM.getValue('email', '');
const password = await GM.getValue('password', '');
const findAltmangaEnabled = await GM.getValue('findAltmangaEnabled', true);
const bookmarksEnabled = await GM.getValue('bookmarksEnabled', true);
const language = await GM.getValue('randomPrefLanguage', '');
const tags = await GM.getValue('randomPrefTags', []);
const pagesMin = await GM.getValue('randomPrefPagesMin', '');
const pagesMax = await GM.getValue('randomPrefPagesMax', '');
const matchAllTags = await GM.getValue('matchAllTags', true);
const blacklistedTags = await GM.getValue('blacklistedTags', []);
const mustAddTagsEnabled = await GM.getValue('mustAddTagsEnabled', false);
const mustAddTags = (await GM.getValue('mustAddTags', [])).map(tag => tag.toLowerCase());
const findAltMangaThumbnailEnabled = await GM.getValue('findAltMangaThumbnailEnabled', true);
const openInNewTabEnabled = await GM.getValue('openInNewTabEnabled', true);
const mangaBookMarkingButtonEnabled = await GM.getValue('mangaBookMarkingButtonEnabled', true);
const mangaBookMarkingType = await GM.getValue('mangaBookMarkingType', 'cover');
const bookmarkArrangementType = await GM.getValue('bookmarkArrangementType', 'default');
const monthFilterEnabled = await GM.getValue('monthFilterEnabled', true);
const tooltipsEnabled = await GM.getValue('tooltipsEnabled', true);
const galleryCaptionTooltipsEnabled = await GM.getValue('galleryCaptionTooltipsEnabled', true);
const mangagroupingenabled = await GM.getValue('mangagroupingenabled', true);
const maxMangaPerBookmark = await GM.getValue('maxMangaPerBookmark', 5);
const openInNewTabType = await GM.getValue('openInNewTabType', 'background');
const offlineFavoritingEnabled = await GM.getValue('offlineFavoritingEnabled', true);
const offlineFavoritesPageEnabled = await GM.getValue('offlineFavoritesPageEnabled', true);
const readMangaPageEnabled = await GM.getValue('readMangaPageEnabled', true);
const maxReadMangaDisplay = await GM.getValue('maxReadMangaDisplay', 100);
const nfmPageEnabled = await GM.getValue('nfmPageEnabled', true);
// Online Data Sync settings
const publicSyncEnabled = await GM.getValue('publicSyncEnabled', false);
const privateSyncEnabled = await GM.getValue('privateSyncEnabled', false);
const privateStorageUrl = await GM.getValue('privateStorageUrl', '');
const privateApiKey = await GM.getValue('privateApiKey', '');
const autoSyncEnabled = await GM.getValue('autoSyncEnabled', false);
const syncInterval = await GM.getValue('syncInterval', 30);
const userUUID = await syncSystem.getUserUUID();
const lastSyncUpload = await GM.getValue('lastSyncUpload', null);
const lastSyncDownload = await GM.getValue('lastSyncDownload', null);
const bookmarksPageEnabled = await GM.getValue('bookmarksPageEnabled', true);
const replaceRelatedWithBookmarks = await GM.getValue('replaceRelatedWithBookmarks', true);
const enableRelatedFlipButton = await GM.getValue('enableRelatedFlipButton', true);
const twitterButtonEnabled = await GM.getValue('twitterButtonEnabled', true);
const enableRandomButton = await GM.getValue('enableRandomButton', true);
const randomOpenType = await GM.getValue('randomOpenType', 'new-tab');
const profileButtonEnabled = await GM.getValue('profileButtonEnabled', true);
const infoButtonEnabled = await GM.getValue('infoButtonEnabled', true);
const logoutButtonEnabled = await GM.getValue('logoutButtonEnabled', true);
const bookmarkLinkEnabled = await GM.getValue('bookmarkLinkEnabled', true);
const findSimilarType = await GM.getValue('findSimilarType', 'immediately');
const showNonEnglish = await GM.getValue('showNonEnglish', 'show');
const showPageNumbersEnabled = await GM.getValue('showPageNumbersEnabled', true);
// New Fade & Read settings
const markAsReadEnabled = await GM.getValue('markAsReadEnabled', true);
const autoMarkReadEnabled = await GM.getValue('autoMarkReadEnabled', true);
const nonEnglishOpacity = await GM.getValue('nonEnglishOpacity', 0.2);
const readGalleriesOpacity = await GM.getValue('readGalleriesOpacity', 0.6);
// New Tag Management settings
const tagWarningEnabled = await GM.getValue('tagWarningEnabled', true);
const blacklistTagsList = await GM.getValue('blacklistTagsList', ['scat', 'guro', 'vore', 'ryona', 'snuff']);
const warningTagsList = await GM.getValue('warningTagsList', ['ntr', 'netorare', 'cheating', 'ugly bastard', 'mind break']);
const favoriteTagsList = await GM.getValue('favoriteTagsList', []);
$('#findSimilarEnabled').prop('checked', findSimilarEnabled);
$('#find-similar-options').toggle(findSimilarEnabled);
$('#showNonEnglishSelect').val(showNonEnglish);
$('#showPageNumbersEnabled').prop('checked', showPageNumbersEnabled);
$('#englishFilterEnabled').prop('checked', englishFilterEnabled);
$('#autoLoginEnabled').prop('checked', autoLoginEnabled);
$('#email').val(email);
$('#password').val(password);
$('#findAltmangaEnabled').prop('checked', findAltmangaEnabled);
$('#bookmarksEnabled').prop('checked', bookmarksEnabled);
$('#pref-language').val(language);
$('#pref-tags').val(tags.join(', '));
$('#pref-pages-min').val(pagesMin);
$('#pref-pages-max').val(pagesMax);
$('#autoLoginCredentials').toggle(autoLoginEnabled);
$('#matchAllTags').prop('checked', matchAllTags);
$('#blacklisted-tags').val(blacklistedTags.join(', '));
$('#mustAddTagsEnabled').prop('checked', mustAddTagsEnabled);
$('#must-add-tags').val(mustAddTags.join(', '));
$('#must-add-tags').prop('disabled', !mustAddTagsEnabled);
$('#mustAddTagsEnabled').on('change', function() {
$('#must-add-tags').prop('disabled', !$(this).is(':checked'));
});
$('#findAltMangaThumbnailEnabled').prop('checked', findAltMangaThumbnailEnabled);
$('#openInNewTabEnabled').prop('checked', openInNewTabEnabled);
$('#mangaBookMarkingButtonEnabled').prop('checked', mangaBookMarkingButtonEnabled);
$('#monthFilterEnabled').prop('checked', monthFilterEnabled);
$('#tooltipsEnabled').prop('checked', tooltipsEnabled);
$('#galleryCaptionTooltipsEnabled').prop('checked', galleryCaptionTooltipsEnabled);
$('#mangagroupingenabled').prop('checked', mangagroupingenabled);
$('#max-manga-per-bookmark-slider').val(maxMangaPerBookmark);
$('#offlineFavoritingEnabled').prop('checked', offlineFavoritingEnabled);
$('#offlineFavoritesPageEnabled').prop('checked', offlineFavoritesPageEnabled);
$('#readMangaPageEnabled').prop('checked', readMangaPageEnabled);
// Initialize Read Manga Page options
$('#max-read-manga-display-slider').val(maxReadMangaDisplay);
$('#max-read-manga-display-value').text(maxReadMangaDisplay);
$('#read-manga-page-options').toggle(readMangaPageEnabled);
$('#nfmPageEnabled').prop('checked', nfmPageEnabled);
$('#bookmarksPageEnabled').prop('checked', bookmarksPageEnabled);
$('#replaceRelatedWithBookmarks').prop('checked', replaceRelatedWithBookmarks);
$('#enableRelatedFlipButton').prop('checked', enableRelatedFlipButton);
$('#twitterButtonEnabled').prop('checked', twitterButtonEnabled);
$('#enableRandomButton').prop('checked', enableRandomButton);
$('#random-open-in-new-tab').prop('checked', randomOpenType === 'new-tab');
$('#random-open-in-current-tab').prop('checked', randomOpenType === 'current-tab');
$('#profileButtonEnabled').prop('checked', profileButtonEnabled);
$('#infoButtonEnabled').prop('checked', infoButtonEnabled);
$('#logoutButtonEnabled').prop('checked', logoutButtonEnabled);
$('#bookmarkLinkEnabled').prop('checked', bookmarkLinkEnabled);
$('#open-immediately').prop('checked', findSimilarType === 'immediately');
$('#input-tags').prop('checked', findSimilarType === 'input-tags');
// Populate new Fade & Read settings
$('#markAsReadEnabled').prop('checked', markAsReadEnabled);
$('#autoMarkReadEnabled').prop('checked', autoMarkReadEnabled);
$('#nonEnglishOpacity').val(nonEnglishOpacity);
$('#nonEnglishOpacityValue').text(nonEnglishOpacity);
$('#readGalleriesOpacity').val(readGalleriesOpacity);
$('#readGalleriesOpacityValue').text(readGalleriesOpacity);
// Populate new Tag Management settings
$('#tagWarningEnabled').prop('checked', tagWarningEnabled);
$('#blacklistTags').val(blacklistTagsList.join(', '));
$('#warningTags').val(warningTagsList.join(', '));
$('#favoriteTags').val(favoriteTagsList.join(', '));
// Populate sync settings
$('#publicSyncEnabled').prop('checked', publicSyncEnabled);
$('#privateSyncEnabled').prop('checked', privateSyncEnabled);
$('#privateStorageUrl').val(privateStorageUrl);
$('#privateApiKey').val(privateApiKey);
$('#autoSyncEnabled').prop('checked', autoSyncEnabled);
$('#syncInterval').val(syncInterval);
$('#userUUID').val(userUUID);
// Update sync status displays
$('#public-last-sync').text(lastSyncUpload ? new Date(lastSyncUpload).toLocaleString() : 'Never');
$('#private-last-sync').text(lastSyncDownload ? new Date(lastSyncDownload).toLocaleString() : 'Never');
// Update autosync status display
const lastAutoSync = await GM.getValue('lastAutoSync', null);
$('#auto-sync-status').text(lastAutoSync ? `Last auto sync: ${new Date(lastAutoSync).toLocaleString()}` : 'No automatic syncs yet');
// Show/hide sync options based on enabled state
$('#public-sync-options').toggle(publicSyncEnabled);
$('#private-sync-options').toggle(privateSyncEnabled);
// Initialize AutoSync Manager
await autoSyncManager.initialize();
// Add event handlers for sync functionality
$('#publicSyncEnabled').on('change', function() {
$('#public-sync-options').toggle($(this).prop('checked'));
});
$('#privateSyncEnabled').on('change', function() {
$('#private-sync-options').toggle($(this).prop('checked'));
});
// Save private storage credentials when they change (even when hidden)
$('#privateStorageUrl').on('input blur', async function() {
const url = $(this).val();
await GM.setValue('privateStorageUrl', url);
});
$('#privateApiKey').on('input blur', async function() {
const apiKey = $(this).val();
await GM.setValue('privateApiKey', apiKey);
});
let originalUUID = null; // Store the original UUID when editing starts
$('#edit-uuid').on('click', async function() {
const isReadonly = $('#userUUID').prop('readonly');
if (isReadonly) {
// Store the original UUID when editing begins
originalUUID = $('#userUUID').val();
// Enable editing
$('#userUUID').prop('readonly', false).css({
'background': '#333',
'color': '#fff',
'border': '1px solid #666'
});
$('#edit-uuid').text('Save');
$('#uuid-edit-warning').show();
$('#regenerate-uuid').prop('disabled', true);
} else {
// Save the edited UUID
const newUUID = $('#userUUID').val().trim().toUpperCase();
// Validate UUID format (5 alphanumeric characters)
if (!/^[A-Z0-9]{5}$/.test(newUUID)) {
alert('UUID must be exactly 5 alphanumeric characters (A-Z, 0-9)');
$('#userUUID').val(originalUUID);
return;
}
if (newUUID !== originalUUID) {
const confirmChange = confirm(
`Are you sure you want to change your UUID from "${originalUUID}" to "${newUUID}"?\n\n` +
'This will affect which data you can access from cloud storage. ' +
'Make sure this is the correct UUID for your data.'
);
if (!confirmChange) {
$('#userUUID').val(originalUUID);
return;
}
await GM.setValue('userUUID', newUUID);
// Force update the syncSystem's cached UUID
syncSystem.cachedUUID = newUUID;
showPopup('UUID updated successfully!');
} else {
// Even if UUID is the same, ensure it's saved to storage
await GM.setValue('userUUID', newUUID);
// Force update the syncSystem's cached UUID
syncSystem.cachedUUID = newUUID;
showPopup('UUID saved successfully!');
}
// Disable editing
$('#userUUID').prop('readonly', true).css({
'background': '#222',
'color': '#ccc',
'border': '1px solid #333'
});
$('#edit-uuid').text('Edit');
$('#uuid-edit-warning').hide();
$('#regenerate-uuid').prop('disabled', false);
}
});
$('#regenerate-uuid').on('click', async function() {
if (confirm('Are you sure you want to regenerate your UUID? This will create a new unique identifier and you may lose access to your existing cloud data.')) {
const newUUID = syncSystem.generateUUID();
await GM.setValue('userUUID', newUUID);
// Force update the syncSystem's cached UUID
syncSystem.cachedUUID = newUUID;
$('#userUUID').val(newUUID);
showPopup('UUID regenerated successfully!');
}
});
$('#browse-users').on('click', async function() {
try {
$('#browse-users').prop('disabled', true).text('Loading...');
// Only work with private sync
if (!$('#privateSyncEnabled').prop('checked')) {
showPopup('Browse Users is only available for Private Sync. Please enable Private Sync first.');
return;
}
const url = $('#privateStorageUrl').val();
const apiKey = $('#privateApiKey').val();
if (!url || !apiKey) {
showPopup('Please enter both Storage URL and API Key for Private Sync to browse users.');
return;
}
let allData = null;
try {
allData = await syncSystem.providers.jsonstorage.download({ url, apiKey });
} catch (error) {
showPopup(`Error accessing private storage: ${error.message}`);
return;
}
if (!allData) {
showPopup('No data found in private storage.');
return;
}
// Display available users
let usersList = '';
const currentUUID = $('#userUUID').val();
if (allData.users) {
// Multi-user format
Object.keys(allData.users).forEach(uuid => {
const userData = allData.users[uuid];
const isCurrent = uuid === currentUUID;
const lastSync = new Date(userData.timestamp).toLocaleString();
const version = userData.version || 'Unknown';
usersList += `
<div style="margin: 5px 0; padding: 8px; background: ${isCurrent ? '#2d5a2d' : '#444'}; border-radius: 3px; cursor: pointer;"
onclick="selectUUID('${uuid}')">
<strong>${uuid}</strong> ${isCurrent ? '(Current)' : ''}
<br><small>Version: ${version} | Last sync: ${lastSync}</small>
</div>
`;
});
} else if (allData.userUUID) {
// Old single-user format
const isCurrent = allData.userUUID === currentUUID;
const lastSync = new Date(allData.timestamp).toLocaleString();
const version = allData.version || 'Unknown';
usersList = `
<div style="margin: 5px 0; padding: 8px; background: ${isCurrent ? '#2d5a2d' : '#444'}; border-radius: 3px; cursor: pointer;"
onclick="selectUUID('${allData.userUUID}')">
<strong>${allData.userUUID}</strong> ${isCurrent ? '(Current)' : ''}
<br><small>Version: ${version} | Last sync: ${lastSync}</small>
</div>
`;
}
$('#users-list').html(`<p><strong>Source:</strong> Private Sync</p>` + usersList);
$('#available-users').show();
} catch (error) {
console.error('Error browsing users:', error);
showPopup(`Error browsing users: ${error.message}`);
} finally {
$('#browse-users').prop('disabled', false).text('Browse Users');
}
});
$('#close-users-list').on('click', function() {
$('#available-users').hide();
});
// Global function to select UUID from the users list
window.selectUUID = async function(uuid) {
if (confirm(`Switch to UUID "${uuid}"? This will change your current UUID and affect which data you can access.`)) {
$('#userUUID').val(uuid);
await GM.setValue('userUUID', uuid);
// Force update the syncSystem's cached UUID
syncSystem.cachedUUID = uuid;
$('#available-users').hide();
showPopup(`UUID changed to ${uuid}`);
}
};
// Manual autosync trigger
$('#trigger-auto-sync').on('click', async function() {
const button = $(this);
const originalText = button.text();
try {
button.prop('disabled', true).text('Syncing...');
await autoSyncManager.performAutoSync();
// Update status display
const lastAutoSync = await GM.getValue('lastAutoSync', null);
$('#auto-sync-status').text(lastAutoSync ? `Last auto sync: ${new Date(lastAutoSync).toLocaleString()}` : 'No automatic syncs yet');
showPopup('Manual autosync completed successfully!');
} catch (error) {
console.error('Manual autosync failed:', error);
showPopup(`Manual autosync failed: ${error.message}`);
} finally {
button.prop('disabled', false).text(originalText);
}
});
// Public sync handlers
$('#public-sync-upload').on('click', async function() {
await handleSyncOperation('upload', 'public');
});
$('#public-sync-download').on('click', async function() {
await handleSyncOperation('download', 'public');
});
// Private sync handlers
$('#private-sync-upload').on('click', async function() {
await handleSyncOperation('upload', 'private');
});
$('#private-sync-download').on('click', async function() {
await handleSyncOperation('download', 'private');
});
// Sync operation handler
async function handleSyncOperation(operation, syncType) {
const statusElement = $(`#${syncType}-sync-status`);
const lastSyncElement = $(`#${syncType}-last-sync`);
try {
// Disable buttons and show loading
$(`#${syncType}-sync-upload, #${syncType}-sync-download`).prop('disabled', true);
statusElement.removeClass('success error').addClass('loading').text('Processing...');
let config;
if (syncType === 'public') {
config = syncSystem.publicConfig;
} else {
const url = $('#privateStorageUrl').val();
const apiKey = $('#privateApiKey').val();
if (!url || !apiKey) {
throw new Error('Please enter both Storage URL and API Key for private sync');
}
config = { url, apiKey };
}
if (operation === 'upload') {
await syncSystem.uploadData('jsonstorage', config);
statusElement.removeClass('loading error').addClass('success').text('Upload successful!');
lastSyncElement.text(new Date().toLocaleString());
showPopup('Data uploaded successfully! Your data has been saved to the cloud.');
} else {
const result = await syncSystem.downloadData('jsonstorage', config);
statusElement.removeClass('loading error').addClass('success').text('Download successful!');
lastSyncElement.text(new Date().toLocaleString());
let message = `Data downloaded successfully! Applied ${result.appliedCount} settings.`;
// if (result.allUsers && result.allUsers.length > 1) {
// message += `\n\nAvailable user UUIDs in cloud storage: ${result.allUsers.join(', ')}`;
// }
showPopup(message);
// Refresh the page to apply downloaded settings
setTimeout(() => {
if (confirm('Settings have been updated. Refresh the page to see changes?')) {
location.reload();
}
}, 2000);
}
} catch (error) {
console.error('Sync operation failed:', error);
statusElement.removeClass('loading success').addClass('error').text(`Error: ${error.message}`);
showPopup(`Sync failed: ${error.message}`);
} finally {
// Re-enable buttons
$(`#${syncType}-sync-upload, #${syncType}-sync-download`).prop('disabled', false);
// Clear status after 5 seconds
setTimeout(() => {
statusElement.removeClass('success error loading').text('');
}, 5000);
}
}
// Nhentai Plus+.user.js (2522-2535)
// Initialize the visibility of the find-similar-options div based on the initial state of the findSimilarEnabled checkbox
$('#find-similar-options').toggle(findSimilarEnabled);
// Add event listener to toggle the find-similar-options div when the findSimilarEnabled checkbox is changed
$('#findSimilarEnabled').on('change', function() {
const isChecked = $(this).is(':checked');
$('#find-similar-options').toggle(isChecked);
});
// Toggle auto login credentials
$('#autoLoginEnabled').on('change', function() {
$('#autoLoginCredentials').toggle(this.checked);
});
// Add expand/collapse functionality for new page management section
// Add expand/collapse functionality for new page management section
const pageManagementExpanded = await GM.getValue('pageManagementExpanded', false);
$('#page-management-content').toggle(pageManagementExpanded);
$('#page-management h3').toggleClass('expanded', pageManagementExpanded);
$('#page-management h3').click(async function() {
const isExpanded = $(this).hasClass('expanded');
$(this).toggleClass('expanded', !isExpanded);
$('#page-management-content').slideToggle();
await GM.setValue('pageManagementExpanded', !isExpanded);
});
// Add expand/collapse functionality for Random Hentai Preferences section
const randomSettingsExpanded = await GM.getValue('randomSettingsExpanded', false);
$('#random-settings-content').toggle(randomSettingsExpanded);
$('#random-settings h3').toggleClass('expanded', randomSettingsExpanded);
$('#random-settings h3').click(async function() {
const isExpanded = $(this).hasClass('expanded');
$(this).toggleClass('expanded', !isExpanded);
$('#random-settings-content').slideToggle();
await GM.setValue('randomSettingsExpanded', !isExpanded);
});
// Add expand/collapse functionality for Online Data Sync section
const onlineSyncExpanded = await GM.getValue('onlineSyncExpanded', false);
$('#online-sync-settings-content').toggle(onlineSyncExpanded);
$('#online-sync-settings h3').toggleClass('expanded', onlineSyncExpanded);
$('#online-sync-settings h3').click(async function() {
const isExpanded = $(this).hasClass('expanded');
$(this).toggleClass('expanded', !isExpanded);
$('#online-sync-settings-content').slideToggle();
await GM.setValue('onlineSyncExpanded', !isExpanded);
});
// Show or hide the random options based on the enableRandomButton value
if (enableRandomButton) {
$('#random-options').show();
} else {
$('#random-options').hide();
}
// Add an event listener to the enableRandomButton to show or hide the random options
$('#enableRandomButton').on('change', function() {
if ($(this).is(':checked')) {
$('#random-options').show();
} else {
$('#random-options').hide();
}
});
$('#max-manga-per-bookmark-slider').on('input', function() {
const value = parseInt($(this).val());
$('#max-manga-per-bookmark-on-mobile-value').text(value);
//GM.setValue('maxMangaPerBookmark', value);
});
(async function() {
const maxMangaPerBookmark = await GM.getValue('maxMangaPerBookmark', 5);
$('#max-manga-per-bookmark-slider').val(maxMangaPerBookmark);
$('#max-manga-per-bookmark-on-mobile-value').text(maxMangaPerBookmark);
})();
$('.tooltip').toggle(tooltipsEnabled);
$('#tooltipsEnabled').on('change', function() {
$('.tooltip').toggle(this.checked);
});
// Toggle gallery caption tooltips
const toggleGalleryCaptionTooltips = (enabled) => {
$('.gallery .caption[title]').toggleClass('tooltip-enabled', enabled);
};
toggleGalleryCaptionTooltips(galleryCaptionTooltipsEnabled);
$('#galleryCaptionTooltipsEnabled').on('change', function() {
toggleGalleryCaptionTooltips(this.checked);
});
if (findAltMangaThumbnailEnabled){
$('#find-Alt-Manga-Thumbnail-options').show();
}
$('#findAltMangaThumbnailEnabled').on('change', function() {
if ($(this).prop('checked')) {
$('#find-Alt-Manga-Thumbnail-options').show();
} else {
$('#find-Alt-Manga-Thumbnail-options').hide();
}
});
if(bookmarksPageEnabled){
$('#bookmark-page-options').show();
}
$('#bookmarksPageEnabled').on('change', function() {
if ($(this).prop('checked')) {
$('#bookmark-page-options').show();
} else {
$('#bookmark-page-options').hide();
}
});
// Show/hide flip button setting based on related bookmarks setting
if ($('#replaceRelatedWithBookmarks').prop('checked')) {
$('#enableRelatedFlipButton').closest('label').show();
} else {
$('#enableRelatedFlipButton').closest('label').hide();
}
// Add event listener to toggle flip button setting visibility
$('#replaceRelatedWithBookmarks').on('change', function() {
if ($(this).prop('checked')) {
$('#enableRelatedFlipButton').closest('label').show();
} else {
$('#enableRelatedFlipButton').closest('label').hide();
}
});
if (mangaBookMarkingButtonEnabled) {
$('#manga-bookmarking-options').show();
}
if (mangaBookMarkingType === 'cover') {
$('#manga-bookmarking-cover').prop('checked', true);
} else if (mangaBookMarkingType === 'title') {
$('#manga-bookmarking-title').prop('checked', true);
} else if (mangaBookMarkingType === 'both') {
$('#manga-bookmarking-both').prop('checked', true);
}
// Initialize bookmark arrangement dropdown
$('#bookmark-arrangement-type').val(bookmarkArrangementType);
$('#mangaBookMarkingButtonEnabled').on('change', function() {
if ($(this).prop('checked')) {
$('#manga-bookmarking-options').show();
} else {
$('#manga-bookmarking-options').hide();
}
});
// Add event listener for Read Manga Page options
$('#readMangaPageEnabled').on('change', function() {
$('#read-manga-page-options').toggle($(this).is(':checked'));
});
// Update the display value for the max read manga slider
$('#max-read-manga-display-slider').on('input', function() {
const value = parseInt($(this).val());
$('#max-read-manga-display-value').text(value);
});
$('#showNonEnglishSelect').on('change', async () => {
const showNonEnglish = $('#showNonEnglishSelect').val();
await GM.setValue('showNonEnglish', showNonEnglish);
applyNonEnglishStyles();
});
// Event handlers for new Fade & Read settings
$('#fade-read-settings h3').on('click', function() {
$('#fade-read-settings-content').toggle();
$(this).toggleClass('expanded');
});
$('#nonEnglishOpacity').on('input', function() {
const value = parseFloat($(this).val());
$('#nonEnglishOpacityValue').text(value);
});
$('#readGalleriesOpacity').on('input', function() {
const value = parseFloat($(this).val());
$('#readGalleriesOpacityValue').text(value);
});
$('#resetFadeSettings').on('click', function() {
$('#nonEnglishOpacity').val(0.2);
$('#nonEnglishOpacityValue').text('0.2');
$('#readGalleriesOpacity').val(0.6);
$('#readGalleriesOpacityValue').text('0.6');
$('#markAsReadEnabled').prop('checked', true);
$('#autoMarkReadEnabled').prop('checked', true);
});
// Event handlers for new Tag Management settings
$('#tag-management-settings h3').on('click', function() {
$('#tag-management-settings-content').toggle();
$(this).toggleClass('expanded');
});
$('#clearFavoriteTags').on('click', async function() {
if (confirm('Are you sure you want to clear all favorite tags?')) {
await GM.setValue('favoriteTagsList', []);
$('#favoriteTags').val('');
}
});
$('#resetTagSettings').on('click', function() {
$('#blacklistTags').val('scat, guro, vore, ryona, snuff');
$('#warningTags').val('ntr, netorare, cheating, ugly bastard, mind break');
$('#tagWarningEnabled').prop('checked', true);
});
// Check if openInNewTabEnabled is true, if not, hide the options
if (openInNewTabEnabled) {
$('#open-in-New-Tab-options').show();
}
// Add event listeners to the radio buttons
$('#open-in-new-tab-background').change(function() {
if (this.checked) {
GM.setValue('openInNewTabType', 'background');
}
});
$('#open-in-new-tab-foreground').change(function() {
if (this.checked) {
GM.setValue('openInNewTabType', 'foreground');
}
});
// Initialize the radio buttons based on the stored value
if (openInNewTabType === 'background') {
$('#open-in-new-tab-background').prop('checked', true);
} else {
$('#open-in-new-tab-foreground').prop('checked', true);
}
// Update the openInNewTabEnabled value in storage when the checkbox is changed
$('#openInNewTabEnabled').change(function() {
const openInNewTabEnabled = this.checked;
GM.setValue('openInNewTabEnabled', openInNewTabEnabled);
if (!openInNewTabEnabled) {
GM.setValue('openInNewTabType', null);
}
$('#open-in-New-Tab-options').toggle(openInNewTabEnabled);
});
})();
// Save settings
$('#settingsForm').on('submit', async function(event) {
event.preventDefault();
const findSimilarEnabled = $('#findSimilarEnabled').prop('checked');
const englishFilterEnabled = $('#englishFilterEnabled').prop('checked');
const autoLoginEnabled = $('#autoLoginEnabled').prop('checked');
const email = $('#email').val();
const password = $('#password').val();
const findAltmangaEnabled = $('#findAltmangaEnabled').prop('checked');
const bookmarksEnabled = $('#bookmarksEnabled').prop('checked');
const language = $('#pref-language').val();
let tags = $('#pref-tags').val().split(',').map(tag => tag.trim());
tags = tags.map(tag => tag.replace(/-/g, ' ')); // Replace hyphens with spaces
let blacklistedTags = $('#blacklisted-tags').val().split(',').map(tag => tag.trim());
blacklistedTags = blacklistedTags.map(tag => tag.replace(/-/g, ' ')); // Replace hyphens with spaces
const mustAddTagsEnabled = $('#mustAddTagsEnabled').is(':checked');
let mustAddTags = [];
if (mustAddTagsEnabled) {
mustAddTags = $('#must-add-tags').val().split(',').map(tag => tag.trim());
mustAddTags = mustAddTags.map(tag => tag.replace(/-/g, ' ')); // Replace hyphens with spaces
}
const pagesMin = $('#pref-pages-min').val();
const pagesMax = $('#pref-pages-max').val();
const matchAllTags = $('#matchAllTags').prop('checked');
const findAltMangaThumbnailEnabled = $('#findAltMangaThumbnailEnabled').prop('checked');
const openInNewTabEnabled = $('#openInNewTabEnabled').prop('checked');
const mangaBookMarkingButtonEnabled = $('#mangaBookMarkingButtonEnabled').prop('checked');
const mangaBookMarkingType = $('input[name="manga-bookmarking-type"]:checked').val();
const bookmarkArrangementType = $('#bookmark-arrangement-type').val();
const monthFilterEnabled = $('#monthFilterEnabled').prop('checked');
const tooltipsEnabled = $('#tooltipsEnabled').prop('checked');
const galleryCaptionTooltipsEnabled = $('#galleryCaptionTooltipsEnabled').prop('checked');
const mangagroupingenabled = $('#mangagroupingenabled').prop('checked');
const maxMangaPerBookmark = parseInt($('#max-manga-per-bookmark-slider').val());
const openInNewTabType = $('input[name="open-in-new-tab"]:checked').val();
const offlineFavoritingEnabled = $('#offlineFavoritingEnabled').prop('checked');
const offlineFavoritesPageEnabled = $('#offlineFavoritesPageEnabled').prop('checked');
const readMangaPageEnabled = $('#readMangaPageEnabled').prop('checked');
const maxReadMangaDisplay = parseInt($('#max-read-manga-display-slider').val());
const nfmPageEnabled = $('#nfmPageEnabled').prop('checked');
const bookmarksPageEnabled = $('#bookmarksPageEnabled').prop('checked');
const replaceRelatedWithBookmarks = $('#replaceRelatedWithBookmarks').prop('checked');
const enableRelatedFlipButton = $('#enableRelatedFlipButton').prop('checked');
const twitterButtonEnabled = $('#twitterButtonEnabled').prop('checked');
const enableRandomButton = $('#enableRandomButton').prop('checked');
const randomOpenType = $('input[name="random-open-type"]:checked').val();
const profileButtonEnabled = $('#profileButtonEnabled').prop('checked');
const infoButtonEnabled = $('#infoButtonEnabled').prop('checked');
const logoutButtonEnabled = $('#logoutButtonEnabled').prop('checked');
const bookmarkLinkEnabled = $('#bookmarkLinkEnabled').prop('checked');
const findSimilarType = $('input[name="find-similar-type"]:checked').val();
const showNonEnglish = $('#showNonEnglishSelect').val();
const showPageNumbersEnabled = $('#showPageNumbersEnabled').prop('checked');
// Collect new Fade & Read settings
const markAsReadEnabled = $('#markAsReadEnabled').prop('checked');
const autoMarkReadEnabled = $('#autoMarkReadEnabled').prop('checked');
const nonEnglishOpacity = parseFloat($('#nonEnglishOpacity').val());
const readGalleriesOpacity = parseFloat($('#readGalleriesOpacity').val());
// Collect new Tag Management settings
const tagWarningEnabled = $('#tagWarningEnabled').prop('checked');
let blacklistTagsList = $('#blacklistTags').val().split(',').map(tag => tag.trim().toLowerCase()).filter(tag => tag);
let warningTagsList = $('#warningTags').val().split(',').map(tag => tag.trim().toLowerCase()).filter(tag => tag);
let favoriteTagsList = $('#favoriteTags').val().split(',').map(tag => tag.trim().toLowerCase()).filter(tag => tag);
// Collect sync settings
const publicSyncEnabledForm = $('#publicSyncEnabled').prop('checked');
const privateSyncEnabledForm = $('#privateSyncEnabled').prop('checked');
const privateStorageUrlForm = $('#privateStorageUrl').val();
const privateApiKeyForm = $('#privateApiKey').val();
const autoSyncEnabledForm = $('#autoSyncEnabled').prop('checked');
const syncIntervalForm = parseInt($('#syncInterval').val());
await GM.setValue('showNonEnglish', showNonEnglish);
await GM.setValue('showPageNumbersEnabled', showPageNumbersEnabled);
await GM.setValue('findSimilarEnabled', findSimilarEnabled);
await GM.setValue('englishFilterEnabled', englishFilterEnabled);
await GM.setValue('autoLoginEnabled', autoLoginEnabled);
await GM.setValue('email', email);
await GM.setValue('password', password);
await GM.setValue('findAltmangaEnabled', findAltmangaEnabled);
await GM.setValue('bookmarksEnabled', bookmarksEnabled);
await GM.setValue('randomPrefLanguage', language);
await GM.setValue('blacklistedTags', blacklistedTags);
await GM.setValue('mustAddTagsEnabled', mustAddTagsEnabled);
await GM.setValue('mustAddTags', mustAddTags);
await GM.setValue('randomPrefTags', tags);
await GM.setValue('randomPrefPagesMin', pagesMin);
await GM.setValue('randomPrefPagesMax', pagesMax);
await GM.setValue('matchAllTags', matchAllTags);
await GM.setValue('findAltMangaThumbnailEnabled', findAltMangaThumbnailEnabled);
await GM.setValue('openInNewTabEnabled', openInNewTabEnabled);
await GM.setValue('mangaBookMarkingButtonEnabled', mangaBookMarkingButtonEnabled);
await GM.setValue('mangaBookMarkingType', mangaBookMarkingType);
await GM.setValue('bookmarkArrangementType', bookmarkArrangementType);
await GM.setValue('monthFilterEnabled', monthFilterEnabled);
await GM.setValue('tooltipsEnabled', tooltipsEnabled);
await GM.setValue('galleryCaptionTooltipsEnabled', galleryCaptionTooltipsEnabled);
await GM.setValue('mangagroupingenabled', mangagroupingenabled);
await GM.setValue('maxMangaPerBookmark', maxMangaPerBookmark);
await GM.setValue('openInNewTabType', openInNewTabType);
await GM.setValue('offlineFavoritingEnabled', offlineFavoritingEnabled);
await GM.setValue('offlineFavoritesPageEnabled', offlineFavoritesPageEnabled);
await GM.setValue('readMangaPageEnabled', readMangaPageEnabled);
await GM.setValue('maxReadMangaDisplay', maxReadMangaDisplay);
await GM.setValue('nfmPageEnabled', nfmPageEnabled);
await GM.setValue('bookmarksPageEnabled', bookmarksPageEnabled);
await GM.setValue('replaceRelatedWithBookmarks', replaceRelatedWithBookmarks);
await GM.setValue('enableRelatedFlipButton', enableRelatedFlipButton);
await GM.setValue('twitterButtonEnabled', twitterButtonEnabled);
await GM.setValue('enableRandomButton', enableRandomButton);
await GM.setValue('randomOpenType', randomOpenType);
await GM.setValue('profileButtonEnabled', profileButtonEnabled);
await GM.setValue('infoButtonEnabled', infoButtonEnabled);
await GM.setValue('logoutButtonEnabled', logoutButtonEnabled);
await GM.setValue('bookmarkLinkEnabled', bookmarkLinkEnabled);
await GM.setValue('findSimilarType', findSimilarType);
// Save new Fade & Read settings
await GM.setValue('markAsReadEnabled', markAsReadEnabled);
await GM.setValue('autoMarkReadEnabled', autoMarkReadEnabled);
await GM.setValue('nonEnglishOpacity', nonEnglishOpacity);
await GM.setValue('readGalleriesOpacity', readGalleriesOpacity);
// Save new Tag Management settings
await GM.setValue('tagWarningEnabled', tagWarningEnabled);
await GM.setValue('blacklistTagsList', blacklistTagsList);
await GM.setValue('warningTagsList', warningTagsList);
await GM.setValue('favoriteTagsList', favoriteTagsList);
// Save sync settings
await GM.setValue('publicSyncEnabled', publicSyncEnabledForm);
await GM.setValue('privateSyncEnabled', privateSyncEnabledForm);
await GM.setValue('privateStorageUrl', privateStorageUrlForm);
await GM.setValue('privateApiKey', privateApiKeyForm);
await GM.setValue('autoSyncEnabled', autoSyncEnabledForm);
await GM.setValue('syncInterval', syncIntervalForm);
// Update AutoSync Manager with new settings
await autoSyncManager.updateSettings(autoSyncEnabledForm, syncIntervalForm);
// Show custom popup instead of alert
showPopup('Settings saved!');
});
// Import Bookmarked Pages
async function importBookmarkedPages(file) {
try {
const reader = new FileReader();
const fileContent = await new Promise((resolve, reject) => {
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(reader.error);
reader.readAsText(file);
});
const importedBookmarks = JSON.parse(fileContent);
if (!Array.isArray(importedBookmarks)) {
throw new Error('Invalid file format');
}
const existingBookmarks = await GM.getValue('bookmarkedPages', []);
const mergedBookmarks = [...new Set([...existingBookmarks, ...importedBookmarks])]; // Merge without duplicates
await GM.setValue('bookmarkedPages', mergedBookmarks);
alert('Bookmarks imported successfully!');
} catch (error) {
alert(`Failed to import bookmarks: ${error.message}`);
}
}
// Add event listeners to buttons on the settings page
function setupBookmarkButtons() {
// Export Button
document.getElementById('exportBookmarks').addEventListener('click', exportBookmarkedPages);
// Import Button
document.getElementById('importBookmarks').addEventListener('click', () => {
document.getElementById('importBookmarksFile').click();
});
// Handle file selection for import
document.getElementById('importBookmarksFile').addEventListener('change', (event) => {
const file = event.target.files[0];
if (file) {
importBookmarkedPages(file);
}
});
}
// Call this function after settings form is rendered
setupBookmarkButtons();
//------------------------------------------------ Advanced Settings Management Functions---------------------------------------------------------
// Toggle advanced settings section
const advancedHeader = document.querySelector('#advanced-settings h3');
const advancedContent = document.getElementById('advanced-settings-content');
if (!advancedHeader) {
console.error('Advanced settings header not found');
return;
}
if (!advancedContent) {
console.error('Advanced settings content not found');
return;
}
console.log('Advanced header found:', advancedHeader);
console.log('Initial display state:', advancedContent.style.display);
advancedHeader.addEventListener('click', function() {
console.log('Header clicked');
advancedContent.style.display = (advancedContent.style.display === 'none' || advancedContent.style.display === '') ? 'block' : 'none';
console.log('New display state:', advancedContent.style.display);
// Toggle the expanded class
advancedHeader.classList.toggle('expanded', advancedContent.style.display === 'block');
console.log('Classes after toggle:', advancedHeader.className);
if (advancedContent.style.display === 'block') {
refreshStorageData();
}
});
// Refresh storage button
const refreshBtn = document.getElementById('refresh-storage');
refreshBtn.addEventListener('click', refreshStorageData);
// Modal controls
const editModal = document.getElementById('edit-value-modal');
const cancelEditBtn = document.getElementById('cancel-edit');
const saveEditBtn = document.getElementById('save-edit');
cancelEditBtn.addEventListener('click', function() {
editModal.style.display = 'none';
});
saveEditBtn.addEventListener('click', function() {
const keyName = document.getElementById('editing-key-name').dataset.key;
const newValue = document.getElementById('edit-value-textarea').value;
try {
// Try to parse the JSON to validate it
const parsedValue = JSON.parse(newValue);
// Save the changes to GM storage
GM.setValue(keyName, parsedValue)
.then(() => {
alert('Value saved successfully!');
editModal.style.display = 'none';
refreshStorageData();
})
.catch(err => {
alert('Error saving value: ' + err.message);
});
} catch (e) {
alert('Invalid JSON format. Please check your input.');
}
});
// Function to refresh storage data with mobile-friendly layout
function refreshStorageData() {
const keysList = document.getElementById('storage-keys-list');
keysList.innerHTML = '<p>Loading storage data...</p>';
// Use GM.listValues() to get all keys
GM.listValues()
.then(keys => {
if (keys.length === 0) {
keysList.innerHTML = '<p>No data found in storage.</p>';
return;
}
keysList.innerHTML = '';
// Sort keys alphabetically for easier navigation
keys.sort();
// Process each key
Promise.all(keys.map(key => {
return GM.getValue(key)
.then(value => {
return { key, value };
});
}))
.then(items => {
// Create responsive container
const container = document.createElement('div');
container.style.width = '100%';
// Add media query detection
const isMobile = window.matchMedia("(max-width: 600px)").matches;
if (isMobile) {
// Mobile view: Card-based layout
items.forEach(item => {
const card = document.createElement('div');
card.style.border = '1px solid #444';
card.style.borderRadius = '4px';
card.style.padding = '10px';
card.style.marginBottom = '15px';
card.style.backgroundColor = '#2a2a2a';
// Key
const keyDiv = document.createElement('div');
keyDiv.style.fontWeight = 'bold';
keyDiv.style.marginBottom = '5px';
keyDiv.style.wordBreak = 'break-word';
keyDiv.textContent = item.key;
card.appendChild(keyDiv);
// Type and Size
const infoDiv = document.createElement('div');
infoDiv.style.display = 'flex';
infoDiv.style.justifyContent = 'space-between';
infoDiv.style.marginBottom = '10px';
infoDiv.style.fontSize = '0.9em';
infoDiv.style.color = '#aaa';
const typeSpan = document.createElement('span');
typeSpan.textContent = `Type: ${getValueType(item.value)}`;
const sizeSpan = document.createElement('span');
sizeSpan.textContent = `Size: ${getValueSize(item.value)}`;
infoDiv.appendChild(typeSpan);
infoDiv.appendChild(sizeSpan);
card.appendChild(infoDiv);
// Actions
const actionDiv = document.createElement('div');
actionDiv.style.display = 'flex';
actionDiv.style.gap = '10px';
const viewBtn = document.createElement('button');
viewBtn.textContent = 'View/Edit';
viewBtn.style.flex = '1';
viewBtn.style.padding = '8px';
viewBtn.style.backgroundColor = '#444';
viewBtn.style.border = 'none';
viewBtn.style.borderRadius = '4px';
viewBtn.style.color = 'white';
viewBtn.style.cursor = 'pointer';
viewBtn.addEventListener('click', function() {
openEditModal(item.key, item.value);
});
const deleteBtn = document.createElement('button');
deleteBtn.textContent = 'Delete';
deleteBtn.style.flex = '1';
deleteBtn.style.padding = '8px';
deleteBtn.style.backgroundColor = '#d9534f';
deleteBtn.style.border = 'none';
deleteBtn.style.borderRadius = '4px';
deleteBtn.style.color = 'white';
deleteBtn.style.cursor = 'pointer';
deleteBtn.addEventListener('click', function() {
if (confirm(`Are you sure you want to delete "${item.key}"?`)) {
GM.deleteValue(item.key)
.then(() => {
refreshStorageData();
})
.catch(err => {
alert('Error deleting value: ' + err.message);
});
}
});
actionDiv.appendChild(viewBtn);
actionDiv.appendChild(deleteBtn);
card.appendChild(actionDiv);
container.appendChild(card);
});
} else {
// Desktop view: Table layout
const table = document.createElement('table');
table.style.width = '100%';
table.style.borderCollapse = 'collapse';
table.style.marginTop = '10px';
// Create table header
const thead = document.createElement('thead');
const headerRow = document.createElement('tr');
['Key', 'Type', 'Size', 'Actions'].forEach(text => {
const th = document.createElement('th');
th.textContent = text;
th.style.textAlign = 'left';
th.style.padding = '8px';
th.style.backgroundColor = '#2a2a2a';
th.style.borderBottom = '1px solid #444';
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
// Create table body
const tbody = document.createElement('tbody');
items.forEach(item => {
const row = document.createElement('tr');
row.style.borderBottom = '1px solid #333';
// Key column
const keyCell = document.createElement('td');
keyCell.textContent = item.key;
keyCell.style.padding = '8px';
keyCell.style.maxWidth = '200px';
keyCell.style.overflow = 'hidden';
keyCell.style.textOverflow = 'ellipsis';
keyCell.style.whiteSpace = 'nowrap';
// Type column
const typeCell = document.createElement('td');
typeCell.textContent = getValueType(item.value);
typeCell.style.padding = '8px';
// Size column
const sizeCell = document.createElement('td');
sizeCell.textContent = getValueSize(item.value);
sizeCell.style.padding = '8px';
// Actions column
const actionsCell = document.createElement('td');
actionsCell.style.padding = '8px';
const actionWrapper = document.createElement('div');
actionWrapper.style.display = 'flex';
actionWrapper.style.gap = '5px';
const viewBtn = document.createElement('button');
viewBtn.textContent = 'View/Edit';
viewBtn.style.padding = '3px 8px';
viewBtn.style.backgroundColor = '#444';
viewBtn.style.border = 'none';
viewBtn.style.borderRadius = '2px';
viewBtn.style.color = 'white';
viewBtn.style.cursor = 'pointer';
viewBtn.addEventListener('click', function() {
openEditModal(item.key, item.value);
});
const deleteBtn = document.createElement('button');
deleteBtn.textContent = 'Delete';
deleteBtn.style.padding = '3px 8px';
deleteBtn.style.backgroundColor = '#d9534f';
deleteBtn.style.border = 'none';
deleteBtn.style.borderRadius = '2px';
deleteBtn.style.color = 'white';
deleteBtn.style.cursor = 'pointer';
deleteBtn.addEventListener('click', function() {
if (confirm(`Are you sure you want to delete "${item.key}"?`)) {
GM.deleteValue(item.key)
.then(() => {
refreshStorageData();
})
.catch(err => {
alert('Error deleting value: ' + err.message);
});
}
});
actionWrapper.appendChild(viewBtn);
actionWrapper.appendChild(deleteBtn);
actionsCell.appendChild(actionWrapper);
// Add all cells to the row
row.appendChild(keyCell);
row.appendChild(typeCell);
row.appendChild(sizeCell);
row.appendChild(actionsCell);
// Add row to table body
tbody.appendChild(row);
});
table.appendChild(tbody);
container.appendChild(table);
}
keysList.appendChild(container);
// Add option to create new key
const addNewSection = document.createElement('div');
addNewSection.style.marginTop = '20px';
const addNewHeading = document.createElement('h4');
addNewHeading.textContent = 'Add New Storage Key';
addNewSection.appendChild(addNewHeading);
const addNewForm = document.createElement('div');
addNewForm.style.display = 'flex';
addNewForm.style.gap = '10px';
addNewForm.style.marginTop = '10px';
addNewForm.style.flexWrap = 'wrap'; // Allow wrapping on small screens
const keyInput = document.createElement('input');
keyInput.type = 'text';
keyInput.placeholder = 'Key name';
keyInput.style.flex = '1';
keyInput.style.padding = '8px';
keyInput.style.backgroundColor = '#333';
keyInput.style.border = '1px solid #444';
keyInput.style.color = '#fff';
keyInput.style.borderRadius = '4px';
keyInput.style.minWidth = '120px'; // Ensure minimum usable width
const valueInput = document.createElement('input');
valueInput.type = 'text';
valueInput.placeholder = 'Value (valid JSON)';
valueInput.style.flex = '2';
valueInput.style.padding = '8px';
valueInput.style.backgroundColor = '#333';
valueInput.style.border = '1px solid #444';
valueInput.style.color = '#fff';
valueInput.style.borderRadius = '4px';
valueInput.style.minWidth = '150px'; // Ensure minimum usable width
const addBtn = document.createElement('button');
addBtn.textContent = 'Add';
addBtn.style.padding = '8px';
addBtn.style.backgroundColor = '#28a745';
addBtn.style.border = 'none';
addBtn.style.borderRadius = '4px';
addBtn.style.color = 'white';
addBtn.style.cursor = 'pointer';
addBtn.addEventListener('click', function() {
const key = keyInput.value.trim();
const value = valueInput.value.trim();
if (!key) {
alert('Please enter a key name.');
return;
}
if (!value) {
alert('Please enter a value.');
return;
}
try {
const parsedValue = JSON.parse(value);
GM.setValue(key, parsedValue)
.then(() => {
alert('New key added successfully!');
keyInput.value = '';
valueInput.value = '';
refreshStorageData();
})
.catch(err => {
alert('Error adding new key: ' + err.message);
});
} catch (e) {
alert('Invalid JSON format. Please check your input.');
}
});
addNewForm.appendChild(keyInput);
addNewForm.appendChild(valueInput);
addNewForm.appendChild(addBtn);
addNewSection.appendChild(addNewForm);
keysList.appendChild(addNewSection);
// Add export/import buttons
const buttonSection = document.createElement('div');
buttonSection.style.marginTop = '20px';
buttonSection.style.display = 'flex';
buttonSection.style.gap = '10px';
buttonSection.style.flexWrap = 'wrap'; // Allow buttons to wrap on small screens
const exportBtn = document.createElement('button');
exportBtn.textContent = 'Export All Storage Data';
exportBtn.style.padding = '10px';
exportBtn.style.backgroundColor = '#007bff';
exportBtn.style.border = 'none';
exportBtn.style.borderRadius = '4px';
exportBtn.style.color = 'white';
exportBtn.style.cursor = 'pointer';
exportBtn.style.flex = '1';
exportBtn.style.minWidth = isMobile ? '100%' : '150px';
exportBtn.addEventListener('click', function() {
const exportData = {};
items.forEach(item => {
exportData[item.key] = item.value;
});
const dataStr = JSON.stringify(exportData, null, 2);
const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr);
const exportLink = document.createElement('a');
exportLink.setAttribute('href', dataUri);
exportLink.setAttribute('download', 'userscript_storage_backup.json');
exportLink.click();
});
const importInput = document.createElement('input');
importInput.type = 'file';
importInput.accept = '.json';
importInput.style.display = 'none';
importInput.id = 'import-storage-file';
const importBtn = document.createElement('button');
importBtn.textContent = 'Import Storage Data';
importBtn.style.padding = '10px';
importBtn.style.backgroundColor = '#6c757d';
importBtn.style.border = 'none';
importBtn.style.borderRadius = '4px';
importBtn.style.color = 'white';
importBtn.style.cursor = 'pointer';
importBtn.style.flex = '1';
importBtn.style.minWidth = isMobile ? '100%' : '150px';
importBtn.addEventListener('click', function() {
importInput.click();
});
importInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
try {
const importData = JSON.parse(e.target.result);
if (confirm(`This will import ${Object.keys(importData).length} keys. Continue?`)) {
// Process each key in the import data
const importPromises = Object.entries(importData).map(([key, value]) => {
return GM.setValue(key, value);
});
Promise.all(importPromises)
.then(() => {
alert('Import completed successfully!');
refreshStorageData();
})
.catch(err => {
alert('Error during import: ' + err.message);
});
}
} catch (e) {
alert('Invalid JSON file. Please check the file format.');
}
};
reader.readAsText(file);
});
buttonSection.appendChild(exportBtn);
buttonSection.appendChild(importBtn);
buttonSection.appendChild(importInput);
keysList.appendChild(buttonSection);
})
.catch(err => {
keysList.innerHTML = `<p>Error processing storage data: ${err.message}</p>`;
});
})
.catch(err => {
keysList.innerHTML = `<p>Error loading storage data: ${err.message}</p>`;
});
}
// Function to open the edit modal
function openEditModal(key, value) {
const editModal = document.getElementById('edit-value-modal');
const keyNameElem = document.getElementById('editing-key-name');
const valueTextarea = document.getElementById('edit-value-textarea');
keyNameElem.textContent = `Key: ${key}`;
keyNameElem.dataset.key = key;
// Format the JSON for better readability
const formattedValue = JSON.stringify(value, null, 2);
valueTextarea.value = formattedValue;
editModal.style.display = 'block';
}
// Helper function to get the type of a value
function getValueType(value) {
if (value === null) return 'null';
if (Array.isArray(value)) return 'array';
return typeof value;
}
// Helper function to get the size of a value
function getValueSize(value) {
const json = JSON.stringify(value);
const bytes = new Blob([json]).size;
if (bytes < 1024) {
return bytes + ' bytes';
} else if (bytes < 1024 * 1024) {
return (bytes / 1024).toFixed(2) + ' KB';
} else {
return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
}
}
}
// Initialize tab arrangement functionality
initializeTabSorting();
updateMenuOrder();
// Initialize Bookmarks Page Arrangement functionality
initializeBookmarksSorting();
// Initialize bookmarks page order from storage or use default order
async function initializeBookmarksOrder() {
const defaultOrder = ['bookmarksTitle', 'searchInput', 'tagSearchInput', 'bookmarksList', 'mangaBookmarksTitle', 'mangaBookmarksList'];
const savedOrder = await GM.getValue('bookmarksContainerOrder');
return savedOrder || defaultOrder;
}
// Function to initialize the bookmarks page sorting functionality
function initializeBookmarksSorting() {
const bookmarksList = document.getElementById('bookmarks-list');
if (!bookmarksList) return;
// Initialize bookmarks list with saved order
initializeBookmarksOrder().then(bookmarksOrder => {
// Reorder the list items according to the saved order
const listItems = Array.from(bookmarksList.children);
const tempContainer = document.createDocumentFragment();
bookmarksOrder.forEach(elementName => {
const item = listItems.find(li => li.dataset.element === elementName);
if (item) tempContainer.appendChild(item);
});
// Clear the list and add all items in the new order
while (bookmarksList.firstChild) {
bookmarksList.removeChild(bookmarksList.firstChild);
}
bookmarksList.appendChild(tempContainer);
});
// Initialize Sortable.js for Bookmarks Page Arrangement
new Sortable(bookmarksList, {
animation: 150,
handle: '.handle',
ghostClass: 'sortable-ghost',
dragClass: 'sortable-drag',
forceFallback: true,
fallbackTolerance: 1,
delayOnTouchOnly: false,
delay: 0,
touchStartThreshold: 1,
preventTextSelection: true,
onStart: function(evt) {
evt.item.classList.add('dragging');
document.body.style.userSelect = 'none';
document.body.style.webkitUserSelect = 'none';
document.body.style.mozUserSelect = 'none';
document.body.style.msUserSelect = 'none';
},
onEnd: async function(evt) {
evt.item.classList.remove('dragging');
document.body.style.userSelect = '';
document.body.style.webkitUserSelect = '';
document.body.style.mozUserSelect = '';
document.body.style.msUserSelect = '';
const newOrder = Array.from(bookmarksList.children).map(item => item.dataset.element);
await GM.setValue('bookmarksContainerOrder', newOrder);
}
});
// Add mouse event listeners to improve drag handle feedback
bookmarksList.querySelectorAll('.handle').forEach(handle => {
handle.addEventListener('mousedown', () => {
handle.style.cursor = 'grabbing';
});
handle.addEventListener('mouseup', () => {
handle.style.cursor = 'grab';
});
});
// Reset button handler
document.getElementById('resetBookmarksOrder').addEventListener('click', async function() {
const defaultOrder = ['bookmarksTitle', 'searchInput', 'tagSearchInput', 'bookmarksList', 'mangaBookmarksTitle', 'mangaBookmarksList'];
await GM.setValue('bookmarksContainerOrder', defaultOrder);
showPopup('Bookmarks page order reset!', {timeout: 1000});
// Reset visual order in settings
const bookmarksList = document.getElementById('bookmarks-list');
defaultOrder.forEach(elementName => {
const item = bookmarksList.querySelector(`[data-element="${elementName}"]`);
if (item) bookmarksList.appendChild(item);
});
});
}
// Initialize tab order from storage or use default order
async function initializeTabOrder() {
const defaultOrder = ['random', 'tags', 'artists', 'characters', 'parodies', 'groups', 'info', 'twitter', 'bookmarks', 'offline_favorites', 'read_manga', 'continue_reading', 'settings'];
const savedOrder = await GM.getValue('tabOrder');
return savedOrder || defaultOrder;
}
// Function to update the menu based on tab order
async function updateMenuOrder() {
const tabOrder = await initializeTabOrder();
const menu = document.querySelector('ul.menu.left');
const dropdown = document.querySelector('ul.dropdown-menu');
if (!menu || !dropdown) return;
// Get all menu items (both desktop and injected)
const allMenuItems = Array.from(menu.querySelectorAll('li:not(.dropdown)'));
// Create a temporary container to hold items during reordering
const tempContainer = document.createDocumentFragment();
// Process each tab in the desired order
for (const tabId of tabOrder) {
// Find the menu item for this tab
const menuItem = allMenuItems.find(li => {
const link = li.querySelector('a');
if (!link) return false;
const href = link.getAttribute('href');
// Special case for Twitter which is an external link
if (tabId === 'twitter' && href.includes('twitter.com/nhentaiOfficial')) {
return true;
}
// Special case for Offline Favorites
if (tabId === 'offline_favorites' && href.includes('/favorite/')) {
return true;
}
// Regular case for internal links
return href.includes(`/${tabId}/`) ||
(tabId === 'read_manga' && href.includes('/read-manga/'));
});
// If found, move it to our temporary container
if (menuItem) {
tempContainer.appendChild(menuItem);
}
}
// Add the dropdown menu item
const dropdownItem = menu.querySelector('li.dropdown');
if (dropdownItem) {
tempContainer.appendChild(dropdownItem);
}
// Clear the menu and add all items in the new order
while (menu.firstChild) {
menu.removeChild(menu.firstChild);
}
menu.appendChild(tempContainer);
// Now update the dropdown menu
// Clear the dropdown menu first
while (dropdown.firstChild) {
dropdown.removeChild(dropdown.firstChild);
}
// Add items to dropdown in the same order
for (const tabId of tabOrder) {
// Find the corresponding desktop item
const desktopItem = Array.from(menu.querySelectorAll('li')).find(li => {
const link = li.querySelector('a');
if (!link) return false;
const href = link.getAttribute('href');
// Special case for Twitter which is an external link
if (tabId === 'twitter' && href.includes('twitter.com/nhentaiOfficial')) {
return true;
}
// Regular case for internal links
return href.includes(`/${tabId}/`) ||
(tabId === 'read_manga' && href.includes('/read-manga/'));
});
if (desktopItem) {
// Clone the link and create a new dropdown item
const link = desktopItem.querySelector('a');
if (link) {
const dropdownLi = document.createElement('li');
dropdownLi.innerHTML = `<a href="${link.getAttribute('href')}">${link.textContent}</a>`;
dropdown.appendChild(dropdownLi);
}
}
}
}
// Helper function to find the reference item for insertion
function findReferenceItem(menu, tabOrder, currentIndex) {
// Find the previous item in the order that exists in the menu
for (let i = currentIndex - 1; i >= 0; i--) {
const prevTabId = tabOrder[i];
const prevItem = Array.from(menu.querySelectorAll('li')).find(li => {
const link = li.querySelector('a');
return link && link.getAttribute('href').includes(prevTabId);
});
if (prevItem) return prevItem;
}
return null;
}
// Initialize Sortable.js for tab arrangement
function initializeTabSorting() {
const tabList = document.getElementById('tab-list');
if (!tabList) return;
// Initialize tab list with saved order
initializeTabOrder().then(tabOrder => {
// First, check if we need to create the dynamic tab items
const bookmarksExists = tabOrder.includes('bookmarks') && !tabList.querySelector('[data-tab="bookmarks"]');
const continueReadingExists = tabOrder.includes('continue_reading') && !tabList.querySelector('[data-tab="continue_reading"]');
const settingsExists = tabOrder.includes('settings') && !tabList.querySelector('[data-tab="settings"]');
// Check if these items exist in the actual menu before adding them to the sortable list
const menu = document.querySelector('ul.menu.left');
if (menu) {
// Only create bookmarks tab item if it exists in the actual menu and not in the DOM
const bookmarksInMenu = Array.from(menu.querySelectorAll('li')).some(li => {
const link = li.querySelector('a');
return link && link.getAttribute('href').includes('/bookmarks/');
});
if (bookmarksInMenu && bookmarksExists) {
const bookmarksTabItem = document.createElement('li');
bookmarksTabItem.className = 'tab-item';
bookmarksTabItem.dataset.tab = 'bookmarks';
bookmarksTabItem.innerHTML = '<i class="fa fa-bars handle"></i> Bookmarks';
tabList.appendChild(bookmarksTabItem);
}
// Only create continue reading tab item if it exists in the actual menu and not in the DOM
const continueReadingInMenu = Array.from(menu.querySelectorAll('li')).some(li => {
const link = li.querySelector('a');
return link && link.getAttribute('href').includes('/continue_reading/');
});
if (continueReadingInMenu && continueReadingExists) {
const continueReadingTabItem = document.createElement('li');
continueReadingTabItem.className = 'tab-item';
continueReadingTabItem.dataset.tab = 'continue_reading';
continueReadingTabItem.innerHTML = '<i class="fa fa-bars handle"></i> Continue Reading';
tabList.appendChild(continueReadingTabItem);
}
// Only create settings tab item if it exists in the actual menu and not in the DOM
const settingsInMenu = Array.from(menu.querySelectorAll('li')).some(li => {
const link = li.querySelector('a');
return link && link.getAttribute('href').includes('/settings/');
});
if (settingsInMenu && settingsExists) {
const settingsTabItem = document.createElement('li');
settingsTabItem.className = 'tab-item';
settingsTabItem.dataset.tab = 'settings';
settingsTabItem.innerHTML = '<i class="fa fa-bars handle"></i> Settings';
tabList.appendChild(settingsTabItem);
}
// Check if we need to add the offline favorites tab (only if user is not logged in)
const isLoggedIn = !document.querySelector('.menu-sign-in');
const offlineFavoritesExists = tabOrder.includes('offline_favorites') && !tabList.querySelector('[data-tab="offline_favorites"]');
if (offlineFavoritesExists && !isLoggedIn) {
const offlineFavoritesTabItem = document.createElement('li');
offlineFavoritesTabItem.className = 'tab-item';
offlineFavoritesTabItem.dataset.tab = 'offline_favorites';
offlineFavoritesTabItem.innerHTML = '<i class="fa fa-bars handle"></i> Offline Favorites';
tabList.appendChild(offlineFavoritesTabItem);
}
}
// Now reorder all tabs according to the saved order
tabOrder.forEach(tabId => {
const item = tabList.querySelector(`[data-tab="${tabId}"]`);
if (item) tabList.appendChild(item);
});
});
// Check for dynamically added menu items and add them to the tab list
function checkForDynamicItems() {
const menu = document.querySelector('ul.menu.left');
if (!menu) return;
// Check for Bookmarks
const bookmarksItem = Array.from(menu.querySelectorAll('li')).find(li => {
const link = li.querySelector('a');
return link && link.getAttribute('href').includes('/bookmarks/');
});
if (bookmarksItem && !tabList.querySelector('[data-tab="bookmarks"]')) {
const bookmarksTabItem = document.createElement('li');
bookmarksTabItem.className = 'tab-item';
bookmarksTabItem.dataset.tab = 'bookmarks';
bookmarksTabItem.innerHTML = '<i class="fa fa-bars handle"></i> Bookmarks';
tabList.appendChild(bookmarksTabItem);
// Reapply the saved order after adding a new item
initializeTabOrder().then(tabOrder => {
tabOrder.forEach(tabId => {
const item = tabList.querySelector(`[data-tab="${tabId}"]`);
if (item) tabList.appendChild(item);
});
});
}
// Check for Continue Reading
const continueReadingItem = Array.from(menu.querySelectorAll('li')).find(li => {
const link = li.querySelector('a');
return link && link.getAttribute('href').includes('/continue_reading/');
});
if (continueReadingItem && !tabList.querySelector('[data-tab="continue_reading"]')) {
const continueReadingTabItem = document.createElement('li');
continueReadingTabItem.className = 'tab-item';
continueReadingTabItem.dataset.tab = 'continue_reading';
continueReadingTabItem.innerHTML = '<i class="fa fa-bars handle"></i> Continue Reading';
tabList.appendChild(continueReadingTabItem);
// Reapply the saved order after adding a new item
initializeTabOrder().then(tabOrder => {
tabOrder.forEach(tabId => {
const item = tabList.querySelector(`[data-tab="${tabId}"]`);
if (item) tabList.appendChild(item);
});
});
}
// Check for Info
const infoItem = Array.from(menu.querySelectorAll('li')).find(li => {
const link = li.querySelector('a');
return link && link.getAttribute('href').includes('/info/');
});
if (infoItem && !tabList.querySelector('[data-tab="info"]')) {
const infoTabItem = document.createElement('li');
infoTabItem.className = 'tab-item';
infoTabItem.dataset.tab = 'info';
infoTabItem.innerHTML = '<i class="fa fa-bars handle"></i> Info';
tabList.appendChild(infoTabItem);
// Reapply the saved order after adding a new item
initializeTabOrder().then(tabOrder => {
tabOrder.forEach(tabId => {
const item = tabList.querySelector(`[data-tab="${tabId}"]`);
if (item) tabList.appendChild(item);
});
});
}
// Check for Twitter
const twitterItem = Array.from(menu.querySelectorAll('li')).find(li => {
const link = li.querySelector('a');
return link && link.getAttribute('href').includes('twitter.com/nhentaiOfficial');
});
if (twitterItem && !tabList.querySelector('[data-tab="twitter"]')) {
const twitterTabItem = document.createElement('li');
twitterTabItem.className = 'tab-item';
twitterTabItem.dataset.tab = 'twitter';
twitterTabItem.innerHTML = '<i class="fa fa-bars handle"></i> Twitter';
tabList.appendChild(twitterTabItem);
// Reapply the saved order after adding a new item
initializeTabOrder().then(tabOrder => {
tabOrder.forEach(tabId => {
const item = tabList.querySelector(`[data-tab="${tabId}"]`);
if (item) tabList.appendChild(item);
});
});
}
// Check for Settings
const settingsItem = Array.from(menu.querySelectorAll('li')).find(li => {
const link = li.querySelector('a');
return link && link.getAttribute('href').includes('/settings/');
});
if (settingsItem && !tabList.querySelector('[data-tab="settings"]')) {
const settingsTabItem = document.createElement('li');
settingsTabItem.className = 'tab-item';
settingsTabItem.dataset.tab = 'settings';
settingsTabItem.innerHTML = '<i class="fa fa-bars handle"></i> Settings';
tabList.appendChild(settingsTabItem);
// Reapply the saved order after adding a new item
initializeTabOrder().then(tabOrder => {
tabOrder.forEach(tabId => {
const item = tabList.querySelector(`[data-tab="${tabId}"]`);
if (item) tabList.appendChild(item);
});
});
}
// Check for Offline Favorites
const offlineFavoritesItem = Array.from(menu.querySelectorAll('li')).find(li => {
const link = li.querySelector('a');
return link && link.getAttribute('href').includes('/favorite/');
});
if (offlineFavoritesItem && !tabList.querySelector('[data-tab="offline_favorites"]')) {
// Check if user is logged in
const isLoggedIn = !document.querySelector('.menu-sign-in');
// Only add the offline favorites tab if the user is not logged in
if (!isLoggedIn) {
const offlineFavoritesTabItem = document.createElement('li');
offlineFavoritesTabItem.className = 'tab-item';
offlineFavoritesTabItem.dataset.tab = 'offline_favorites';
offlineFavoritesTabItem.innerHTML = '<i class="fa fa-bars handle"></i> Offline Favorites';
tabList.appendChild(offlineFavoritesTabItem);
// Reapply the saved order after adding a new item
initializeTabOrder().then(tabOrder => {
tabOrder.forEach(tabId => {
const item = tabList.querySelector(`[data-tab="${tabId}"]`);
if (item) tabList.appendChild(item);
});
});
}
}
// Check for Read Manga
const readMangaItem = Array.from(menu.querySelectorAll('li')).find(li => {
const link = li.querySelector('a');
return link && link.getAttribute('href').includes('/read-manga/');
});
if (readMangaItem && !tabList.querySelector('[data-tab="read_manga"]')) {
const readMangaTabItem = document.createElement('li');
readMangaTabItem.className = 'tab-item';
readMangaTabItem.dataset.tab = 'read_manga';
readMangaTabItem.innerHTML = '<i class="fa fa-bars handle"></i> Read Manga';
tabList.appendChild(readMangaTabItem);
// Reapply the saved order after adding a new item
initializeTabOrder().then(tabOrder => {
tabOrder.forEach(tabId => {
const item = tabList.querySelector(`[data-tab="${tabId}"]`);
if (item) tabList.appendChild(item);
});
});
}
}
// Check for dynamic items initially and then every second
checkForDynamicItems();
setInterval(checkForDynamicItems, 1000);
new Sortable(tabList, {
animation: 150,
handle: '.handle',
ghostClass: 'sortable-ghost',
dragClass: 'sortable-drag',
forceFallback: true,
fallbackTolerance: 1,
delayOnTouchOnly: false,
delay: 0,
touchStartThreshold: 1,
preventTextSelection: true,
onStart: function(evt) {
evt.item.classList.add('dragging');
document.body.style.userSelect = 'none';
document.body.style.webkitUserSelect = 'none';
document.body.style.mozUserSelect = 'none';
document.body.style.msUserSelect = 'none';
},
onEnd: async function(evt) {
evt.item.classList.remove('dragging');
document.body.style.userSelect = '';
document.body.style.webkitUserSelect = '';
document.body.style.mozUserSelect = '';
document.body.style.msUserSelect = '';
const newOrder = Array.from(tabList.children).map(item => item.dataset.tab);
await GM.setValue('tabOrder', newOrder);
updateMenuOrder();
}
});
// Add mouse event listeners to improve drag handle feedback
tabList.querySelectorAll('.handle').forEach(handle => {
handle.addEventListener('mousedown', () => {
handle.style.cursor = 'grabbing';
});
handle.addEventListener('mouseup', () => {
handle.style.cursor = 'grab';
});
});
// Reset button handler
document.getElementById('resetTabOrder').addEventListener('click', async function() {
const defaultOrder = ['random', 'tags', 'artists', 'characters', 'parodies', 'groups', 'info', 'twitter', 'bookmarks', 'offline_favorites', 'continue_reading', 'settings'];
await GM.setValue('tabOrder', defaultOrder);
showPopup('Tab order reset!', {timeout: 1000});
// Reset visual order in settings
const tabList = document.getElementById('tab-list');
defaultOrder.forEach(tabId => {
const item = tabList.querySelector(`[data-tab="${tabId}"]`);
if (item) tabList.appendChild(item);
});
updateMenuOrder();
});
}
// Function to check if the menu is in the correct order
async function isMenuInOrder() {
// console.log("Checking if menu is in order...");
const menu = document.querySelector('ul.menu.left');
if (!menu) return false;
// console.log("Menu:", menu);
const tabOrder = await initializeTabOrder(); // Wait for the promise to resolve
// console.log("Tab order:", tabOrder);
// Get all menu items except dropdown in their DOM order
const allMenuItems = Array.from(menu.querySelectorAll('li:not(.dropdown)'));
// console.log("All menu items:", allMenuItems);
// Create a map of tab IDs to their desired position
const tabPositions = {};
tabOrder.forEach((tabId, index) => {
tabPositions[tabId] = index;
});
// Extract the tab IDs from the DOM in order
const currentTabOrder = [];
for (const menuItem of allMenuItems) {
const link = menuItem.querySelector('a');
if (link) {
const href = link.getAttribute('href');
// Special case for Twitter which is an external link
if (href.includes('twitter.com/nhentaiOfficial')) {
currentTabOrder.push('twitter');
continue;
}
// Special case for Offline Favorites
if (href.includes('/favorite/')) {
currentTabOrder.push('offline_favorites');
continue;
}
// Extract the tab ID from the href for internal links
const match = href.match(/\/([^\/]+)\//);
if (match && match[1]) {
currentTabOrder.push(match[1]);
}
}
}
// console.log("Current tab order from DOM:", currentTabOrder);
// console.log("Desired tab order:", tabOrder);
// Get the tabs that are actually present in the menu
const presentTabs = tabOrder.filter(tabId => currentTabOrder.includes(tabId));
// If no tabs from the order are present, consider it in order to avoid constant updates
if (presentTabs.length === 0) {
return true;
}
// Check if the offline favorites tab is the only one missing
const missingTabs = tabOrder.filter(tabId => !currentTabOrder.includes(tabId));
if (missingTabs.length === 1 && missingTabs[0] === 'offline_favorites') {
// If only the offline favorites tab is missing, consider the menu in order
return true;
}
// Check if there are other important tabs missing
const importantMissingTabs = missingTabs.filter(tabId =>
tabId !== 'offline_favorites' &&
tabId !== 'twitter' &&
tabId !== 'info'
);
if (importantMissingTabs.length > 0) {
// If important tabs are missing, the menu is not in order
return false;
}
// Now check if the relative order is correct for the tabs that exist
// Skip tabs that don't exist in the current DOM
let lastFoundIndex = -1;
for (const tabId of tabOrder) {
const currentIndex = currentTabOrder.indexOf(tabId);
if (currentIndex !== -1) {
// If this tab exists in the DOM, it should come after the last found tab
if (currentIndex < lastFoundIndex) {
console.log(`Tab ${tabId} is out of order: found at ${currentIndex}, should be after ${lastFoundIndex}`);
return false;
}
lastFoundIndex = currentIndex;
}
}
// If we get here, all existing tabs are in the correct relative order
// console.log("Menu is in correct order");
return true;
}
// Call updateMenuOrder only when the menu is not in the correct order
// Use a longer interval to reduce constant updates
setInterval(async () => {
if (!await isMenuInOrder()) {
updateMenuOrder();
}
}, 1000);
//------------------------------------------------ Advanced Settings Management Functions---------------------------------------------------------
function showPopup(message, options = {}) {
// Default options
const defaultOptions = {
timeout: 3000, // Default timeout of 3 seconds
width: '250px', // Default width
buttons: [], // Additional buttons besides close
closeButton: true, // Show close button
autoClose: true // Auto close after timeout
};
// Merge default options with provided options
const settings = { ...defaultOptions, ...options };
// Create popup element
const popup = document.createElement('div');
popup.id = 'popup';
// Create buttons HTML if provided
let buttonsHTML = '';
if (settings.buttons && settings.buttons.length > 0) {
buttonsHTML = '<div class="popup-buttons">';
settings.buttons.forEach(button => {
buttonsHTML += `<button class="popup-btn" data-action="${button.action || ''}">${button.text}</button>`;
});
buttonsHTML += '</div>';
}
// Create close button HTML if enabled
const closeButtonHTML = settings.closeButton ?
'<button class="close-btn">×</button>' : '';
// Populate popup HTML
popup.innerHTML = `
<div class="popup-content">
${closeButtonHTML}
<p>${message}</p>
${buttonsHTML}
</div>
`;
document.body.appendChild(popup);
// Add CSS styling for the popup
const style = document.createElement('style');
style.textContent = `
#popup {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.9);
color: #fff;
border-radius: 5px;
z-index: 9999;
padding: 15px;
width: ${settings.width};
text-align: center;
}
.popup-content {
position: relative;
padding: 10px;
}
.close-btn {
position: absolute;
top: 5px;
right: 10px;
background: none;
border: none;
color: #fff;
font-size: 18px;
cursor: pointer;
transition: color 0.3s, opacity 0.3s;
}
.close-btn:hover {
color: #ff0000;
opacity: 0.7;
}
.popup-buttons {
margin-top: 15px;
display: flex;
justify-content: center;
gap: 10px;
}
.popup-btn {
background: #333;
color: #fff;
border: 1px solid #555;
border-radius: 3px;
padding: 5px 10px;
cursor: pointer;
transition: background 0.3s;
}
.popup-btn:hover {
background: #444;
}
`;
document.head.appendChild(style);
// Function to close the popup
const closePopup = () => {
if (document.body.contains(popup)) {
document.body.removeChild(popup);
document.head.removeChild(style);
}
};
// Close button event listener
if (settings.closeButton) {
const closeBtn = popup.querySelector('.close-btn');
if (closeBtn) {
closeBtn.addEventListener('click', closePopup);
}
}
// Add event listeners for custom buttons
if (settings.buttons && settings.buttons.length > 0) {
const buttons = popup.querySelectorAll('.popup-btn');
buttons.forEach((btn, index) => {
btn.addEventListener('click', (e) => {
// Execute the callback if provided
if (settings.buttons[index].callback && typeof settings.buttons[index].callback === 'function') {
settings.buttons[index].callback(e);
}
// Close the popup after button click if closeOnClick is true
if (settings.buttons[index].closeOnClick !== false) {
closePopup();
}
});
});
}
// Auto-close the popup after the specified timeout
let timeoutId;
if (settings.autoClose && settings.timeout > 0) {
timeoutId = setTimeout(closePopup, settings.timeout);
}
// Return an object with methods to control the popup
return {
close: closePopup,
updateMessage: (newMessage) => {
const messageElement = popup.querySelector('p');
if (messageElement) {
messageElement.innerHTML = newMessage;
}
},
resetTimeout: () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
if (settings.autoClose && settings.timeout > 0) {
timeoutId = setTimeout(closePopup, settings.timeout);
}
}
};
}
function exportBookmarkedPages() {
GM.getValue('bookmarkedPages', []).then(bookmarkedPages => {
const blob = new Blob([JSON.stringify(bookmarkedPages, null, 2)], { type: 'application/json' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'bookmarked_pages.json';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
}
//----------------------------**Settings**--------------------------------------------
//----------------------------**Random Hentai Preferences**----------------------------
// Intercept random button clicks only if preferences are set
document.addEventListener('click', async function(event) {
const target = event.target;
if (target.tagName === 'A' && target.getAttribute('href') === '/random/') {
event.preventDefault(); // Prevent the default navigation
// Show the loading popup immediately
showLoadingPopup();
// Check if user preferences are set
const preferencesSet = await arePreferencesSet();
if (preferencesSet) {
// Set a flag to stop the search if needed
window.searchInProgress = true;
fetchRandomHentai();
} else {
// Close the popup and proceed with the default action
hideLoadingPopup();
window.location.href = '/random/';
}
}
});
async function arePreferencesSet() {
try {
const language = await GM.getValue('randomPrefLanguage', '');
const tags = await GM.getValue('randomPrefTags', []);
const pagesMin = parseInt(await GM.getValue('randomPrefPagesMin', ''), 10);
const pagesMax = parseInt(await GM.getValue('randomPrefPagesMax', ''), 10);
return language || tags.length > 0 || !isNaN(pagesMin) || !isNaN(pagesMax);
} catch (error) {
console.error('Error checking preferences:', error);
return false;
}
}
function showLoadingPopup() {
if (window.searchInProgress) {
showPopup('Already searching for random content!');
return;
}
// Create and display the popup
const popup = document.createElement('div');
popup.id = 'loading-popup';
popup.style.position = 'fixed';
popup.style.top = '50%';
popup.style.left = '50%';
popup.style.transform = 'translate(-50%, -50%)';
popup.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
popup.style.color = 'white';
popup.style.padding = '20px';
popup.style.borderRadius = '8px';
popup.style.zIndex = '9999';
popup.style.display = 'flex';
popup.style.flexDirection = 'column';
popup.style.alignItems = 'center';
popup.style.justifyContent = 'center';
// Popup content with image container and buttons
popup.innerHTML = `
<span>Searching for random content...</span>
<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;">
<a id="cover-preview-link" href="#" style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; text-decoration: none;">
<img id="cover-preview" style="max-width: 100%; max-height: 100%; object-fit: contain; display: none; cursor: pointer;" />
</a>
</div>
<div id="preview-notes" style="margin-top: 10px; color: white; text-align: center;">
<!-- Notes will be inserted here -->
</div>
<div style="margin-top: 20px; display: flex; gap: 15px;">
<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;">
<i class="fas fa-arrow-left"></i>
</button>
<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;">
<i class="fas fa-pause"></i>
</button>
<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;">
<i class="fas fa-arrow-right"></i>
</button>
</div>
<button class="close" style="margin-top: 20px; background: none; border: none; font-size: 24px; color: white; cursor: pointer;">×</button>
`;
document.body.appendChild(popup);
// Add event listener to close button
const closeButton = popup.querySelector('.close');
closeButton.addEventListener('click', function() {
hideLoadingPopup();
window.searchInProgress = false; // Stop the search
});
// Add hover effect for the close button
closeButton.addEventListener('mouseenter', function() {
closeButton.style.color = 'red';
closeButton.style.opacity = '0.7';
});
closeButton.addEventListener('mouseleave', function() {
closeButton.style.color = 'white';
closeButton.style.opacity = '1';
});
// Add hover effect for control buttons
const controlButtons = document.querySelectorAll('.control-button');
controlButtons.forEach(button => {
button.addEventListener('mouseenter', function() {
button.style.color = '#ddd'; // Light color on hover
button.style.transform = 'scale(1.1)'; // Slightly enlarge button
});
button.addEventListener('mouseleave', function() {
button.style.color = 'white'; // Original color
button.style.transform = 'scale(1)'; // Return to original size
});
});
// Add event listeners for control buttons
document.getElementById('previous-image').addEventListener('click', showPreviousImage);
document.getElementById('pause-search').addEventListener('click', togglePause);
document.getElementById('next-image').addEventListener('click', showNextImage);
// Add click event listener to the preview image to navigate to the content URL
document.getElementById('cover-preview').addEventListener('click', function() {
const currentImageIndex = parseInt(localStorage.getItem('currentImageIndex') || '0', 10);
const images = getImagesFromLocalStorage();
if (images[currentImageIndex] && images[currentImageIndex].url) {
window.location.href = images[currentImageIndex].url;
}
});
}
function hideLoadingPopup() {
const popup = document.getElementById('loading-popup');
if (popup) {
document.body.removeChild(popup);
}
}
async function fetchRandomHentai() {
try {
if (!window.searchInProgress) return; // Stop if search was canceled
const response = await fetch('https://nhentai.net/random/', { method: 'HEAD' });
await analyzeURL(response.url);
} catch (error) {
console.error('Error fetching random URL:', error);
}
}
async function analyzeURL(url) {
try {
if (!window.searchInProgress) {
return; // Stop if search was canceled
}
const response = await fetch(url);
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const coverImage = doc.querySelector('#cover img.lazyload');
const coverImageUrl = coverImage ? (coverImage.getAttribute('data-src') || coverImage.src) : null;
const title = doc.querySelector('#info h1')?.textContent.trim();
const tags = Array.from(doc.querySelectorAll('#tags .tag')).map(tag => tag.textContent.trim());
const pages = parseInt(doc.querySelector('#tags .tag-container:nth-last-child(2) .name')?.textContent.trim(), 10);
const uploadDate = doc.querySelector('#tags .tag-container:last-child time')?.getAttribute('datetime');
// Extract and handle languages
let languages = [];
const tagContainers = doc.querySelectorAll('.tag-container.field-name');
tagContainers.forEach(container => {
if (container.textContent.includes('Languages:')) {
const languageElements = container.querySelectorAll('.tags .tag .name');
languageElements.forEach(languageElement => {
let language = languageElement.textContent.trim().toLowerCase();
languages.push(language);
});
}
});
// Determine which language to display
let languageDisplay = 'Unknown';
if (languages.includes('english')) {
languageDisplay = 'English';
} else if (languages.includes('translated') && languages.length === 1) {
languageDisplay = 'English';
} else if (languages.includes('translated') && languages.length > 1) {
// Exclude 'translated' and show other language(s)
const otherLanguages = languages.filter(lang => lang !== 'translated');
languageDisplay = otherLanguages.length > 0 ? otherLanguages.map(lang => lang.charAt(0).toUpperCase() + lang.slice(1)).join(', ') : 'Unknown';
} else {
languageDisplay = languages.map(lang => lang.charAt(0).toUpperCase() + lang.slice(1)).join(', ');
}
if (coverImageUrl) {
saveImageToLocalStorage(coverImageUrl, url, languageDisplay, pages, title);
showPreviousImage();
}
if (await meetsUserPreferences(tags, pages)) {
hideLoadingPopup();
window.location.href = url;
} else {
fetchRandomHentai();
}
} catch (error) {
console.error('Error analyzing page:', error);
}
}
async function meetsUserPreferences(tags, pages) {
try {
const preferredLanguage = (await GM.getValue('randomPrefLanguage', '')).toLowerCase();
const preferredTags = (await GM.getValue('randomPrefTags', [])).map(tag => tag.toLowerCase());
const blacklistedTags = (await GM.getValue('blacklistedTags', [])).map(tag => tag.toLowerCase()).filter(tag => tag !== '');
const preferredPagesMin = parseInt(await GM.getValue('randomPrefPagesMin', ''), 10);
const preferredPagesMax = parseInt(await GM.getValue('randomPrefPagesMax', ''), 10);
const matchAllTags = await GM.getValue('matchAllTags', true);
// Strip tag counts and only keep the tag names
const cleanedTags = tags.map(tag => tag.replace(/\d+K?$/, '').trim().toLowerCase());
const hasPreferredLanguage = preferredLanguage ? cleanedTags.includes(preferredLanguage) : true;
let hasPreferredTags;
if (preferredTags.length > 0) {
if (matchAllTags) {
hasPreferredTags = preferredTags.every(tag => cleanedTags.includes(tag));
} else {
hasPreferredTags = preferredTags.some(tag => cleanedTags.includes(tag));
}
} else {
hasPreferredTags = true;
}
const withinPageRange = (!isNaN(preferredPagesMin) ? pages >= preferredPagesMin : true) &&
(!isNaN(preferredPagesMax) ? pages <= preferredPagesMax : true);
const hasBlacklistedTags = blacklistedTags.some(tag => cleanedTags.includes(tag));
const meetsPreferences = hasPreferredLanguage && hasPreferredTags && withinPageRange && !hasBlacklistedTags;
return meetsPreferences;
} catch (error) {
console.error('Error checking user preferences:', error);
return false;
}
}
function saveImageToLocalStorage(imageUrl, hentaiUrl, language, pages, title) {
let images = JSON.parse(localStorage.getItem('hentaiImages') || '[]');
images.unshift({ imageUrl, url: hentaiUrl, language, pages, title }); // Add title to stored data
if (images.length > 10) {
images.pop();
}
localStorage.setItem('hentaiImages', JSON.stringify(images));
localStorage.setItem('currentImageIndex', '0');
updatePreviewImage(imageUrl, language, pages, title);
}
function getImagesFromLocalStorage() {
return JSON.parse(localStorage.getItem('hentaiImages') || '[]');
}
function showNextImage() {
const images = getImagesFromLocalStorage();
if (images.length === 0) return;
let currentIndex = parseInt(localStorage.getItem('currentImageIndex') || '0', 10);
currentIndex = (currentIndex - 1 + images.length) % images.length;
localStorage.setItem('currentImageIndex', currentIndex.toString());
const currentImage = images[currentIndex];
updatePreviewImage(currentImage.imageUrl, currentImage.language, currentImage.pages, currentImage.title);
}
function showPreviousImage() {
const images = getImagesFromLocalStorage();
if (images.length === 0) return;
let currentIndex = parseInt(localStorage.getItem('currentImageIndex') || '0', 10);
currentIndex = (currentIndex + 1) % images.length;
localStorage.setItem('currentImageIndex', currentIndex.toString());
const currentImage = images[currentIndex];
updatePreviewImage(currentImage.imageUrl, currentImage.language, currentImage.pages, currentImage.title);
}
function updatePreviewImage(imageUrl, language = '', pages = '', title = '') {
const coverPreview = document.getElementById('cover-preview');
const coverPreviewLink = document.getElementById('cover-preview-link');
const notesContainer = document.getElementById('preview-notes');
const isPaused = !window.searchInProgress;
if (coverPreview) {
coverPreview.src = imageUrl;
coverPreview.style.display = 'block';
}
// Update the link URL
if (coverPreviewLink) {
const images = getImagesFromLocalStorage();
const currentIndex = parseInt(localStorage.getItem('currentImageIndex') || '0', 10);
if (images[currentIndex] && images[currentIndex].url) {
coverPreviewLink.href = images[currentIndex].url;
}
}
if (notesContainer) {
notesContainer.innerHTML = `
${isPaused ? `<div style="margin-bottom: 5px;"><span style="font-weight: bold;">Title:</span> ${title || 'Title Not Available'}</div>` : ''}
<div>Language: ${language || 'N/A'}</div>
<div>Pages: ${pages || 'N/A'}</div>
`;
}
}
// Remove the old click event listener from the image and add it to the link instead (Not necessary may remove later)
document.addEventListener('DOMContentLoaded', function() {
const coverPreviewLink = document.getElementById('cover-preview-link');
if (coverPreviewLink) {
coverPreviewLink.addEventListener('click', function(event) {
event.preventDefault();
const currentImageIndex = parseInt(localStorage.getItem('currentImageIndex') || '0', 10);
const images = getImagesFromLocalStorage();
if (images[currentImageIndex] && images[currentImageIndex].url) {
window.location.href = images[currentImageIndex].url;
}
});
}
});
function togglePause() {
window.searchInProgress = !window.searchInProgress;
const pauseButtonIcon = document.querySelector('#pause-search i');
pauseButtonIcon.className = window.searchInProgress ? 'fas fa-pause' : 'fas fa-play';
// Update the current image display with the new pause state
const images = getImagesFromLocalStorage();
const currentIndex = parseInt(localStorage.getItem('currentImageIndex') || '0', 10);
if (images[currentIndex]) {
const currentImage = images[currentIndex];
updatePreviewImage(currentImage.imageUrl, currentImage.language, currentImage.pages, currentImage.title);
}
if (window.searchInProgress) {
fetchRandomHentai();
}
}
// Initialize the current image index
localStorage.setItem('currentImageIndex', '0');
//----------------------------**Random Hentai Preferences**----------------------------
//---------------------------**Open In New Tab Button**---------------------------------
// Add this code after the existing findVersionButton code in the same section
async function addNewTabButtons() {
// Check if the feature is enabled
const openInNewTabEnabled = await GM.getValue('openInNewTabEnabled', true);
if (!openInNewTabEnabled) return;
const openInNewTabType = await GM.getValue('openInNewTabType', 'background');
const baseUrl = 'https://nhentai.net';
const covers = document.querySelectorAll('.cover');
covers.forEach(cover => {
// Check if the button doesn't already exist for this cover
if (!cover.querySelector('.newTabButton')) {
const newTabButton = document.createElement('div');
newTabButton.className = 'newTabButton';
newTabButton.innerHTML = '<i class="fas fa-external-link-alt"></i>'; // Updated to include icon
// Add click event listener
newTabButton.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation(); // Prevent the click from bubbling up to the cover
// Get the href from the cover
const mangaUrl = cover.getAttribute('href');
console.log('Opening manga URL:', mangaUrl); // Debugging log
if (mangaUrl) {
const fullUrl = baseUrl + mangaUrl; // Construct the full URL
if (openInNewTabType === 'foreground') {
console.log("foreground");
window.open(fullUrl, '_blank'); // Open in new tab and focus on it
} else {
console.log("background");
GM.openInTab(fullUrl, { active: false }); // Open in new tab without focusing on it
}
}else {
console.error('No URL found for this cover.'); // Error log if no URL
}
});
cover.appendChild(newTabButton);
}
});
}
// Add observer to handle dynamically loaded content
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.addedNodes.length) {
addNewTabButtons();
}
});
});
// Start observing the document with the configured parameters
observer.observe(document.body, { childList: true, subtree: true });
// Initial call to add buttons to existing covers
addNewTabButtons();
//---------------------------**Open In New Tab Button**---------------------------------
//----------------------------**Manga BookMark**---------------------------------
function mangaBookmarking() {
GM.addStyle(`
@keyframes bookmark-pulse {
0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(237, 37, 83, 0.7); }
70% { transform: scale(1.05); box-shadow: 0 0 5px 10px rgba(237, 37, 83, 0); }
100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(237, 37, 83, 0); }
}
.bookmark-animation {
animation: bookmark-pulse 0.6s ease-out;
}
`);
// Get the download button
const downloadButton = document.getElementById('download');
if (!downloadButton) {
console.log('Download button not found.');
return;
}
// Check if the manga bookmarking button is enabled in settings
async function getMangaBookMarkingButtonEnabled() {
return await GM.getValue('mangaBookMarkingButtonEnabled', true);
}
getMangaBookMarkingButtonEnabled().then(mangaBookMarkingButtonEnabled => {
if (!mangaBookMarkingButtonEnabled) return;
// Get the current URL
const currentUrl = window.location.href;
// Check if the current manga is already bookmarked
async function getBookmarkedMangas() {
try {
const bookmarkedMangas = await GM.getValue('bookmarkedMangas', []);
return bookmarkedMangas;
} catch (error) {
console.error('Error checking bookmarks:', error);
return [];
}
}
getBookmarkedMangas().then(bookmarkedMangas => {
let bookmarkText = 'Bookmark';
let bookmarkClass = 'btn-enabled';
if (bookmarkedMangas.some(manga => manga.url === currentUrl)) {
bookmarkText = 'Bookmarked';
bookmarkClass = 'btn-disabled';
}
const MangaBookMarkHtml = `
<a class="btn btn-primary ${bookmarkClass} tooltip bookmark" id="bookmark-button">
<i class="fas fa-bookmark"></i>
<span>${bookmarkText}</span>
<div class="top">Click to save this manga for later<i></i></div>
</a>
`;
// Insert 'Find Similar' button next to the download button
$(downloadButton).after(MangaBookMarkHtml);
// Add event listener to the bookmark button
document.getElementById('bookmark-button').addEventListener('click', async function() {
// Get the current URL
const currentUrl = window.location.href;
// Get the cover image URL
const coverImageContainer = document.getElementById('cover');
const coverImage = coverImageContainer.querySelector('img');
const coverImageUrl = coverImage.dataset.src || coverImage.src;
try {
// Get the bookmarked mangas (asynchronously)
const bookmarkedMangas = await GM.getValue('bookmarkedMangas', []);
const existingManga = bookmarkedMangas.find(manga => manga.url === currentUrl);
if (existingManga) {
// If already bookmarked, remove it
const index = bookmarkedMangas.indexOf(existingManga);
bookmarkedMangas.splice(index, 1);
this.querySelector('span').textContent = 'Bookmark';
this.classList.remove('btn-disabled');
this.classList.add('btn-enabled');
} else {
// If not bookmarked, add it
bookmarkedMangas.push({
url: currentUrl,
coverImageUrl: coverImageUrl
});
this.querySelector('span').textContent = 'Bookmarked';
this.classList.remove('btn-enabled');
this.classList.add('btn-disabled');
}
const isNewlyBookmarked = !existingManga; // True if we just added a bookmark
// Save the updated list (asynchronously)
await GM.setValue('bookmarkedMangas', bookmarkedMangas);
if (isNewlyBookmarked) { // If a new bookmark was added
this.classList.add('bookmark-animation');
setTimeout(() => {
this.classList.remove('bookmark-animation');
}, 600); // Animation duration 0.6s
}
} catch (error) {
console.error('Error handling bookmarks:', error);
// Optionally display an error to the user
alert('An error occurred while saving your bookmark.');
}
});
});
});
}
mangaBookmarking();
//----------------------------**Manga BookMark**---------------------------------
//---------------------------**Month Filter**------------------------------------
async function addMonthFilter() {
const monthFilterEnabled = await GM.getValue('monthFilterEnabled', true);
if (!monthFilterEnabled) return;
const path = window.location.pathname;
if (/^\/(search|tag|artist|character|parody)\//.test(path)) {
const sortTypes = document.getElementsByClassName("sort-type");
if (sortTypes.length > 1) {
let baseUrl = window.location.pathname;
// Remove existing popularity filter from the path if present.
baseUrl = baseUrl.replace(/\/popular(-\w+)?$/, '');
const urlParams = new URLSearchParams(window.location.search);
urlParams.delete('sort'); // Remove any sort parameter from the query string
const remainingParams = urlParams.toString();
if (remainingParams) {
baseUrl += '?' + remainingParams;
}
const monthFilterHtml = `
<span class="sort-name">Popular:</span>
<a href="${baseUrl}${baseUrl.includes('?') ? '&' : '?'}sort=popular-today">today</a>
<a href="${baseUrl}${baseUrl.includes('?') ? '&' : '?'}sort=popular-week">week</a>
<a href="${baseUrl}${baseUrl.includes('?') ? '&' : '?'}sort=popular-month">month</a>
<a href="${baseUrl}${baseUrl.includes('?') ? '&' : '?'}sort=popular">all time</a>
`;
sortTypes[1].innerHTML = monthFilterHtml;
}
}
}
addMonthFilter();
//--------------------------*Month Filter**----------------------------------------
//--------------------------- **Replace Related Manga with Bookmarks** ---------------------------
// Function to get manga details (cover image and title)
async function getMangaDetails(mangaId) {
try {
// First check if we have details cached
const cachedDetails = await GM.getValue(`manga_details_${mangaId}`, null);
if (cachedDetails) {
return cachedDetails;
}
// If not cached, fetch it from the page
const response = await fetch(`https://nhentai.net/g/${mangaId}/`);
if (!response.ok) {
console.error(`Failed to fetch manga page for ${mangaId}: ${response.status}`);
return { coverUrl: null, title: null };
}
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// Get the cover image
const coverImg = doc.querySelector("#cover > a > img");
const coverUrl = coverImg ? (coverImg.getAttribute('data-src') || coverImg.getAttribute('src')) : null;
// Get the manga title
let title = null;
// Try to get the title from the span.before element
const titleSpan = doc.querySelector("#info > h1 > a > u > span.before");
if (titleSpan) {
title = titleSpan.textContent.trim();
}
// If not found, try the main h1 title
if (!title) {
const mainTitle = doc.querySelector("#info > h1");
if (mainTitle) {
title = mainTitle.textContent.trim();
}
}
// Extract all tags from the page
const allTags = Array.from(doc.querySelectorAll('#tags span.name')).map(span =>
span.textContent.trim().toLowerCase()
);
// Determine language from tags
let language = null;
if (allTags.includes('english')) {
language = 'english';
} else if (allTags.includes('japanese')) {
language = 'japanese';
} else if (allTags.includes('chinese')) {
language = 'chinese';
}
// Cache the details
const details = { coverUrl, title, language };
await GM.setValue(`manga_details_${mangaId}`, details);
return details;
} catch (error) {
console.error(`Error getting manga details for ${mangaId}:`, error);
return { coverUrl: null, title: null };
}
}
// Function to get cover image URL for a manga (for backward compatibility)
async function getMangaCoverImage(mangaId) {
const details = await getMangaDetails(mangaId);
return details.coverUrl;
}
// Language flag URLs
const LANGUAGE_FLAGS = {
english: "https://i.imgur.com/vSnHmmi.gif",
japanese: "https://i.imgur.com/GlArpuS.gif",
chinese: "https://i.imgur.com/7B55DYm.gif"
};
// Function to replace the related manga section with bookmarked content
async function replaceRelatedWithBookmarks() {
// Check if the feature is enabled
const replaceRelatedWithBookmarks = await GM.getValue('replaceRelatedWithBookmarks', true);
if (!replaceRelatedWithBookmarks) return;
// Check if flip button is enabled
const enableRelatedFlipButton = await GM.getValue('enableRelatedFlipButton', true);
// Check if we're on a manga page and if the related container exists
const relatedContainer = document.querySelector("#related-container");
if (!relatedContainer || !window.location.pathname.includes('/g/')) return;
// Store original content for flipping back
const originalContent = relatedContainer.innerHTML;
// State management for flip functionality (only if flip button is enabled)
let isShowingBookmarks = true; // Default to showing bookmarks when enabled
let bookmarkedContent = null;
// Add a loading indicator
relatedContainer.innerHTML = `
<h2>Finding Related Manga from Your Bookmarks...</h2>
<div class="container" style="text-align: center; padding: 20px;">
<div class="loading-spinner" style="display: inline-block; width: 40px; height: 40px; border: 4px solid #f3f3f3; border-top: 4px solid #555; border-radius: 50%; animation: spin 1s linear infinite;"></div>
</div>
<style>
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
`;
// Get the current manga ID
const currentMangaId = window.location.pathname.match(/\/g\/(\d+)/)?.[1];
if (!currentMangaId) return;
// Get the current manga's tags
const tagsContainer = document.querySelector("#tags");
if (!tagsContainer) return;
// Extract all tags from the current manga
const tagElements = tagsContainer.querySelectorAll('.tag');
const currentTags = Array.from(tagElements).map(tag => {
return tag.querySelector('.name')?.textContent.trim().toLowerCase() || '';
}).filter(tag => tag !== '');
console.log('Current manga tags:', currentTags);
// Get all bookmarks
const bookmarks = await getBookmarksFromStorage();
if (!bookmarks || bookmarks.length === 0) {
console.log('No bookmarks found');
return;
}
console.log(`Found ${bookmarks.length} bookmarks`);
// Filter out the current manga from bookmarks
const filteredBookmarks = bookmarks.filter(bookmark => bookmark.id !== currentMangaId);
// Function to score bookmarks based on tag similarity
async function scoreBookmark(bookmark) {
try {
// Get manga info with tags - only use cached data
const mangaInfo = await GM.getValue(`manga_${bookmark.id}`, null);
// Try to get additional tags from the URL-based cache
const mangaUrl = `https://nhentai.net/g/${bookmark.id}/`;
const additionalTags = await GM.getValue(`tags_${mangaUrl}`, []);
// If no cached info or tags from either source, skip this bookmark
if ((!mangaInfo || !mangaInfo.tags || mangaInfo.tags.length === 0) && additionalTags.length === 0) {
return { bookmark, score: 0, tags: [], tagIds: [] };
}
// Initialize bookmarkTags array
let bookmarkTags = [];
// Add tags from mangaInfo if available
if (mangaInfo && mangaInfo.tags && mangaInfo.tags.length > 0) {
bookmarkTags = mangaInfo.tags.map(tag =>
tag.replace(/\d+K?$/, '').trim().toLowerCase()
);
}
// Add tags from URL-based cache if available
if (additionalTags.length > 0) {
// Clean up additional tags and add them to bookmarkTags
const cleanedAdditionalTags = additionalTags.map(tag =>
typeof tag === 'string' ? tag.replace(/\d+K?$/, '').trim().toLowerCase() : ''
).filter(tag => tag !== '');
// Merge tags, avoiding duplicates
bookmarkTags = [...new Set([...bookmarkTags, ...cleanedAdditionalTags])];
}
// Get tag IDs if available
const tagIds = mangaInfo?.tagIds || [];
// Calculate score based on matching tags
let score = 0;
const matchingTags = [];
for (const tag of currentTags) {
if (bookmarkTags.includes(tag)) {
score++;
matchingTags.push(tag);
}
}
// Determine language from tags
let language = null;
if (bookmarkTags.includes('english')) {
language = 'english';
} else if (bookmarkTags.includes('japanese')) {
language = 'japanese';
} else if (bookmarkTags.includes('chinese')) {
language = 'chinese';
}
return {
bookmark,
score,
tags: bookmarkTags,
matchingTags,
tagIds,
title: mangaInfo?.title || `Manga ${bookmark.id}`,
thumbnail: mangaInfo?.thumbnail || null,
language
};
} catch (error) {
console.error(`Error scoring bookmark ${bookmark.id}:`, error);
return { bookmark, score: 0, tags: [], tagIds: [] };
}
}
// Process all bookmarks since we're only using cached data
console.log(`Processing ${filteredBookmarks.length} bookmarks (using cached data only)`);
// Score bookmarks in batches to avoid freezing the browser
const BATCH_SIZE = 50; // Larger batch size since we're only using cached data
const scoredBookmarks = [];
for (let i = 0; i < filteredBookmarks.length; i += BATCH_SIZE) {
const batch = filteredBookmarks.slice(i, i + BATCH_SIZE);
console.log(`Processing batch ${Math.floor(i/BATCH_SIZE) + 1} of ${Math.ceil(filteredBookmarks.length/BATCH_SIZE)}`);
const batchPromises = batch.map(scoreBookmark);
const batchResults = await Promise.all(batchPromises);
scoredBookmarks.push(...batchResults);
// Small delay to keep the UI responsive
if (i + BATCH_SIZE < filteredBookmarks.length) {
await new Promise(resolve => setTimeout(resolve, 10));
}
}
// Sort by score (highest first)
scoredBookmarks.sort((a, b) => b.score - a.score);
// Filter out bookmarks with no matching tags
const bookmarksWithMatches = scoredBookmarks.filter(item => item.score > 0);
console.log(`Found ${bookmarksWithMatches.length} bookmarks with matching tags`);
// If no related bookmarks found with matching tags, restore the original content
if (bookmarksWithMatches.length === 0) {
console.log('No related bookmarks found with matching tags');
relatedContainer.innerHTML = originalContent;
return;
}
// Take top 5 or fewer if less available
const topBookmarks = bookmarksWithMatches.slice(0, 5);
console.log('Top related bookmarks:', topBookmarks);
// Pre-fetch titles and thumbnails for top bookmarks
await Promise.all(topBookmarks.map(async item => {
// Get manga details for title and cover
const details = await getMangaDetails(item.bookmark.id);
// Update title if needed
if ((!item.title || item.title === `Manga ${item.bookmark.id}`) && details.title) {
item.title = details.title;
}
// Update thumbnail if needed
if (!item.thumbnail && details.coverUrl) {
item.thumbnail = details.coverUrl;
}
}));
// Clear the related container
relatedContainer.innerHTML = '';
// Create a header container for the section with flip button
const headerContainer = document.createElement('div');
headerContainer.style.display = 'flex';
headerContainer.style.alignItems = 'center';
headerContainer.style.justifyContent = 'center';
headerContainer.style.gap = '10px';
headerContainer.style.position = 'relative';
const header = document.createElement('h2');
header.textContent = 'Related Manga from Your Bookmarks';
header.style.margin = '0';
header.style.textAlign = 'center';
header.style.flex = '1';
headerContainer.appendChild(header);
// Only create flip button if the setting is enabled
let flipButton = null;
if (enableRelatedFlipButton) {
// Create flip button
flipButton = document.createElement('button');
flipButton.textContent = 'Flip';
flipButton.style.cssText = `
background: #ed2553;
color: white;
border: none;
padding: 5px 10px;
border-radius: 3px;
cursor: pointer;
font-size: 12px;
font-weight: bold;
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
`;
// Add hover effect
flipButton.addEventListener('mouseenter', () => {
flipButton.style.background = '#d91e47';
});
flipButton.addEventListener('mouseleave', () => {
flipButton.style.background = '#ed2553';
});
headerContainer.appendChild(flipButton);
}
relatedContainer.appendChild(headerContainer);
// Create a container for the galleries
const galleryContainer = document.createElement('div');
galleryContainer.className = 'container';
relatedContainer.appendChild(galleryContainer);
// Store the bookmarked content for later use
const storeBookmarkedContent = () => {
bookmarkedContent = relatedContainer.innerHTML;
};
// Function to toggle between bookmarked and original content
const toggleContent = () => {
if (isShowingBookmarks) {
// Switch to original content
relatedContainer.innerHTML = originalContent;
isShowingBookmarks = false;
// Force load thumbnails in original content with comprehensive loading
setTimeout(() => {
const images = relatedContainer.querySelectorAll('img');
images.forEach((img, index) => {
// Store original attributes
const dataSrc = img.dataset.src || img.getAttribute('data-src');
const originalSrc = img.src;
// Function to attempt loading the image
const loadImage = (src) => {
return new Promise((resolve, reject) => {
const testImg = new Image();
testImg.onload = () => {
img.src = src;
img.style.opacity = '1';
resolve(src);
};
testImg.onerror = () => reject(src);
testImg.src = src;
});
};
// Try multiple loading strategies
const tryLoadImage = async () => {
const sources = [];
// Add data-src if available
if (dataSrc && dataSrc !== originalSrc) {
sources.push(dataSrc);
}
// Add original src if available and different
if (originalSrc && !originalSrc.includes('placeholder') && !originalSrc.includes('loading')) {
sources.push(originalSrc);
}
// Try each source
for (const src of sources) {
try {
await loadImage(src);
console.log(`Successfully loaded image ${index + 1}:`, src);
return;
} catch (error) {
console.log(`Failed to load image ${index + 1} from:`, src);
}
}
// If all sources fail, try to reconstruct the URL
const galleryLink = img.closest('a');
if (galleryLink && galleryLink.href) {
const mangaId = galleryLink.href.match(/\/g\/(\d+)/)?.[1];
if (mangaId) {
const reconstructedUrl = `https://t.nhentai.net/galleries/${mangaId}/thumb.jpg`;
try {
await loadImage(reconstructedUrl);
console.log(`Successfully loaded reconstructed image ${index + 1}:`, reconstructedUrl);
} catch (error) {
console.log(`Failed to load reconstructed image ${index + 1}:`, reconstructedUrl);
}
}
}
};
// Start loading process
tryLoadImage();
// Remove lazy loading classes that might prevent loading
img.classList.remove('lazyload', 'lazyloading');
img.classList.add('lazyloaded');
});
// Trigger any lazy loading libraries that might be present
if (window.lazyLoadInstance) {
window.lazyLoadInstance.update();
}
// Trigger intersection observer if present
if (window.IntersectionObserver) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
if (img.dataset.src && !img.src) {
img.src = img.dataset.src;
}
}
});
});
images.forEach(img => observer.observe(img));
// Disconnect after a short time
setTimeout(() => observer.disconnect(), 2000);
}
}, 100);
// Add a flip button to the original content to switch back (only if flip button is enabled)
const originalHeader = relatedContainer.querySelector('h2');
if (originalHeader && enableRelatedFlipButton) {
// Create container for original header with flip button
const originalHeaderContainer = document.createElement('div');
originalHeaderContainer.style.display = 'flex';
originalHeaderContainer.style.alignItems = 'center';
originalHeaderContainer.style.justifyContent = 'center';
originalHeaderContainer.style.gap = '10px';
originalHeaderContainer.style.position = 'relative';
// Style the original header
originalHeader.style.margin = '0';
originalHeader.style.textAlign = 'center';
originalHeader.style.flex = '1';
// Create flip button for original content
const originalFlipButton = document.createElement('button');
originalFlipButton.textContent = 'Flip';
originalFlipButton.style.cssText = `
background: #ed2553;
color: white;
border: none;
padding: 5px 10px;
border-radius: 3px;
cursor: pointer;
font-size: 12px;
font-weight: bold;
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
`;
// Add hover effects
originalFlipButton.addEventListener('mouseenter', () => {
originalFlipButton.style.background = '#d91e47';
});
originalFlipButton.addEventListener('mouseleave', () => {
originalFlipButton.style.background = '#ed2553';
});
// Add click event to flip back
originalFlipButton.addEventListener('click', toggleContent);
// Replace the original header with the container
originalHeader.parentNode.insertBefore(originalHeaderContainer, originalHeader);
originalHeaderContainer.appendChild(originalHeader);
originalHeaderContainer.appendChild(originalFlipButton);
}
} else {
// Switch back to bookmarked content
if (bookmarkedContent) {
relatedContainer.innerHTML = bookmarkedContent;
// Re-attach event listener to the new flip button
const newFlipButton = relatedContainer.querySelector('button');
if (newFlipButton) {
newFlipButton.addEventListener('click', toggleContent);
// Re-add hover effects
newFlipButton.addEventListener('mouseenter', () => {
newFlipButton.style.background = '#d91e47';
});
newFlipButton.addEventListener('mouseleave', () => {
newFlipButton.style.background = '#ed2553';
});
}
isShowingBookmarks = true;
}
}
};
// Add click event to flip button (only if it exists)
if (flipButton) {
flipButton.addEventListener('click', toggleContent);
}
// Add each bookmark to the container
for (const item of topBookmarks) {
const { bookmark, score, matchingTags, tagIds, title, thumbnail, language } = item;
if (score === 0) continue; // Skip bookmarks with no matching tags
try {
// Create gallery HTML directly using the format from nhentai
const tagIdsString = tagIds && tagIds.length > 0 ? tagIds.join(' ') : '';
// Calculate aspect ratio for padding (default to 141.2% if not available)
const aspectRatio = 141.2;
// Get thumbnail URL - use the cover image if available
let thumbUrl = thumbnail || null;
// If no thumbnail, try to get it from manga_info
if (!thumbUrl) {
const mangaInfo = await GM.getValue(`manga_info_${bookmark.id}`, null);
if (mangaInfo && mangaInfo.thumbnail) {
thumbUrl = mangaInfo.thumbnail;
}
}
// If still no thumbnail, try to get the cover image
if (!thumbUrl) {
thumbUrl = await getMangaCoverImage(bookmark.id);
}
// If still no thumbnail, use placeholder
if (!thumbUrl) {
thumbUrl = 'https://t.nhentai.net/galleries/0/thumb.jpg';
}
// Format the title with manga name if available
let displayTitle = title || `Manga ${bookmark.id}`;
// Create gallery HTML with centered title (no inline language flag)
const galleryHTML = `
<div class="gallery" data-tags="${tagIdsString}" data-detected-language="${language || ''}">
<a href="/g/${bookmark.id}/" class="cover" style="padding:0 0 ${aspectRatio}% 0">
<img class="lazyload" width="250" height="353" data-src="${thumbUrl}" src="${thumbUrl}">
<noscript><img src="${thumbUrl}" width="250" height="353" /></noscript>
<div class="caption" style="text-align: center;">${displayTitle}</div>
</a>
</div>
`;
// Add to container
galleryContainer.insertAdjacentHTML('beforeend', galleryHTML);
// Get the newly added gallery element
const lastGallery = galleryContainer.lastElementChild;
// Add matching tags info if available
if (matchingTags && matchingTags.length > 0) {
const caption = lastGallery.querySelector('.caption');
const tagsInfo = document.createElement('div');
tagsInfo.className = 'matching-tags';
tagsInfo.textContent = `Matching tags: ${matchingTags.join(', ')}`;
tagsInfo.style.fontSize = '12px';
tagsInfo.style.color = '#888';
tagsInfo.style.marginTop = '5px';
tagsInfo.style.textAlign = 'center';
caption.appendChild(tagsInfo);
}
// Add language flag using the overlay system if language is available
if (language && lastGallery) {
// Check if we have access to the language flag system
if (typeof window.nhentaiPlus !== 'undefined' && window.nhentaiPlus.systems && window.nhentaiPlus.systems.languageDetection) {
window.nhentaiPlus.systems.languageDetection.addLanguageFlag(lastGallery, language);
}
}
} catch (error) {
console.error(`Error creating gallery item for bookmark ${bookmark.id}:`, error);
}
}
// Store the bookmarked content after all galleries are added
storeBookmarkedContent();
}
// Call the function when the page is loaded
$(document).ready(function() {
setTimeout(replaceRelatedWithBookmarks, 1000); // Delay to ensure page is fully loaded
});
//---------------------------**BookMark-Random-Button**-----------------------------
async function appendButton() {
const enableRandomButton = await GM.getValue('enableRandomButton', true);
if (!enableRandomButton) return;
// Check if we're on the bookmarks page
if (window.location.pathname.includes('/bookmarks')) {
// Pre-fetch the bookmarks outside the observer
const bookmarks = await getBookmarksFromStorage();
// Create a function to check for the element and append the button
function checkAndAppendButton() {
const target = document.querySelector("#bookmarksContainer > h2:nth-child(1)");
if (target) {
// Append the button
const button = $('<button class="random-button"><i class="fas fa-random"></i> Random</button>');
$(target).after(button);
$(target).css('display', 'inline-block');
button.css({
'display': 'inline-block',
'margin-left': '10px',
'position': 'relative',
'top': '-3px'
});
button.on('click', async () => {
if (bookmarks.length > 0) {
const randomIndex = Math.floor(Math.random() * bookmarks.length);
const randomBookmark = bookmarks[randomIndex];
const link = `https://nhentai.net/g/${randomBookmark.id}`;
// Store bookmark info in localStorage for the next page
localStorage.setItem('randomMangaSource', JSON.stringify({
source: randomBookmark.source,
id: randomBookmark.id
}));
// Get the openInNewTabType value from storage
const openInNewTabType = await GM.getValue('openInNewTabType', 'new-tab');
const enableRandomButton = await GM.getValue('enableRandomButton', true);
const randomOpenType = await GM.getValue('randomOpenType', 'new-tab');
// Determine how to open the link based on the openInNewTabType value
if (enableRandomButton && randomOpenType === 'new-tab') {
// Open the link in a new tab
window.open(link, '_blank');
} else if (enableRandomButton && randomOpenType === 'current-tab') {
// Open the link in the current tab
window.location.href = link;
} else if (openInNewTabType === 'new-tab') {
// Open the link in a new tab
window.open(link, '_blank');
} else if (openInNewTabType === 'current-tab') {
// Open the link in the current tab
window.location.href = link;
}
} else {
showPopup("No bookmarks found.", {
timeout: 3000
});
}
});
// Clear the interval since we've found the element
clearInterval(intervalId);
}
}
// Set an interval to check for the element every second
const intervalId = setInterval(checkAndAppendButton, 1);
} else {
// Check if we're on a manga page and show the popup
checkRandomMangaSource();
}
}
function checkRandomMangaSource() {
const randomMangaSource = localStorage.getItem('randomMangaSource');
if (randomMangaSource) {
try {
const { source } = JSON.parse(randomMangaSource);
let popupText;
if (source.startsWith('bookmark_manga_ids_')) {
const link = source.replace('bookmark_manga_ids_', '');
const maxLength = 40; // maximum length of the link to display
const displayedLink = link.length > maxLength ? link.substring(0, maxLength) + '...' : link;
popupText = `Random manga from <a href="${link}" target="_blank" style="word-wrap: break-word; width: 200px; display: inline-block; vertical-align: top;">${displayedLink}</a>`;
} else {
popupText = `Random manga from ${source}`;
}
// Create popup with options to random again or continue browsing
showPopup(popupText, {
autoClose: false,
width: 250, // adjust the width to fit the link
buttons: [
{
text: "<i class='fas fa-check'></i> Continue",
callback: () => {
// Just close the popup
}
},
{
text: "<i class='fas fa-random'></i> Again",
callback: async () => {
// Get bookmarks and find a new random one directly
const bookmarks = await getBookmarksFromStorage();
if (bookmarks.length > 0) {
const randomIndex = Math.floor(Math.random() * bookmarks.length);
const randomBookmark = bookmarks[randomIndex];
const link = `https://nhentai.net/g/${randomBookmark.id}`;
// Store bookmark info in localStorage for the next page
localStorage.setItem('randomMangaSource', JSON.stringify({
source: randomBookmark.source,
id: randomBookmark.id
}));
// Navigate to the new manga page
window.location.href = link;
} else {
showPopup("No bookmarks found.", {
timeout: 3000
});
}
}
}
]
});
// Clear the localStorage item
localStorage.removeItem('randomMangaSource');
} catch (error) {
console.error('Error parsing random manga source', error);
}
}
}
appendButton();
async function getBookmarksFromStorage() {
const bookmarks = [];
const addedIds = new Set();
// Check for bookmarks in the first format (simple array of IDs)
const allKeys = await GM.listValues();
for (const key of allKeys) {
if (key.startsWith("bookmark_manga_ids_")) {
const ids = await GM.getValue(key);
if (Array.isArray(ids)) {
// Add each ID as a bookmark object
ids.forEach(id => {
if (!addedIds.has(id)) {
bookmarks.push({
id: id,
url: `https://nhentai.net/g/${id}/`,
source: key
});
addedIds.add(id);
}
});
}
}
}
// Check for bookmarks in the second format (array of objects)
const bookmarkedMangas = await GM.getValue("bookmarkedMangas");
if (Array.isArray(bookmarkedMangas)) {
bookmarkedMangas.forEach(manga => {
// Extract ID from URL if it exists
if (manga.url) {
const match = manga.url.match(/\/g\/(\d+)/);
if (match && match[1]) {
const id = match[1];
// Check if this ID is already in our bookmarks array
if (!addedIds.has(id)) {
bookmarks.push({
id: id,
url: manga.url,
cover: manga.cover || null,
title: manga.title || null,
source: "bookmarkedMangas"
});
addedIds.add(id);
}
}
}
});
}
return bookmarks;
}
function getMangaLink(mangaID) {
return `https://nhentai.net/g/${mangaID}`;
}
//---------------------------**BookMark-Random-Button**-----------------------------
//--------------------------**Offline Favoriting**----------------------------------------------
// Main function to initialize the script
async function init() {
const offlineFavoritingEnabled = await GM.getValue('offlineFavoritingEnabled', true);
if (!offlineFavoritingEnabled) return;
console.log("NHentai Favorite Manager initialized");
// Check if user is logged in
const isLoggedIn = !document.querySelector('.menu-sign-in');
console.log("User logged in status:", isLoggedIn);
// Process stored favorites if user is logged in, regardless of current page
if (isLoggedIn) {
const toFavorite = await GM.getValue('toFavorite', []);
if (Array.isArray(toFavorite) && toFavorite.length > 0) {
console.log("Found stored favorites to process:", toFavorite);
await processFavorites(toFavorite);
}
}
// Only proceed with manga-specific features if we're on a manga page
if (window.location.pathname.includes('/g/')) {
await handleMangaPage(isLoggedIn);
}
}
// Handle manga page-specific functionality
async function handleMangaPage(isLoggedIn) {
// Get the manga ID from the URL
const mangaId = getMangaIdFromUrl();
console.log("Current manga ID:", mangaId);
if (!mangaId) {
console.log("Could not find manga ID, exiting manga-specific handling");
return;
}
// Get favorite button
const favoriteBtn = document.querySelector("#info > div")?.firstElementChild;
// Set up interval to log favorite button every 5 seconds
/*setInterval(() => {
console.log("Favorite button:", favoriteBtn);
}, 5000);*/
if (!favoriteBtn) {
console.log("Could not find favorite button, exiting manga-specific handling");
return;
}
// Get stored favorites
let toFavorite = await GM.getValue('toFavorite', []);
if (!Array.isArray(toFavorite)) {
toFavorite = [];
await GM.setValue('toFavorite', toFavorite);
}
console.log("Stored favorites:", toFavorite);
// Is this manga in our favorites?
const isFavorited = toFavorite.includes(mangaId);
console.log("Current manga in stored favorites:", isFavorited);
// Enable button if disabled
if (favoriteBtn.classList.contains('btn-disabled') && !isLoggedIn) {
favoriteBtn.classList.remove('btn-disabled');
console.log("Favorite button enabled");
}
// Update button state if it's in our favorites
if (isFavorited && !isLoggedIn) {
updateButtonToFavorited(favoriteBtn);
}
// Add click event to favorite button
favoriteBtn.addEventListener('click', async function(e) {
e.preventDefault();
e.stopPropagation();
console.log("Favorite button clicked");
// Get the CURRENT list of favorites (not the one from page load)
// This ensures we have the most up-to-date list
let currentFavorites = await GM.getValue('toFavorite', []);
if (!Array.isArray(currentFavorites)) {
currentFavorites = [];
}
// Check if this manga is CURRENTLY in favorites
const currentlyFavorited = currentFavorites.includes(mangaId);
console.log("Manga currently in favorites:", currentlyFavorited);
if (isLoggedIn) {
// Send favorite request directly to API
try {
} catch (error) {
console.error("Failed to favorite manga:", error);
// Show error popup
showPopup("Failed to favorite manga: " + error.message, {
timeout: 4000,
width: '300px'
});
}
} else {
// Toggle in stored favorites
if (currentlyFavorited) {
// Remove from favorites
const index = currentFavorites.indexOf(mangaId);
currentFavorites.splice(index, 1);
updateButtonToUnfavorited(favoriteBtn);
// showPopup("Removed from offline favorites", {
// timeout: 2000,
// width: '300px'
// });
console.log("Removed manga from stored favorites:", mangaId);
} else {
// Add to favorites
currentFavorites.push(mangaId);
updateButtonToFavorited(favoriteBtn);
// showPopup("Added to offline favorites", {
// timeout: 2000,
// width: '300px'
// });
console.log("Added manga to stored favorites:", mangaId);
}
await GM.setValue('toFavorite', currentFavorites);
console.log("Updated stored favorites:", currentFavorites);
}
});
}
// Helper function to get manga ID from URL
function getMangaIdFromUrl() {
const urlPath = window.location.pathname;
const match = urlPath.match(/\/g\/(\d+)/);
return match ? match[1] : null;
}
// Extract CSRF token from page
function getCsrfToken() {
// Try to get from app initialization
const scriptText = document.body.innerHTML;
const tokenMatch = scriptText.match(/csrf_token:\s*"([^"]+)"/);
if (tokenMatch && tokenMatch[1]) {
console.log("Found CSRF token from script:", tokenMatch[1]);
return tokenMatch[1];
}
// Try alternative method - look for form inputs
const csrfInput = document.querySelector('input[name="csrfmiddlewaretoken"]');
if (csrfInput) {
console.log("Found CSRF token from input:", csrfInput.value);
return csrfInput.value;
}
console.log("Could not find CSRF token");
return null;
}
// Nhentai Plus+.user.js (4405-4427)
function updateButtonToFavorited(button) {
button.classList.add('favorited');
const icon = button.querySelector('i');
const text = button.querySelector('span');
if (icon) icon.className = 'far fa-heart'; // Solid (filled) heart
if (text) {
const countSpan = text.querySelector('span.nobold');
text.innerText = 'Unfavorite ';
if (countSpan) {
text.appendChild(countSpan);
}
}
console.log("Button updated to favorited state");
}
function updateButtonToUnfavorited(button) {
button.classList.remove('favorited');
const icon = button.querySelector('i');
const text = button.querySelector('span');
if (icon) icon.className = 'fas fa-heart'; // Regular (outline) heart
if (text) {
const countSpan = text.querySelector('span.nobold');
text.innerText = 'Favorite ';
if (countSpan) {
text.appendChild(countSpan);
}
}
console.log("Button updated to unfavorited state");
}
// Modified sendFavoriteRequest function with improved CSRF token handling
async function sendFavoriteRequest(mangaId) {
const isIOSDevice = await GM.getValue('isIOSDevice', false);
if (isIOSDevice) {
// For iOS, we'll use a more compatible method
return new Promise((resolve, reject) => {
console.log("Using iOS-compatible favoriting method for manga:", mangaId);
// Get CSRF token using improved method
const csrfToken = getCsrfToken();
if (!csrfToken) {
console.error("Could not find CSRF token for request");
reject(new Error("Missing CSRF token"));
return;
}
// Create a temporary form to submit
const form = document.createElement('form');
form.method = 'POST';
form.action = `https://nhentai.net/api/gallery/${mangaId}/favorite`;
form.style.display = 'none';
// Add CSRF token to form
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = 'csrf_token';
csrfInput.value = csrfToken;
form.appendChild(csrfInput);
// Add a hidden iframe to target the form
const iframe = document.createElement('iframe');
iframe.name = 'favorite_frame';
iframe.style.display = 'none';
document.body.appendChild(iframe);
// Set up form target and add to document
form.target = 'favorite_frame';
document.body.appendChild(form);
// Set up response handling
let timeoutId;
iframe.onload = () => {
clearTimeout(timeoutId);
try {
// Check if favoriting was successful
if (iframe.contentDocument.body.textContent.includes('success')) {
console.log("Successfully favorited manga:", mangaId);
resolve({ status: 200 });
} else {
console.error("Failed to favorite manga:", mangaId);
reject(new Error("Failed to favorite manga"));
}
} catch (e) {
// If we can't access iframe content due to CORS, assume success
console.log("Could not access iframe content, assuming success");
resolve({ status: 200 });
}
// Clean up
setTimeout(() => {
document.body.removeChild(form);
document.body.removeChild(iframe);
}, 100);
};
// Set timeout in case of no response
timeoutId = setTimeout(() => {
console.error("Favorite request timed out for manga:", mangaId);
document.body.removeChild(form);
document.body.removeChild(iframe);
reject(new Error("Request timed out"));
}, 10000);
// Submit the form
form.submit();
});
}else{
return new Promise((resolve, reject) => {
console.log("Sending favorite request for manga:", mangaId);
// Get CSRF token - trying multiple methods
let csrfToken = getCsrfToken();
if (!csrfToken) {
console.error("Could not find CSRF token for request");
reject(new Error("Missing CSRF token"));
return;
}
// Use fetch API instead of GM.xmlHttpRequest for iOS compatibility
// Note: This requires Tampermonkey to grant fetch permissions
fetch(`https://nhentai.net/api/gallery/${mangaId}/favorite`, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"X-CSRFToken": csrfToken,
"Referer": "https://nhentai.net/g/" + mangaId + "/",
"User-Agent": navigator.userAgent
},
body: `csrf_token=${encodeURIComponent(csrfToken)}`,
credentials: "include", // Important for sending cookies properly
mode: "cors"
})
.then(response => {
console.log("Favorite request response for manga " + mangaId + ":", response.status);
if (response.status === 200) {
resolve(response);
} else {
console.error("Favorite request failed for manga " + mangaId + ":", response.status);
reject(new Error(`Request failed with status ${response.status}`));
}
})
.catch(error => {
console.error("Favorite request error for manga " + mangaId + ":", error);
reject(error);
});
});
}
}
// Improved CSRF token extraction function
function getCsrfToken() {
// Try to get from script tag with the most up-to-date token
const scriptTags = document.querySelectorAll('script:not([src])');
for (const script of scriptTags) {
const tokenMatch = script.textContent.match(/csrf_token:\s*"([^"]+)"/);
if (tokenMatch && tokenMatch[1]) {
console.log("Found CSRF token from inline script:", tokenMatch[1]);
return tokenMatch[1];
}
}
// Try to get from window._n_app object which should have the most recent token
if (window._n_app && window._n_app.csrf_token) {
console.log("Found CSRF token from window._n_app:", window._n_app.csrf_token);
return window._n_app.csrf_token;
}
// Try getting from page HTML (your original method)
const scriptText = document.body.innerHTML;
const tokenMatch = scriptText.match(/csrf_token:\s*"([^"]+)"/);
if (tokenMatch && tokenMatch[1]) {
console.log("Found CSRF token from page HTML:", tokenMatch[1]);
return tokenMatch[1];
}
// Try alternative method - look for form inputs
const csrfInput = document.querySelector('input[name="csrfmiddlewaretoken"]');
if (csrfInput) {
console.log("Found CSRF token from input:", csrfInput.value);
return csrfInput.value;
}
console.log("Could not find CSRF token");
return null;
}
// Add this function to check if cookies are properly enabled and set
function verifyCookies() {
return new Promise((resolve, reject) => {
// Try setting a test cookie
document.cookie = "test_cookie=1; path=/;";
// Check if the cookie was set
if (document.cookie.indexOf("test_cookie=1") === -1) {
console.error("Cookies appear to be disabled or restricted");
reject(new Error("Cookies appear to be disabled or restricted"));
return;
}
// Verify session cookies by making a simple request
fetch("https://nhentai.net/", {
method: "GET",
credentials: "include"
})
.then(response => {
if (response.ok) {
// Check if we're actually logged in by looking for specific elements in the response
return response.text().then(html => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");
// If the menu-sign-in element is present, we're not properly logged in
const signInElement = doc.querySelector('.menu-sign-in');
if (signInElement) {
console.error("Session cookies not working correctly - not logged in");
reject(new Error("Session cookies not working correctly - not logged in"));
} else {
console.log("Cookies and session verified successfully");
resolve(true);
}
});
} else {
console.error("Failed to verify session");
reject(new Error("Failed to verify session"));
}
})
.catch(error => {
console.error("Error verifying cookies:", error);
reject(error);
});
});
}
// Modify the processFavorites function to check cookies first
async function processFavorites(favorites) {
if (window.location.href.startsWith("https://nhentai.net/login/")) {
return;
}
console.log("Processing stored favorites:", favorites);
// Verify cookies before proceeding
try {
await verifyCookies();
} catch (error) {
console.error("Cookie verification failed:", error);
showPopup(`Cannot process favorites: ${error.message}. Try logging in again.`, {
timeout: 5000,
width: '300px'
});
return;
}
// Create and show a popup with progress information
const progressPopup = showPopup(`Processing favorites: 0/${favorites.length}`, {
autoClose: false,
width: '300px',
buttons: [
{
text: "Cancel",
callback: () => {
// User canceled processing
processingCanceled = true;
}
}
]
});
const successfulOnes = [];
const failedOnes = [];
let processingCanceled = false;
for (let i = 0; i < favorites.length; i++) {
if (processingCanceled) {
progressPopup.updateMessage(`Processing canceled. Completed: ${successfulOnes.length}/${favorites.length}`);
break;
}
const mangaId = favorites[i];
// Update progress in popup
progressPopup.updateMessage(`Processing favorites: ${i+1}/${favorites.length}`);
try {
await sendFavoriteRequest(mangaId);
console.log("Successfully favorited manga:", mangaId);
successfulOnes.push(mangaId);
} catch (error) {
console.error("Error favoriting manga:", mangaId, error);
failedOnes.push(mangaId);
}
// Small delay to avoid hammering the server
await new Promise(resolve => setTimeout(resolve, 500));
}
// Keep only the failed ones in storage
if (failedOnes.length > 0) {
await GM.setValue('toFavorite', failedOnes);
console.log("Updated stored favorites with failed ones:", failedOnes);
} else {
// Clear stored favorites after processing
await GM.setValue('toFavorite', []);
console.log("Cleared stored favorites");
}
// Update final result in popup
progressPopup.updateMessage(`Completed: ${successfulOnes.length} successful, ${failedOnes.length} failed`);
// Close the popup
progressPopup.close();
// Show a summary popup that auto-closes
showPopup(`Completed: ${successfulOnes.length} successful, ${failedOnes.length} failed`, {
timeout: 5000,
width: '300px',
buttons: [
{
text: "OK",
callback: () => {}
}
]
});
}
init();
//--------------------------**Offline Favoriting**----------------------------------------------
//-----------------------------------------------------NFM-Debugging------------------------------------------------------------------
// Add this function to create a settings menu
async function createSettingsMenu() {
const nfmPageEnabled = await GM.getValue('nfmPageEnabled', true);
if (!nfmPageEnabled) return;
// Create settings button
const nav = document.querySelector('nav .menu.left');
if (!nav) return;
const settingsLi = document.createElement('li');
settingsLi.className = 'desktop';
const settingsLink = document.createElement('a');
settingsLink.href = '#';
settingsLink.innerHTML = '<i class="fas fa-cog" style="color:pink;"></i> NFM';
settingsLi.appendChild(settingsLink);
nav.appendChild(settingsLi);
// Create settings popup
settingsLink.addEventListener('click', async (e) => {
e.preventDefault();
const offlineFavoritingEnabled = await GM.getValue('offlineFavoritingEnabled', true);
const toFavorite = await GM.getValue('toFavorite', []);
const isIOSDevice = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
const content = `
<div style="padding: 1rem;">
<h3>NHentai Favorite Manager Settings</h3>
<div style="margin-bottom: 1rem;">
<label>
<input type="checkbox" id="nfm-offline-favoriting" ${offlineFavoritingEnabled ? 'checked' : ''}>
Enable offline favoriting
</label>
</div>
<div style="margin-bottom: 1rem;">
<p>Pending favorites: ${toFavorite.length}</p>
<button id="nfm-clear-favorites" class="btn btn-secondary">Clear Pending Favorites</button>
<button id="nfm-process-favorites" class="btn btn-primary">Process Now</button>
</div>
<div style="margin-bottom: 1rem;">
<h4>Debug Info</h4>
<p>iOS Device: ${isIOSDevice ? 'Yes' : 'No'}</p>
<p>Logged In: ${!document.querySelector('.menu-sign-in') ? 'Yes' : 'No'}</p>
<p>Cookies Enabled: ${navigator.cookieEnabled ? 'Yes' : 'No'}</p>
<button id="nfm-test-request" class="btn btn-secondary">Test API Request</button>
</div>
</div>
`;
const popup = showPopup(content, {
autoClose: false,
width: '400px',
buttons: [
{
text: "Close",
callback: () => {}
}
]
});
// Add event listeners
document.getElementById('nfm-offline-favoriting').addEventListener('change', async (e) => {
await GM.setValue('offlineFavoritingEnabled', e.target.checked);
console.log("Offline favoriting enabled:", e.target.checked);
});
document.getElementById('nfm-clear-favorites').addEventListener('click', async () => {
await GM.setValue('toFavorite', []);
console.log("Cleared pending favorites");
popup.updateMessage('Pending favorites cleared!');
setTimeout(() => popup.close(), 1500);
});
document.getElementById('nfm-process-favorites').addEventListener('click', async () => {
popup.close();
const toFavorite = await GM.getValue('toFavorite', []);
if (toFavorite.length > 0) {
await processFavorites(toFavorite);
} else {
showPopup("No pending favorites to process.", {
timeout: 2000,
width: '300px'
});
}
});
document.getElementById('nfm-test-request').addEventListener('click', async () => {
console.log("Testing API request...");
try {
await verifyCookies();
showPopup("Cookie test successful!", {
timeout: 2000,
width: '300px'
});
} catch (error) {
showPopup(`Cookie test failed: ${error.message}`, {
timeout: 4000,
width: '300px'
});
}
});
});
}
// Add this to your init function
createSettingsMenu();
//-----------------------------------------------------NFM-Debugging------------------------------------------------------------------
//-------------------------------------------------**Delete-Twitter-Button**-----------------------------------------------
async function deleteTwitterButton() {
const twitterButtonEnabled = await GM.getValue('twitterButtonEnabled', true);
if (!twitterButtonEnabled) return;
$('a[href="https://twitter.com/nhentaiOfficial"]').remove();
}
deleteTwitterButton();
//-------------------------------------------------**Delete-Twitter-Button**-----------------------------------------------
//-------------------------------------------------**Delete-Info-Button**-----------------------------------------------
async function deleteInfoButton() {
const infoButtonEnabled = await GM.getValue('infoButtonEnabled', true);
if (!infoButtonEnabled) return;
$("a[href='/info/']").remove();
}
//Call the function to execute
deleteInfoButton();
//-------------------------------------------------**Delete-Info-Button**-----------------------------------------------
//-------------------------------------------------**Delete-Profile-Button**-----------------------------------------------
async function deleteProfileButton() {
const profileButtonEnabled = await GM.getValue('profileButtonEnabled', true);
if (!profileButtonEnabled) return;
$("li a[href^='/users/']").remove();
}
//Call the function to execute.
deleteProfileButton();
//-------------------------------------------------**Delete-Profile-Button**-----------------------------------------------
//-------------------------------------------------**Delete-Logout-Button**-----------------------------------------------
async function deleteLogoutButton() {
const logoutButtonEnabled = await GM.getValue('logoutButtonEnabled', true);
if (!logoutButtonEnabled) return;
$("li a[href='/logout/?next=/settings/']").parent().remove();
}
deleteLogoutButton();
//-------------------------------------------------**Delete-Logout-Button**-----------------------------------------------
//-------------------------------------------------**BookMark-Link**---------------------------------------------------------
async function createBookmarkLink() {
const bookmarkLinkEnabled = await GM.getValue('bookmarkLinkEnabled', true);
if (!bookmarkLinkEnabled) return;
// Extract current manga ID from URL
const currentMangaId = window.location.pathname.split('/')[2];
// Get all GM keys
const allKeys = await GM.listValues();
// Filter bookmark keys and check for current ID
let bookmarkUrl = null;
for (const key of allKeys) {
if (key.startsWith('bookmark_manga_ids_')) {
const mangaIds = await GM.getValue(key, []);
if (mangaIds.includes(currentMangaId)) {
// Extract original bookmark URL from key
bookmarkUrl = key.replace('bookmark_manga_ids_', '');
break;
}
}
}
// Update title if bookmark found
if (bookmarkUrl) {
const $title = $('h1.title');
const linkHtml = `<a href="${bookmarkUrl}" class="bookmark-link" style="color: inherit; text-decoration: none;"><u>${$title.html()}</u></a>`;
$title.html(linkHtml).css('cursor', 'pointer');
}
}
createBookmarkLink();
//-------------------------------------------------**BookMark-Link**---------------------------------------------------------
//-------------------------------------------------**Offline-Favorites-Page**---------------------------------------------------------
// Function to handle the offline favorites page
async function handleOfflineFavoritesPage() {
// Check if we're on the favorites page
if (window.location.pathname !== '/favorite/' && window.location.pathname !== '/favorite') {
return;
}
// Check if the feature is enabled
const offlineFavoritesPageEnabled = await GM.getValue('offlineFavoritesPageEnabled', true);
if (!offlineFavoritesPageEnabled) {
return;
}
// Remove any 404 elements
const notFoundHeading = document.querySelector('h1');
if (notFoundHeading?.textContent === '404 – Not Found') {
notFoundHeading.remove();
}
const notFoundMessage = document.querySelector('p');
if (notFoundMessage?.textContent === "Looks like what you're looking for isn't here.") {
notFoundMessage.remove();
}
// Get offline favorites
const offlineFavorites = await GM.getValue('toFavorite', []);
// Create container for favorites
const container = document.createElement('div');
container.className = 'container';
container.id = 'offline-favorites-container';
// Create heading
const heading = document.createElement('h1');
heading.textContent = 'Offline Favorites';
container.appendChild(heading);
// Create description
const description = document.createElement('p');
description.textContent = 'These are manga you have favorited while offline. They will be synced to your account when you log in.';
container.appendChild(description);
// Create sort controls
const sortControls = document.createElement('div');
sortControls.className = 'sort-controls';
sortControls.innerHTML = `
<label>
Sort by:
<select id="sort-favorites">
<option value="newest">Newest First</option>
<option value="oldest">Oldest First</option>
</select>
</label>
`;
container.appendChild(sortControls);
// Create gallery container
const galleryContainer = document.createElement('div');
galleryContainer.className = 'gallery-container';
container.appendChild(galleryContainer);
// Add container to page
document.getElementById('content').appendChild(container);
// Add CSS styles
// Add CSS styles
GM.addStyle(`
#offline-favorites-container {
padding: 20px 0;
max-width: 1200px;
margin: 0 auto;
}
#offline-favorites-container h1 {
margin-bottom: 10px;
color: #ed2553;
font-size: 2em;
}
#offline-favorites-container p {
margin-bottom: 20px;
color: #888;
font-size: 1em;
}
.sort-controls {
margin-bottom: 20px;
}
.sort-controls select {
background-color: #252525;
color: #f1f1f1;
border: 1px solid #3d3d3d;
padding: 8px 12px;
border-radius: 3px;
cursor: pointer;
font-size: 14px;
}
.sort-controls select:hover {
background-color: #3d3d3d;
}
.gallery-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 25px;
padding: 0 10px;
}
.favorite-item {
position: relative;
border-radius: 3px;
overflow: hidden;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);
transition: all 0.3s ease;
background: #252525;
}
.favorite-item:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(237, 37, 83, 0.2);
}
.favorite-item img {
width: 100%;
height: auto;
display: block;
aspect-ratio: 3/4;
object-fit: cover;
}
.favorite-item .title {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.9));
color: #ffffff;
padding: 15px 10px;
font-size: 14px;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.favorite-item .remove-btn {
position: absolute;
top: 5px;
right: 5px;
background: rgba(0,0,0,0.5);
color: white;
border: none;
border-radius: 50%;
width: 24px;
height: 24px;
font-size: 14px;
cursor: pointer;
opacity: 0;
transition: opacity 0.2s ease;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
}
@media (max-width: 768px) {
.favorite-item .remove-btn {
width: 12px;
height: 12px;
font-size: 12px;
}
}
.favorite-item:hover .remove-btn {
opacity: 1;
}
.favorite-item .remove-btn:hover {
background: #ed2553;
transform: scale(1.1);
}
.no-favorites {
grid-column: 1 / -1;
text-align: center;
padding: 80px 0;
color: #888;
font-size: 1.2em;
background: #252525;
border-radius: 5px;
margin: 20px 0;
}
@media (max-width: 768px) {
.gallery-container {
grid-template-columns: repeat(auto-fill, minmax(115px, .5fr));
gap: 15px;
}
#offline-favorites-container h1 {
font-size: 1.5em;
text-align: center;
}
}
`);
// Function to render favorites
async function renderFavorites(favorites, sortOrder = 'newest') {
galleryContainer.innerHTML = '';
if (favorites.length === 0) {
const noFavorites = document.createElement('div');
noFavorites.className = 'no-favorites';
noFavorites.textContent = 'No offline favorites found. Add some by clicking the heart icon on manga pages.';
galleryContainer.appendChild(noFavorites);
return;
}
// Sort favorites if needed
let sortedFavorites = [...favorites];
if (sortOrder === 'newest') {
sortedFavorites.reverse();
}
// Create a fragment to improve performance
const fragment = document.createDocumentFragment();
// Process each favorite
for (const mangaId of sortedFavorites) {
const favoriteItem = document.createElement('div');
favoriteItem.className = 'favorite-item';
favoriteItem.dataset.id = mangaId;
// Create link to manga
const link = document.createElement('a');
link.href = `https://nhentai.net/g/${mangaId}/`;
// Create thumbnail image (placeholder initially)
const img = document.createElement('img');
img.src = 'https://i.nhentai.net/galleries/0/placeholder.jpg'; // Placeholder
img.alt = `Manga ${mangaId}`;
link.appendChild(img);
// Create title element (will be updated with actual title)
const title = document.createElement('div');
title.className = 'title';
title.textContent = `Loading...`;
link.appendChild(title);
// Create remove button
const removeBtn = document.createElement('button');
removeBtn.className = 'remove-btn';
removeBtn.innerHTML = '✖';
removeBtn.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
// Show confirmation popup before deleting
showPopup('Are you sure you want to remove this from favorites?', {
autoClose: false,
width: '300px',
buttons: [
{
text: "Cancel",
callback: () => {
// Do nothing, just close the popup
}
},
{
text: "Remove",
callback: async () => {
// Store the manga ID for potential undo
const deletedMangaId = mangaId;
let deletedMangaInfo = null;
try {
// Try to get manga info for the undo popup
deletedMangaInfo = await GM.getValue(`manga_info_${mangaId}`, null);
} catch (error) {
console.error("Error getting manga info for undo:", error);
}
// Remove from favorites
const updatedFavorites = sortedFavorites.filter(id => id !== mangaId);
await GM.setValue('toFavorite', updatedFavorites);
// Remove from display
favoriteItem.remove();
// Update display if no favorites left
if (updatedFavorites.length === 0) {
renderFavorites(updatedFavorites, sortOrder);
}
// Show confirmation with undo button
showPopup(
`Removed from favorites${deletedMangaInfo?.title ? ': ' + deletedMangaInfo.title : ''}`,
{
timeout: 5000,
width: '300px',
buttons: [
{
text: "Undo",
callback: async () => {
// Get current favorites
const currentFavorites = await GM.getValue('toFavorite', []);
// Add the manga back if it's not already there
if (!currentFavorites.includes(deletedMangaId)) {
currentFavorites.push(deletedMangaId);
await GM.setValue('toFavorite', currentFavorites);
// Refresh the display to show the restored item
renderFavorites(currentFavorites, sortOrder);
// Show confirmation
showPopup('Favorite restored', { timeout: 2000 });
}
}
}
]
}
);
}
}
]
});
});
favoriteItem.appendChild(link);
favoriteItem.appendChild(removeBtn);
fragment.appendChild(favoriteItem);
// Fetch manga info asynchronously
fetchMangaInfo(mangaId).then(info => {
if (info) {
// Update thumbnail
if (info.thumbnail) {
img.src = info.thumbnail;
}
// Update title
if (info.title) {
title.textContent = info.title;
}
}
}).catch(error => {
console.error(`Error fetching info for manga ${mangaId}:`, error);
});
}
galleryContainer.appendChild(fragment);
}
// Function to fetch manga info
async function fetchMangaInfo(mangaId) {
try {
// Try to get from cache first
const cachedInfo = await GM.getValue(`manga_info_${mangaId}`, null);
if (cachedInfo) {
return cachedInfo;
}
// Fetch from API
const response = await fetch(`https://nhentai.net/g/${mangaId}/`);
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// Extract title
let title = '';
const titleElement = doc.querySelector('.title');
if (titleElement) {
const prettyElement = titleElement.querySelector('.pretty');
title = prettyElement ? prettyElement.textContent : titleElement.textContent;
}
// Extract thumbnail
let thumbnail = '';
const coverImg = doc.querySelector('#cover img');
if (coverImg) {
thumbnail = coverImg.getAttribute('data-src') || coverImg.getAttribute('src');
}
// Create info object
const info = { title, thumbnail };
// Cache the info
await GM.setValue(`manga_info_${mangaId}`, info);
return info;
} catch (error) {
console.error(`Error fetching manga info for ${mangaId}:`, error);
return null;
}
}
// Initialize with current favorites
renderFavorites(offlineFavorites);
// Add event listener for sort control
document.getElementById('sort-favorites').addEventListener('change', function() {
renderFavorites(offlineFavorites, this.value);
});
}
// Call the function to handle the favorites page
handleOfflineFavoritesPage();
//-------------------------------------------------**Offline-Favorites-Page**---------------------------------------------------------
//-------------------------------------------------**Non-English-Manga**--------------------------------------------------------
async function applyNonEnglishStyles() {
// Remove existing styles
$('style[data-non-english]').remove();
const showNonEnglish = await GM.getValue('showNonEnglish', 'show');
let style = '';
if (showNonEnglish === 'hide') {
style = `.gallery:not([data-tags~='12227']) { display: none; }`;
} else if (showNonEnglish === 'fade') {
const nonEnglishFadeOpacity = 0.5; // Or get this from settings
style = `.gallery:not([data-tags~='12227']) > .cover > img, .gallery:not([data-tags~='12227']) > .cover > .caption { opacity: ${nonEnglishFadeOpacity}; }`;
}
if (style) {
const newStyle = document.createElement('style');
newStyle.dataset.nonEnglish = true;
newStyle.innerHTML = style;
document.head.appendChild(newStyle);
}
}
applyNonEnglishStyles(); // Apply styles on initial load
//-------------------------------------------------**Non-English-Manga**--------------------------------------------------------
// -----------------------------------------------**Thumbnail-Page-Numbers**--------------------------------------------------------
// Function to add page numbers to manga thumbnails
async function addPageNumbersToThumbnails() {
// Check if the feature is enabled
const showPageNumbersEnabled = await GM.getValue('showPageNumbersEnabled', true);
if (!showPageNumbersEnabled) return;
// Add CSS for page number display
const pageNumberCSS = `
.page-number-display {
position: absolute;
top: 5px;
right: 5px;
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 12px;
font-weight: bold;
z-index: 10;
background-color: rgba(0,0,0,.4);
opacity: 1;
}
`;
GM.addStyle(pageNumberCSS);
// Function to extract page count from a manga page
async function getPageCount(url) {
try {
const response = await fetch(url);
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// Look for the page count in the tags section
const pageElement = doc.querySelector('#tags .tag-container:nth-last-child(2) .name');
if (pageElement) {
const pageCount = parseInt(pageElement.textContent.trim(), 10);
if (!isNaN(pageCount)) {
return pageCount;
}
}
return null;
} catch (error) {
console.error('Error fetching page count:', error);
return null;
}
}
// Function to add page number display to a gallery item
async function addPageNumberToGallery(galleryItem) {
// Get the manga URL first
const coverLink = galleryItem.querySelector('.cover');
if (!coverLink) return;
// Check if this specific cover link already has a page number display
if (coverLink.querySelector('.page-number-display')) {
return;
}
// Mark this cover as being processed to prevent race conditions
if (coverLink.dataset.pageNumberProcessing === 'true') {
return;
}
coverLink.dataset.pageNumberProcessing = 'true';
const mangaUrl = coverLink.getAttribute('href');
if (!mangaUrl) {
coverLink.dataset.pageNumberProcessing = 'false';
return;
}
const fullUrl = `https://nhentai.net${mangaUrl}`;
try {
// Try to get page count
const pageCount = await getPageCount(fullUrl);
if (pageCount) {
// Double-check that no page number display was added while we were fetching
if (!coverLink.querySelector('.page-number-display')) {
// Create and add the page number display
const pageNumberDisplay = document.createElement('div');
pageNumberDisplay.className = 'page-number-display';
pageNumberDisplay.textContent = `${pageCount} ${pageCount === 1 ? 'page' : 'pages'}`;
// Add to the cover element
coverLink.style.position = 'relative';
coverLink.appendChild(pageNumberDisplay);
}
}
} finally {
// Always clear the processing flag
coverLink.dataset.pageNumberProcessing = 'false';
}
}
// Process all gallery items on the page
function processGalleryItems() {
const galleryItems = document.querySelectorAll('.gallery');
galleryItems.forEach(galleryItem => {
addPageNumberToGallery(galleryItem);
});
}
// Initial processing
processGalleryItems();
// Set up a MutationObserver to handle dynamically added content
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.addedNodes && mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach(node => {
// Check if the added node is a gallery or contains galleries
if (node.classList && node.classList.contains('gallery')) {
addPageNumberToGallery(node);
} else if (node.querySelectorAll) {
const galleries = node.querySelectorAll('.gallery');
galleries.forEach(gallery => {
addPageNumberToGallery(gallery);
});
}
});
}
});
});
// Start observing the document with the configured parameters
observer.observe(document.body, { childList: true, subtree: true });
}
// Call the function to add page numbers to thumbnails
addPageNumbersToThumbnails();
// -----------------------------------------------**Thumbnail-Page-Numbers**--------------------------------------------------------
// -----------------------------------------------**Background Max Manga Sync**--------------------------------------------------------
// Wait for the HTML document to be fully loaded
setTimeout(async function() {
// Function to fetch and process bookmarked pages
async function processBookmarkedPages(isBackgroundProcess = false) {
// Get all bookmarked pages from storage
const bookmarkedPages = await GM.getValue('bookmarkedPages', []);
// Get the max manga per bookmark from the slider
const maxMangaPerBookmark = await GM.getValue('maxMangaPerBookmark', 5);
// Get the last processing timestamp to avoid processing too frequently
const lastProcessTime = await GM.getValue('lastBookmarkProcessTime', 0);
const now = new Date().getTime();
// If this is a background process, only run if it's been at least 1 hour since last run
if (isBackgroundProcess && (now - lastProcessTime < 3600000)) {
console.log('Background processing skipped - last run was less than 1 hour ago');
return;
}
// Always get bookmarks from storage, but also check DOM if we're on the bookmarks page
let bookmarkLinks = [];
// First, always use the stored bookmarked pages as the primary source
bookmarkLinks = bookmarkedPages.map(url => ({ href: url }));
// If we're on the bookmarks page, also check the DOM for any new bookmarks
if (!isBackgroundProcess && window.location.href.includes('/bookmarks')) {
const domBookmarkLinks = document.querySelectorAll('.bookmarks-list .bookmark-link');
console.log('Found bookmark links in DOM:', domBookmarkLinks.length);
// Convert DOM collection to array and add any links not already in our list
Array.from(domBookmarkLinks).forEach(link => {
if (link.href && !bookmarkLinks.some(existing => existing.href === link.href)) {
bookmarkLinks.push(link);
}
});
}
console.log('Processing bookmarks:', bookmarkLinks.length);
console.log('Max manga per bookmark setting:', maxMangaPerBookmark);
if (bookmarkLinks.length === 0) {
console.log('No bookmark links found');
return;
}
// Log the fetched bookmarked URLs
console.log('Processing bookmarked URLs:');
// Store the current time as the last processing time
// await GM.setValue('lastBookmarkProcessTime', now);
// Track rate limit status
let isRateLimited = false;
let rateLimitResetTime = 0;
// Request each bookmark URL and extract manga URLs
for (const link of bookmarkLinks) {
if (!link.href) {
console.log('Bookmark link has no href attribute, skipping');
continue;
}
// If we're rate limited and the reset time hasn't passed, skip processing
if (isRateLimited && now < rateLimitResetTime) {
console.log(`Skipping processing due to rate limit. Will reset at ${new Date(rateLimitResetTime).toLocaleTimeString()}`);
continue;
}
// Check if bookmark has existing cache
const existingCache = await GM.getValue(`bookmark_manga_ids_${link.href}`);
if (existingCache && !isBackgroundProcess) {
console.log(`Skipping bookmark ${link.href} as it has existing cache`);
continue;
}
console.log(`Processing bookmark: ${link.href}`);
try {
// Fetch the bookmark page with retry logic
const bookmarkResponse = await fetchWithRetry(link.href);
// Check for rate limiting headers
const rateLimitRemaining = bookmarkResponse.headers.get('X-RateLimit-Remaining');
const rateLimitReset = bookmarkResponse.headers.get('X-RateLimit-Reset');
if (rateLimitRemaining === '0' && rateLimitReset) {
isRateLimited = true;
rateLimitResetTime = parseInt(rateLimitReset) * 1000; // Convert to milliseconds
console.log(`Rate limit reached. Will reset at ${new Date(rateLimitResetTime).toLocaleTimeString()}`);
}
const html = await bookmarkResponse.text();
const doc = new DOMParser().parseFromString(html, 'text/html');
// Extract all manga URLs from the page (main gallery thumbnails)
const mangaLinks = doc.querySelectorAll('.gallery a.cover');
const allMangaUrls = Array.from(mangaLinks).map(link => {
return {
url: 'https://nhentai.net' + link.getAttribute('href'),
id: link.getAttribute('href').split('/g/')[1].replace('/', '')
};
});
// Store the complete list of manga IDs for this bookmark
await GM.setValue(`bookmark_manga_ids_${link.href}`, allMangaUrls.map(item => item.id));
// Apply limit if maxMangaPerBookmark is valid
const limitToApply = (!isNaN(maxMangaPerBookmark) && maxMangaPerBookmark > 0)
? maxMangaPerBookmark
: allMangaUrls.length;
// Slice the array to the appropriate length
const mangaToProcess = allMangaUrls.slice(0, limitToApply);
// Log the fetched manga URLs from each bookmark with limit info
console.log(`Found ${allMangaUrls.length} manga in bookmark, processing ${mangaToProcess.length} (limit: ${limitToApply})`);
// Fetch and process tags for each manga URL (limited by maxMangaPerBookmark)
for (const manga of mangaToProcess) {
// Check if we've hit a rate limit during processing
if (isRateLimited && now < rateLimitResetTime) {
console.log(`Pausing manga processing due to rate limit. Will resume later.`);
break;
}
const mangaId = manga.id;
const mangaUrl = manga.url;
// Use a simpler cache key that only depends on the manga ID
let mangaInfo = await GM.getValue(`manga_${mangaId}`, null);
// Track when this manga was last seen
const currentTime = new Date().getTime();
if (!mangaInfo) {
console.log(`Fetching new manga info for ID: ${mangaId}, URL: ${mangaUrl}`);
try {
// Fetch the manga page with retry logic
const mangaResponse = await fetchWithRetry(mangaUrl);
// Check for rate limiting headers
const mangaRateLimitRemaining = mangaResponse.headers.get('X-RateLimit-Remaining');
const mangaRateLimitReset = mangaResponse.headers.get('X-RateLimit-Reset');
if (mangaRateLimitRemaining === '0' && mangaRateLimitReset) {
isRateLimited = true;
rateLimitResetTime = parseInt(mangaRateLimitReset) * 1000; // Convert to milliseconds
console.log(`Rate limit reached during manga fetch. Will reset at ${new Date(rateLimitResetTime).toLocaleTimeString()}`);
}
const html = await mangaResponse.text();
const doc = new DOMParser().parseFromString(html, 'text/html');
const tagsList = doc.querySelectorAll('#tags .tag');
// Get the title
const titleElement = doc.querySelector('h1.title');
const title = titleElement ? titleElement.textContent.trim() : null;
if (tagsList.length > 0) {
const tags = Array.from(tagsList).map(tag => tag.textContent.trim());
console.log(`Fetched tags for ${mangaUrl}:`, tags);
mangaInfo = {
id: mangaId,
url: mangaUrl,
title: title,
tags: tags,
lastSeen: currentTime,
bookmarks: [link.href] // Track which bookmarks this manga appears in
};
} else {
console.log(`No tags found for ${mangaUrl}`);
mangaInfo = {
id: mangaId,
url: mangaUrl,
title: title,
tags: [],
lastSeen: currentTime,
bookmarks: [link.href]
};
}
await GM.setValue(`manga_${mangaId}`, mangaInfo);
} catch (error) {
console.error(`Error fetching tags for: ${mangaUrl}`, error);
mangaInfo = {
id: mangaId,
url: mangaUrl,
tags: [],
lastSeen: currentTime,
bookmarks: [link.href]
};
await GM.setValue(`manga_${mangaId}`, mangaInfo);
// Check if the error was due to rate limiting
if (error.message && error.message.includes('429')) {
isRateLimited = true;
rateLimitResetTime = currentTime + 300000; // Wait 5 minutes by default
console.log(`Rate limit detected from error. Pausing for 5 minutes.`);
}
}
} else {
// Update the existing manga info with the current timestamp
// and add this bookmark if not already present
if (!mangaInfo.bookmarks.includes(link.href)) {
mangaInfo.bookmarks.push(link.href);
}
mangaInfo.lastSeen = currentTime;
await GM.setValue(`manga_${mangaId}`, mangaInfo);
console.log(`Updated existing manga cache for ${mangaId}`);
}
// Add a small delay between manga requests to avoid rate limiting
await new Promise(resolve => setTimeout(resolve, 500));
}
} catch (error) {
console.error(`Error processing bookmark: ${link.href}`, error);
// Check if the error was due to rate limiting
if (error.message && error.message.includes('429')) {
isRateLimited = true;
rateLimitResetTime = now + 300000; // Wait 5 minutes by default
console.log(`Rate limit detected from error. Pausing for 5 minutes.`);
}
}
// Add a delay between bookmark processing to avoid rate limiting
await new Promise(resolve => setTimeout(resolve, 1000));
}
// Optional: clean up old cached manga data that hasn't been seen in a while
await cleanupOldCacheData(30); // Clean data older than 30 days
console.log('Bookmark processing completed');
// Store the current time as the last processing time AFTER processing is complete
// Use current time instead of the 'now' variable from the beginning of the function
await GM.setValue('lastBookmarkProcessTime', new Date().getTime());
}
// Helper function to clean up old cache data
async function cleanupOldCacheData(daysOld) {
try {
const allKeys = await GM.listValues();
const mangaKeys = allKeys.filter(key => key.startsWith('manga_'));
const now = new Date().getTime();
const cutoffTime = now - (daysOld * 24 * 60 * 60 * 1000); // Convert days to milliseconds
let removedCount = 0;
for (const key of mangaKeys) {
const mangaInfo = await GM.getValue(key);
// If there's no lastSeen or if it's older than the cutoff, remove it
if (!mangaInfo || !mangaInfo.lastSeen || mangaInfo.lastSeen < cutoffTime) {
await GM.deleteValue(key);
removedCount++;
}
}
if (removedCount > 0) {
console.log(`Cleaned up ${removedCount} old manga entries from cache`);
}
} catch (error) {
console.error('Error cleaning up old cache data:', error);
}
}
// Helper function to fetch with retry logic for 429 errors
async function fetchWithRetry(url, maxRetries = 10, initialDelay = 2000) {
let retries = 0;
let delay = initialDelay;
while (retries < maxRetries) {
try {
const response = await fetch(url);
// If we got a 429 Too Many Requests, retry after a delay
if (response.status === 429) {
retries++;
console.log(`Rate limited (429) on ${url}. Retry ${retries}/${maxRetries} after ${delay}ms delay.`);
// Check for Retry-After header
const retryAfter = response.headers.get('Retry-After');
if (retryAfter) {
// Retry-After is in seconds, convert to milliseconds
delay = parseInt(retryAfter) * 1000;
console.log(`Server specified Retry-After: ${retryAfter} seconds`);
}
await new Promise(resolve => setTimeout(resolve, delay));
// Increase delay for subsequent retries (exponential backoff)
delay = Math.min(delay * 1.5, 30000); // Cap at 30 seconds
} else {
// For any other status, return the response
return response;
}
} catch (error) {
retries++;
console.error(`Fetch error for ${url}. Retry ${retries}/${maxRetries}.`, error);
if (retries >= maxRetries) throw error;
await new Promise(resolve => setTimeout(resolve, delay));
// Increase delay for subsequent retries
delay = Math.min(delay * 1.5, 30000);
}
}
throw new Error(`Failed to fetch ${url} after ${maxRetries} retries.`);
}
// Helper function to update manga cache when limit changes
async function updateMangaCache() {
const maxMangaPerBookmark = await GM.getValue('maxMangaPerBookmark', 5);
const allKeys = await GM.listValues();
const mangaKeys = allKeys.filter(key => key.startsWith('manga_'));
for (const key of mangaKeys) {
const mangaInfo = await GM.getValue(key);
if (mangaInfo) {
const newLimit = maxMangaPerBookmark;
const existingLimit = mangaInfo.limit;
if (newLimit !== existingLimit) {
console.log(`Updating manga cache for ${mangaInfo.id} with new limit ${newLimit}`);
mangaInfo.limit = newLimit;
await GM.setValue(key, mangaInfo);
}
}
}
}
// Set up periodic background processing
function setupBackgroundProcessing() {
// Process bookmarks in the background every hour
setInterval(async () => {
console.log('Starting background bookmark processing...');
await processBookmarkedPages(true);
}, 3600000); // 1 hour in milliseconds
// Also run once at startup after a short delay
setTimeout(async () => {
console.log('Running initial background bookmark processing...');
await processBookmarkedPages(true);
}, 30000); // 30 seconds after page load
}
// Process bookmarks on any page, but with different behavior
// If we're on the bookmarks page, process immediately with DOM integration
// Otherwise, process in background mode after a short delay
if (window.location.href.includes('/bookmarks')) {
processBookmarkedPages(false); // Process with DOM integration
} else {
// On other pages, process in background mode after a short delay
setTimeout(() => {
processBookmarkedPages(true); // Process in background mode
}, 5000); // 5 second delay to not interfere with page loading
}
// Set up background processing regardless of current page
setupBackgroundProcessing();
// Update manga cache when limit changes
updateMangaCache();
}, 2000);
//-----------------------------------------------**Background Max Manga Sync**--------------------------------------------------------
// -----------------------------------------------**Must-Add-Tags**-----------------------------------------------------------------------
// Function to update the must-add tags list
// Intercept both XHR and form submissions
// Ensure this is defined globally or within a scope accessible to both handlers
const originalOpen = XMLHttpRequest.prototype.open;
// Handle form submissions
document.querySelector('form.search').addEventListener('submit', async function(e) {
e.preventDefault();
const searchInput = this.querySelector('input[name="q"]');
let query = searchInput.value; // Get the raw query string
// Always split the query into an array of tags
let queryArray = query.split(/\s+/).filter(tag => tag.length > 0).map(tag => tag.toLowerCase());
const mustAddTagsEnabled = await GM.getValue('mustAddTagsEnabled', false);
const mustAddTags = (await GM.getValue('mustAddTags', [])).map(tag => tag.toLowerCase());
if (mustAddTagsEnabled) {
queryArray = [...new Set([...queryArray, ...mustAddTags])];
}
query = queryArray.join(' ');
window.location.href = `/search/?q=${encodeURIComponent(query)}`;
});
// Modify XHR requests
XMLHttpRequest.prototype.open = function(method, url) {
// Store the original arguments and context for later use
const xhr = this;
const args = arguments;
if (typeof url === 'string' && url.includes('/search/')) {
try {
const urlObj = new URL(url, window.location.origin);
let query = urlObj.searchParams.get('q') || '';
let queryArray = query.split(/\s+/).filter(tag => tag.length > 0).map(tag => tag.toLowerCase());
// Fetch mustAddTags asynchronously for XHR as well
GM.getValue('mustAddTagsEnabled', false).then(mustAddTagsEnabled => {
if (mustAddTagsEnabled) {
GM.getValue('mustAddTags', []).then(mustAddTagsRaw => {
const mustAddTags = mustAddTagsRaw.map(tag => tag.toLowerCase());
try {
queryArray = [...new Set([...queryArray, ...mustAddTags])];
query = queryArray.join(' ');
urlObj.searchParams.set('q', query);
// Re-open the request with the modified URL
return originalOpen.apply(xhr, [method, urlObj.toString(), ...Array.prototype.slice.call(args, 2)]);
} catch (error) {
console.error('Error in XHR open override (mustAddTags):', error);
// Fall back to original URL if there's an error
return originalOpen.apply(xhr, args);
}
}).catch(error => {
console.error('Error getting mustAddTags:', error);
return originalOpen.apply(xhr, args);
});
} else {
return originalOpen.apply(xhr, args);
}
}).catch(error => {
console.error('Error getting mustAddTagsEnabled:', error);
return originalOpen.apply(xhr, args);
});
} catch (error) {
console.error('Error in XHR open override:', error);
return originalOpen.apply(xhr, args);
}
} else {
return originalOpen.apply(xhr, args);
}
};
// -----------------------------------------------**AutoSync Initialization**-----------------------------------------------------------------------
// AutoSync initialization moved to be within scope of autoSyncManager definition
// -----------------------------------------------**Must-Add-Tags**-----------------------------------------------------------------------
// -----------------------------------------------**Manga-Sync**-----------------------------------------------------------------------
// Online Data Sync Implementation
class OnlineDataSync {
constructor() {
this.providers = {
jsonstorage: new JSONStorageProvider()
};
this.publicConfig = {
url: 'https://api.jsonstorage.net/v1/json/d206ce58-9543-48db-a5e4-997cfc745ef3/6629f339-5696-4b2e-b63d-b5d092dc46f6',
apiKey: '2f9e71c8-be66-4623-a2cc-a6f05e958563'
};
}
// Generate 5-character alphanumeric UUID
generateUUID() {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let result = '';
for (let i = 0; i < 5; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
// Get or create user UUID
async getUserUUID() {
// Check if we have a cached UUID first
if (this.cachedUUID) {
return this.cachedUUID;
}
let uuid = await GM.getValue('userUUID');
if (!uuid) {
uuid = this.generateUUID();
await GM.setValue('userUUID', uuid);
}
// Cache the UUID for future use
this.cachedUUID = uuid;
return uuid;
}
// Collect all syncable data
async collectSyncData() {
const allKeys = await GM.listValues();
const syncData = {
version: CURRENT_VERSION,
timestamp: new Date().toISOString(),
userUUID: await this.getUserUUID(),
data: {}
};
// Define which keys to sync
const syncableKeys = [
'bookmarkedPages', 'offlineFavorites', 'mustAddTags', 'mustAddTagsEnabled',
'randomPrefLanguage', 'randomPrefTags', 'randomPrefPagesMin', 'randomPrefPagesMax',
'blacklistedTags', 'findSimilarEnabled', 'bookmarksEnabled', 'maxTagsToSelect',
'showNonEnglish', 'showPageNumbersEnabled', 'maxMangaPerBookmark',
'englishFilterEnabled', 'autoLoginEnabled','findAltmangaEnabled',
'bookmarksEnabled', 'language', 'tags', 'pagesMin', 'pagesMax', 'matchAllTags',
'mustAddTagsEnabled', 'findAltMangaThumbnailEnabled', 'openInNewTabEnabled',
'mangaBookMarkingButtonEnabled', 'mangaBookMarkingType', 'bookmarkArrangementType',
'monthFilterEnabled', 'tooltipsEnabled', 'mangagroupingenabled', 'maxMangaPerBookmark',
'openInNewTabType', 'offlineFavoritingEnabled', 'offlineFavoritesPageEnabled',
'nfmPageEnabled', 'publicSyncEnabled', 'privateSyncEnabled',
'autoSyncEnabled', 'syncInterval', 'lastSyncUpload',
'lastSyncDownload', 'bookmarksPageEnabled', 'replaceRelatedWithBookmarks',
'enableRelatedFlipButton', 'twitterButtonEnabled', 'enableRandomButton',
'randomOpenType', 'profileButtonEnabled', 'infoButtonEnabled', 'logoutButtonEnabled',
'bookmarkLinkEnabled', 'findSimilarType', 'bookmarkedMangas'
];
for (const key of syncableKeys) {
if (allKeys.includes(key)) {
syncData.data[key] = await GM.getValue(key);
}
}
return syncData;
}
// Apply synced data
async applySyncData(syncData) {
if (!syncData || !syncData.data) {
throw new Error('Invalid sync data format');
}
const currentUUID = await this.getUserUUID();
if (syncData.userUUID && syncData.userUUID !== currentUUID) {
const confirmMerge = confirm(
`This data belongs to a different user (${syncData.userUUID}). ` +
`Your UUID is ${currentUUID}. Do you want to merge this data anyway?`
);
if (!confirmMerge) {
throw new Error('User cancelled data merge');
}
}
let appliedCount = 0;
for (const [key, value] of Object.entries(syncData.data)) {
await GM.setValue(key, value);
appliedCount++;
}
await GM.setValue('lastSyncDownload', new Date().toISOString());
return appliedCount;
}
// Upload data using specified provider (supports multiple users)
async uploadData(providerType, config) {
const provider = this.providers[providerType];
if (!provider) {
throw new Error(`Unknown provider: ${providerType}`);
}
const userSyncData = await this.collectSyncData();
const userUUID = userSyncData.userUUID;
// Download existing data to merge with current user's data
let existingData = {};
try {
existingData = await provider.download(config);
} catch (error) {
// If download fails (e.g., no data exists yet), start with empty object
console.log('No existing data found, creating new storage');
}
// Ensure existingData has the correct structure for multiple users
if (!existingData.users) {
existingData = {
// version: CURRENT_VERSION,
//lastUpdated: new Date().toISOString(),
users: {}
};
}
// Add/update current user's data
existingData.users[userUUID] = userSyncData;
// existingData.lastUpdated = new Date().toISOString();
// existingData.version = CURRENT_VERSION;
await provider.upload(config, existingData);
await GM.setValue('lastSyncUpload', new Date().toISOString());
return userSyncData;
}
// Download data using specified provider (supports multiple users)
async downloadData(providerType, config) {
const provider = this.providers[providerType];
if (!provider) {
throw new Error(`Unknown provider: ${providerType}`);
}
const allData = await provider.download(config);
const userUUID = await this.getUserUUID();
// Handle both old single-user format and new multi-user format
let userSyncData;
if (allData.users && allData.users[userUUID]) {
// New multi-user format
userSyncData = allData.users[userUUID];
} else if (allData.userUUID === userUUID) {
// Old single-user format
userSyncData = allData;
} else if (allData.users) {
// Multi-user format but user not found
const availableUsers = Object.keys(allData.users);
throw new Error(`No data found for UUID ${userUUID}. Available UUIDs: ${availableUsers.join(', ')}`);
} else {
// Single-user format but different user
throw new Error(`Data belongs to UUID ${allData.userUUID}, but your UUID is ${userUUID}`);
}
const appliedCount = await this.applySyncData(userSyncData);
return { syncData: userSyncData, appliedCount, allUsers: allData.users ? Object.keys(allData.users) : [allData.userUUID] };
}
// Get available users from cloud storage without downloading data
async getAvailableUsers(providerType, config) {
const provider = this.providers[providerType];
if (!provider) {
throw new Error(`Unknown provider: ${providerType}`);
}
const allData = await provider.download(config);
if (allData.users) {
// Multi-user format
return Object.keys(allData.users).map(uuid => ({
uuid,
version: allData.users[uuid].version || 'Unknown',
timestamp: allData.users[uuid].timestamp,
dataCount: Object.keys(allData.users[uuid].data || {}).length
}));
} else if (allData.userUUID) {
// Old single-user format
return [{
uuid: allData.userUUID,
version: allData.version || 'Unknown',
timestamp: allData.timestamp,
dataCount: Object.keys(allData.data || {}).length
}];
}
return [];
}
}
// JSONStorage.net provider implementation
class JSONStorageProvider {
async upload(config, data) {
return new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: 'PUT',
url: `${config.url}?apiKey=${config.apiKey}`,
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify(data),
onload: function(response) {
if (response.status === 200) {
resolve(JSON.parse(response.responseText));
} else {
reject(new Error(`Upload failed: ${response.status} ${response.statusText}`));
}
},
onerror: function(error) {
reject(new Error(`Network error: ${error}`));
}
});
});
}
async download(config) {
return new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: 'GET',
url: `${config.url}?apiKey=${config.apiKey}`,
headers: {
'Accept': 'application/json'
},
onload: function(response) {
if (response.status === 200) {
resolve(JSON.parse(response.responseText));
} else {
reject(new Error(`Download failed: ${response.status} ${response.statusText}`));
}
},
onerror: function(error) {
reject(new Error(`Network error: ${error}`));
}
});
});
}
}
// AutoSync Manager Class
// Handles automatic syncing of user data at specified intervals
// Features:
// - Configurable sync intervals (5-1440 minutes)
// - Automatic retry with exponential backoff on errors
// - Support for both public and private sync endpoints
// - Manual trigger capability
// - Data change detection for immediate sync
class AutoSyncManager {
constructor(syncSystem) {
this.syncSystem = syncSystem;
this.intervalId = null;
this.isEnabled = false;
this.intervalMinutes = 30;
this.lastSyncAttempt = null;
this.consecutiveErrors = 0;
this.maxRetries = 3;
}
// Initialize autosync based on current settings
async initialize() {
const autoSyncEnabled = await GM.getValue('autoSyncEnabled', false);
const syncInterval = await GM.getValue('syncInterval', 30);
this.isEnabled = autoSyncEnabled;
this.intervalMinutes = syncInterval;
if (this.isEnabled) {
this.start();
console.log(`AutoSync initialized: enabled=${this.isEnabled}, interval=${this.intervalMinutes} minutes`);
} else {
console.log('AutoSync disabled');
}
}
// Start the autosync timer
start() {
this.stop(); // Clear any existing timer
if (!this.isEnabled) {
console.log('AutoSync not enabled, skipping start');
return;
}
console.log(`AutoSync started with ${this.intervalMinutes} minute interval`);
// Check if sync is needed on script load
this.checkAndPerformSync();
}
// Stop the autosync timer
stop() {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
console.log('AutoSync stopped');
}
}
// Check if sync is needed based on time elapsed since last sync
async checkAndPerformSync() {
if (!this.isEnabled) {
return;
}
const lastAutoSync = await GM.getValue('lastAutoSync', null);
const now = new Date().getTime();
const intervalMs = this.intervalMinutes * 60 * 1000; // Convert minutes to milliseconds
// If no previous sync or enough time has passed, perform sync
if (!lastAutoSync || (now - new Date(lastAutoSync).getTime()) >= intervalMs) {
console.log('AutoSync: Time interval reached, performing sync...');
await this.performAutoSync();
} else {
const timeUntilNextSync = intervalMs - (now - new Date(lastAutoSync).getTime());
const minutesUntilNext = Math.round(timeUntilNextSync / 60000);
console.log(`AutoSync: Next sync in approximately ${minutesUntilNext} minutes`);
}
}
// Update settings and restart if needed
async updateSettings(enabled, intervalMinutes) {
const wasEnabled = this.isEnabled;
const oldInterval = this.intervalMinutes;
this.isEnabled = enabled;
this.intervalMinutes = intervalMinutes;
// Restart if settings changed
if (enabled && (!wasEnabled || oldInterval !== intervalMinutes)) {
this.start();
} else if (!enabled && wasEnabled) {
this.stop();
}
console.log(`AutoSync settings updated: enabled=${enabled}, interval=${intervalMinutes} minutes`);
}
// Perform the actual sync operation
async performAutoSync() {
if (!this.isEnabled) {
return;
}
this.lastSyncAttempt = new Date().toISOString();
console.log('AutoSync: Starting automatic sync...');
try {
// Determine which sync method to use based on user preferences
const publicSyncEnabled = await GM.getValue('publicSyncEnabled', false);
const privateSyncEnabled = await GM.getValue('privateSyncEnabled', false);
let syncPerformed = false;
// Try private sync first if enabled
if (privateSyncEnabled) {
const privateStorageUrl = await GM.getValue('privateStorageUrl', '');
const privateApiKey = await GM.getValue('privateApiKey', '');
if (privateStorageUrl && privateApiKey) {
await this.syncSystem.uploadData('jsonstorage', {
url: privateStorageUrl,
apiKey: privateApiKey
});
console.log('AutoSync: Private sync completed successfully');
syncPerformed = true;
}
}
// Fall back to public sync if private sync wasn't performed and public is enabled
if (!syncPerformed && publicSyncEnabled) {
await this.syncSystem.uploadData('jsonstorage', this.syncSystem.publicConfig);
console.log('AutoSync: Public sync completed successfully');
syncPerformed = true;
}
if (syncPerformed) {
this.consecutiveErrors = 0; // Reset error counter on success
await GM.setValue('lastAutoSync', new Date().toISOString());
} else {
console.log('AutoSync: No sync method enabled, skipping');
}
} catch (error) {
this.consecutiveErrors++;
console.error(`AutoSync error (attempt ${this.consecutiveErrors}):`, error);
// If we've had too many consecutive errors, temporarily disable autosync
if (this.consecutiveErrors >= this.maxRetries) {
console.warn(`AutoSync: Too many consecutive errors (${this.consecutiveErrors}), temporarily disabling for this session`);
this.stop();
// Show a notification to the user if they're on the page
if (typeof showPopup === 'function') {
showPopup(`AutoSync temporarily disabled due to repeated errors: ${error.message}`, {
timeout: 10000
});
}
}
}
}
// Get status information
async getStatus() {
const lastAutoSync = await GM.getValue('lastAutoSync', null);
return {
enabled: this.isEnabled,
intervalMinutes: this.intervalMinutes,
lastSyncAttempt: this.lastSyncAttempt,
lastSuccessfulSync: lastAutoSync,
consecutiveErrors: this.consecutiveErrors,
isRunning: this.intervalId !== null
};
}
// Trigger immediate sync if autosync is enabled and data has changed
async triggerDataChangeSync(changedKey) {
if (!this.isEnabled) {
return;
}
// Define which keys should trigger immediate sync
const syncableKeys = [
'bookmarkedPages', 'offlineFavorites', 'mustAddTags', 'mustAddTagsEnabled',
'randomPrefLanguage', 'randomPrefTags', 'randomPrefPagesMin', 'randomPrefPagesMax',
'blacklistedTags', 'findSimilarEnabled', 'bookmarksEnabled', 'maxTagsToSelect',
'showNonEnglish', 'showPageNumbersEnabled', 'maxMangaPerBookmark'
];
if (syncableKeys.includes(changedKey)) {
console.log(`AutoSync: Data change detected for ${changedKey}, triggering immediate sync`);
// Debounce rapid changes - only sync if last sync was more than 30 seconds ago
const lastSync = await GM.getValue('lastAutoSync', null);
const now = new Date().getTime();
const thirtySecondsAgo = now - 30000;
if (!lastSync || new Date(lastSync).getTime() < thirtySecondsAgo) {
console.log('AutoSync: Performing immediate sync due to data change');
await this.performAutoSync();
} else {
console.log('AutoSync: Skipping immediate sync due to recent sync activity');
}
}
}
}
// Initialize sync system
const syncSystem = new OnlineDataSync();
const autoSyncManager = new AutoSyncManager(syncSystem);
// Helper function to save data and trigger autosync
async function setValueWithAutoSync(key, value) {
await GM.setValue(key, value);
await autoSyncManager.triggerDataChangeSync(key);
}
//------------------------ **Mark as Read System** ------------------
/**
* Mark as Read System - Enhanced version with configurable opacity and auto-mark functionality
*/
class MarkAsReadSystem {
constructor() {
this.readGalleries = new Set();
this.settings = {
enabled: true,
autoMarkEnabled: true,
readOpacity: 0.6,
nonEnglishOpacity: 0.2
};
this.init();
}
/**
* Initialize the Mark as Read system
*/
async init() {
await this.loadSettings();
await this.loadReadGalleries();
if (this.settings.enabled) {
this.addCSS();
this.addMarkAsReadButtons();
this.applyReadStatus();
this.setupAutoMark();
await this.checkAndApplyJustRead(); // New: Check and apply justRead data
}
}
/**
* Load settings from GM storage
*/
async loadSettings() {
this.settings.enabled = await GM.getValue('markAsReadEnabled', true);
this.settings.autoMarkEnabled = await GM.getValue('autoMarkReadEnabled', true);
this.settings.readOpacity = await GM.getValue('readGalleriesOpacity', 0.6);
this.settings.nonEnglishOpacity = await GM.getValue('nonEnglishOpacity', 0.2);
}
/**
* Load read galleries from localStorage
*/
async loadReadGalleries() {
try {
const readList = await GM.getValue('readGalleries', []);
this.readGalleries = new Set(readList);
} catch (error) {
console.error('Error loading read galleries:', error);
this.readGalleries = new Set();
}
}
/**
* Save read galleries to localStorage
*/
async saveReadGalleries() {
try {
await GM.setValue('readGalleries', Array.from(this.readGalleries));
} catch (error) {
console.error('Error saving read galleries:', error);
}
}
/**
* Check for 'justRead' key in localStorage, apply, and remove it.
*/
async checkAndApplyJustRead() {
const justReadData = localStorage.getItem('justRead');
if (justReadData) {
try {
const galleries = JSON.parse(justReadData);
if (Array.isArray(galleries)) {
let markedCount = 0;
for (const gallery of galleries) {
if (gallery && gallery.id && !this.readGalleries.has(gallery.id)) {
this.readGalleries.add(gallery.id);
await this.cacheGalleryData(gallery.id, gallery.title, gallery.coverImageUrl);
markedCount++;
}
}
if (markedCount > 0) {
await this.saveReadGalleries();
this.applyReadStatus(); // Re-apply styles to reflect new reads
this.showAutoMarkNotification(markedCount);
}
}
} catch (error) {
console.error('Error parsing justRead data from localStorage:', error);
} finally {
localStorage.removeItem('justRead'); // Always remove after processing
}
}
}
/**
* Show a notification for auto-marked galleries.
*/
showAutoMarkNotification(count) {
// Placeholder for notification logic. Implement as needed.
console.log(`Auto-marked ${count} galleries as read from 'justRead' data.`);
// Example: You might want to use a more visible notification system here.
// For instance, a temporary div that fades out, similar to the changelog popup.
}
/**
* Extract gallery ID from URL or element
*/
extractGalleryId(url) {
if (!url) return null;
const match = url.match(/\/g\/(\d+)\//);
return match ? match[1] : null;
}
/**
* Check if a gallery is marked as read
*/
isRead(galleryId) {
return this.readGalleries.has(galleryId);
}
/**
* Mark a gallery as read
*/
async markAsRead(galleryId) {
if (!galleryId) return;
this.readGalleries.add(galleryId);
await this.saveReadGalleries();
// Cache gallery data for Read Manga page
await this.cacheGalleryData(galleryId);
this.updateGalleryVisuals(galleryId);
}
/**
* Unmark a gallery as read
*/
async unmarkAsRead(galleryId) {
if (!galleryId) return;
this.readGalleries.delete(galleryId);
await this.saveReadGalleries();
this.updateGalleryVisuals(galleryId);
}
/**
* Toggle read status of a gallery
*/
async toggleReadStatus(galleryId) {
if (this.isRead(galleryId)) {
await this.unmarkAsRead(galleryId);
} else {
await this.markAsRead(galleryId);
}
}
/**
* Add CSS styles for the mark as read system
*/
addCSS() {
const css = `
/* Mark as Read Button Styles */
.mark-as-read-btn {
position: absolute;
top: 5px;
right: 5px;
width: 24px;
height: 24px;
background: rgba(0, 0, 0, 0.7);
border: none;
border-radius: 50%;
color: #fff;
cursor: pointer;
font-size: 12px;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.mark-as-read-btn:hover {
background: rgba(0, 0, 0, 0.9);
transform: scale(1.1);
}
.mark-as-read-btn.read {
background: rgba(46, 125, 50, 0.8);
color: #fff;
}
.mark-as-read-btn.read:hover {
background: rgba(46, 125, 50, 1);
}
/* Read Gallery Badge */
.read-badge {
position: absolute;
top: 5px;
left: 5px;
background: rgba(46, 125, 50, 0.9);
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
font-weight: bold;
z-index: 10;
pointer-events: none;
}
/* Gallery container positioning */
.gallery {
position: relative;
}
/* Read gallery opacity */
.gallery.read-gallery .cover img,
.gallery.read-gallery .cover .caption {
opacity: var(--read-opacity, 0.6);
transition: opacity 0.3s ease;
}
.gallery.read-gallery:hover .cover img,
.gallery.read-gallery:hover .cover .caption {
opacity: 1;
}
/* Animation for marking as read */
@keyframes markAsReadAnimation {
0% { transform: scale(1); }
50% { transform: scale(1.2); }
100% { transform: scale(1); }
}
.mark-as-read-animation {
animation: markAsReadAnimation 0.3s ease;
}
/* Custom Mark as Read Button for Gallery Pages */
.btn-nhi-mark-as-read {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
border-radius: 6px;
text-decoration: none;
font-weight: 500;
transition: all 0.3s ease;
border: 1px solid transparent;
cursor: pointer;
user-select: none;
}
.btn-nhi-mark-as-read:hover {
text-decoration: none;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
.btn-nhi-mark-as-read:active {
transform: translateY(0);
}
.btn-nhi-mark-as-read svg {
transition: transform 0.3s ease;
}
.btn-nhi-mark-as-read:hover svg {
transform: scale(1.1);
}
/* Success state styling */
.btn-nhi-mark-as-read.btn-success {
color: white;
border-color: rgba(255, 255, 255, 0.2);
}
.btn-nhi-mark-as-read.btn-success:hover {
background: linear-gradient(135deg, #2e7d32 0%, #1b5e20 100%) !important;
color: white;
}
`;
GM.addStyle(css);
// Set CSS custom property for read opacity
document.documentElement.style.setProperty('--read-opacity', this.settings.readOpacity);
}
/**
* Add mark as read buttons to gallery thumbnails
*/
addMarkAsReadButtons() {
// Don't add mark-as-read buttons on the read-manga page
if (window.location.pathname === '/read-manga/' ||
window.location.hash === '#read-manga' ||
document.body.classList.contains('read-manga-active') ||
document.querySelector('.read-manga-page')) {
return;
}
const galleries = document.querySelectorAll('.gallery');
galleries.forEach(gallery => {
// Skip if button already exists
if (gallery.querySelector('.mark-as-read-btn')) return;
// Skip if this is a read-manga-gallery (from read manga page)
if (gallery.classList.contains('read-manga-gallery')) return;
const coverLink = gallery.querySelector('.cover');
if (!coverLink) return;
const galleryId = this.extractGalleryId(coverLink.getAttribute('href'));
if (!galleryId) return;
const button = document.createElement('button');
button.className = 'mark-as-read-btn';
button.title = 'Mark as Read';
button.innerHTML = this.isRead(galleryId) ? '✓' : '○';
if (this.isRead(galleryId)) {
button.classList.add('read');
}
button.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
button.classList.add('mark-as-read-animation');
await this.toggleReadStatus(galleryId);
setTimeout(() => {
button.classList.remove('mark-as-read-animation');
}, 300);
});
gallery.appendChild(button);
});
}
/**
* Apply read status visual effects to galleries
*/
applyReadStatus() {
const galleries = document.querySelectorAll('.gallery');
galleries.forEach(gallery => {
const coverLink = gallery.querySelector('.cover');
if (!coverLink) return;
const galleryId = this.extractGalleryId(coverLink.getAttribute('href'));
if (!galleryId) return;
if (this.isRead(galleryId)) {
gallery.classList.add('read-gallery');
this.addReadBadge(gallery);
} else {
gallery.classList.remove('read-gallery');
this.removeReadBadge(gallery);
}
});
}
/**
* Add read badge to a gallery
*/
addReadBadge(gallery) {
if (gallery.querySelector('.read-badge')) return;
const badge = document.createElement('div');
badge.className = 'read-badge';
badge.textContent = 'READ';
gallery.appendChild(badge);
}
/**
* Remove read badge from a gallery
*/
removeReadBadge(gallery) {
const badge = gallery.querySelector('.read-badge');
if (badge) {
badge.remove();
}
}
/**
* Update visual state of a specific gallery
*/
updateGalleryVisuals(galleryId) {
const galleries = document.querySelectorAll('.gallery');
galleries.forEach(gallery => {
const coverLink = gallery.querySelector('.cover');
if (!coverLink) return;
const currentId = this.extractGalleryId(coverLink.getAttribute('href'));
if (currentId !== galleryId) return;
const button = gallery.querySelector('.mark-as-read-btn');
const isRead = this.isRead(galleryId);
if (button) {
button.innerHTML = isRead ? '✓' : '○';
button.title = isRead ? 'Mark as Unread' : 'Mark as Read';
button.classList.toggle('read', isRead);
}
gallery.classList.toggle('read-gallery', isRead);
if (isRead) {
this.addReadBadge(gallery);
} else {
this.removeReadBadge(gallery);
}
});
}
/**
* Setup auto-mark functionality for individual gallery pages
*/
setupAutoMark() {
if (!this.settings.autoMarkEnabled) return;
// Check if we're on a gallery page
const galleryMatch = window.location.pathname.match(/\/g\/(\d+)\//);
if (!galleryMatch) return;
const galleryId = galleryMatch[1];
// Add custom mark-as-read button to gallery page
this.addGalleryPageMarkButton(galleryId);
// Check if we're on the last page of the gallery
this.checkLastPage(galleryId);
}
/**
* Add custom mark-as-read button to individual gallery pages
*/
addGalleryPageMarkButton(galleryId) {
// Check if button already exists
if (document.getElementById('nhi-mar-button')) return;
// Find the specific .buttons container
const buttonsContainer = document.querySelector('.buttons');
if (!buttonsContainer) {
console.warn('Buttons container not found, mark-as-read button will not be added');
return;
}
// Create the custom mark-as-read button to match the existing button style
const isRead = this.isRead(galleryId);
const buttonHTML = `
<a href="#" id="nhi-mar-button" class="btn ${isRead ? 'btn-success' : 'btn-secondary'} btn-enabled tooltip btn-nhi-mark-as-read">
<i class="fas fa-book"></i>
<span>${isRead ? 'Mark as unread' : 'Mark as read'}</span>
<div class="top">${isRead ? 'Remove from read list' : 'Mark this manga as read'}<i></i></div>
</a>
`;
// Insert the button
const buttonElement = document.createElement('div');
buttonElement.innerHTML = buttonHTML;
const button = buttonElement.firstElementChild;
// Add click event listener using shared method
this.addButtonEventHandling(button, galleryId);
// Insert button into the .buttons container
buttonsContainer.appendChild(button);
}
/**
* Add event handling to mark-as-read button
*/
addButtonEventHandling(button, galleryId) {
button.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
const originalText = button.querySelector('span').textContent;
button.querySelector('span').textContent = 'Processing...';
button.style.opacity = '0.7';
try {
await this.toggleReadStatus(galleryId);
const newIsRead = this.isRead(galleryId);
button.querySelector('span').textContent = newIsRead ? 'Mark as unread' : 'Mark as read';
if (button.querySelector('.top')) {
button.querySelector('.top').innerHTML = `${newIsRead ? 'Remove from read list' : 'Mark this manga as read'}<i></i>`;
}
if (newIsRead) {
button.classList.remove('btn-secondary');
button.classList.add('btn-success');
this.showMarkNotification('Gallery marked as read!');
} else {
button.classList.remove('btn-success');
button.classList.add('btn-secondary');
this.showMarkNotification('Gallery marked as unread!');
}
button.style.transform = 'scale(1.05)';
setTimeout(() => {
button.style.transform = 'scale(1)';
}, 200);
} catch (error) {
console.error('Error toggling read status:', error);
button.querySelector('span').textContent = originalText;
this.showMarkNotification('Error updating read status', 'error');
}
button.style.opacity = '1';
});
}
/**
* Check if user is on the last page and auto-mark if enabled
*/
async checkLastPage(galleryId) {
// Look for the specific page number button to determine current and total pages
const pageNumberButton = document.querySelector('.page-number.btn.btn-unstyled');
if (!pageNumberButton) {
console.log('Page number button not found, skipping auto-mark check');
return;
}
const currentPageSpan = pageNumberButton.querySelector('.current');
const totalPagesSpan = pageNumberButton.querySelector('.num-pages');
if (!currentPageSpan || !totalPagesSpan) {
console.log('Current or total page spans not found');
return;
}
const currentPage = parseInt(currentPageSpan.textContent.trim());
const totalPages = parseInt(totalPagesSpan.textContent.trim());
console.log(`Auto-mark check: Page ${currentPage} of ${totalPages}`);
// If we're on the last page, mark as read
if (currentPage === totalPages && totalPages > 1) {
console.log(`Reached last page (${currentPage}/${totalPages}), marking as read`);
// For auto-mark, we need to cache the cover image from the first page
await this.cacheGalleryDataFromFirstPage(galleryId);
await this.markAsRead(galleryId);
this.showAutoMarkNotification();
}
}
/**
* Show notification when gallery is auto-marked as read
*/
showAutoMarkNotification() {
this.showMarkNotification('Gallery automatically marked as read!');
}
/**
* Show notification for mark as read actions
*/
showMarkNotification(message, type = 'success') {
const notification = document.createElement('div');
const backgroundColor = type === 'error' ? 'rgba(244, 67, 54, 0.9)' : 'rgba(46, 125, 50, 0.9)';
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: ${backgroundColor};
color: white;
padding: 12px 20px;
border-radius: 8px;
z-index: 10000;
font-size: 14px;
font-weight: 500;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.2);
animation: slideInRight 0.4s ease;
max-width: 300px;
`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.opacity = '0';
notification.style.transform = 'translateX(100px)';
notification.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
setTimeout(() => notification.remove(), 300);
}, type === 'error' ? 4000 : 2000);
}
/**
* Cache gallery data for Read Manga page
*/
async cacheGalleryData(galleryId, title = null, coverImageUrl = null) {
try {
const cachedData = await GM.getValue('readGalleriesCache', {});
// Skip if already cached
if (cachedData[galleryId]) return;
const galleryInfo = {
id: galleryId,
title: title || 'Gallery ' + galleryId,
thumbnail: coverImageUrl || null,
pages: 'Unknown',
tags: [],
url: `/g/${galleryId}/`,
cached: true,
cachedAt: new Date().toISOString()
};
// Try to get data from current page
// Method 1: Check if we're on the gallery page
const galleryMatch = window.location.pathname.match(/\/g\/(\d+)\//);
if (galleryMatch && galleryMatch[1] === galleryId) {
// Get title from h1 or h2
const titleElement = document.querySelector('h1, h2');
if (titleElement) {
galleryInfo.title = titleElement.textContent.trim();
}
// Get cover image from #cover
const coverElement = document.querySelector('#cover img');
if (coverElement && coverElement.src) {
galleryInfo.thumbnail = coverElement.src;
}
// Get page count from page number button
const pageNumberButton = document.querySelector('.page-number.btn.btn-unstyled .num-pages');
if (pageNumberButton) {
galleryInfo.pages = pageNumberButton.textContent.trim();
}
// Get tags from tag containers
const tagElements = document.querySelectorAll('.tag-container .tag .name');
galleryInfo.tags = Array.from(tagElements).map(tag => tag.textContent.trim());
}
// Method 2: Check if we're on a listing page and can find the gallery
const galleryElement = document.querySelector(`[data-gallery-id="${galleryId}"], .gallery a[href*="/g/${galleryId}/"]`);
if (galleryElement) {
const gallery = galleryElement.closest('.gallery') || galleryElement;
const titleElement = gallery.querySelector('.caption');
if (titleElement) {
galleryInfo.title = titleElement.textContent.trim();
}
const imgElement = gallery.querySelector('img');
if (imgElement && imgElement.src) {
galleryInfo.thumbnail = imgElement.src;
}
}
// Save to cache
cachedData[galleryId] = galleryInfo;
await GM.setValue('readGalleriesCache', cachedData);
console.log(`Cached gallery data for ${galleryId}:`, galleryInfo);
} catch (error) {
console.error('Error caching gallery data:', error);
}
}
/**
* Cache gallery data from first page (for auto-mark scenarios)
*/
async cacheGalleryDataFromFirstPage(galleryId) {
try {
const cachedData = await GM.getValue('readGalleriesCache', {});
// Skip if already cached
if (cachedData[galleryId]) return;
const galleryInfo = {
id: galleryId,
title: 'Gallery ' + galleryId,
thumbnail: null,
pages: 'Unknown',
tags: [],
url: `/g/${galleryId}/`,
cached: true,
cachedAt: new Date().toISOString()
};
// Try to get data from current page first (we might be on a gallery page)
const galleryMatch = window.location.pathname.match(/\/g\/(\d+)\//);
if (galleryMatch && galleryMatch[1] === galleryId) {
// Get title from h1 or h2
const titleElement = document.querySelector('h1, h2');
if (titleElement) {
galleryInfo.title = titleElement.textContent.trim();
}
// Get page count from page number button
const pageNumberButton = document.querySelector('.page-number.btn.btn-unstyled .num-pages');
if (pageNumberButton) {
galleryInfo.pages = pageNumberButton.textContent.trim();
}
// Get tags from tag containers
const tagElements = document.querySelectorAll('.tag-container .tag .name');
galleryInfo.tags = Array.from(tagElements).map(tag => tag.textContent.trim());
}
// For auto-mark scenarios, try to fetch the cover from the first page
try {
const firstPageUrl = `/g/${galleryId}/1/`;
console.log(`Fetching cover image from first page: ${firstPageUrl}`);
const response = await fetch(firstPageUrl);
if (response.ok) {
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// Get the cover image from #image-container img
const coverImg = doc.querySelector('#image-container img');
if (coverImg && coverImg.src) {
galleryInfo.thumbnail = coverImg.src;
console.log(`Found cover image: ${coverImg.src}`);
}
// Also try to get title if we don't have it
if (galleryInfo.title === `Gallery ${galleryId}`) {
const titleElement = doc.querySelector('h1, h2');
if (titleElement) {
galleryInfo.title = titleElement.textContent.trim();
}
}
}
} catch (fetchError) {
console.log(`Could not fetch first page for gallery ${galleryId}:`, fetchError);
// Fallback: try to construct thumbnail URL
const thumbnailUrl = `https://t.nhentai.net/galleries/${galleryId}/thumb.jpg`;
try {
const thumbResponse = await fetch(thumbnailUrl, { method: 'HEAD' });
if (thumbResponse.ok) {
galleryInfo.thumbnail = thumbnailUrl;
console.log(`Using fallback thumbnail: ${thumbnailUrl}`);
}
} catch (thumbError) {
console.log(`Fallback thumbnail also failed for gallery ${galleryId}`);
}
}
// Save to cache
cachedData[galleryId] = galleryInfo;
await GM.setValue('readGalleriesCache', cachedData);
console.log(`Cached gallery data from first page for ${galleryId}:`, galleryInfo);
} catch (error) {
console.error('Error caching gallery data from first page:', error);
}
}
}
// Initialize Mark as Read System
let markAsReadSystem;
async function initMarkAsReadSystem() {
const enabled = await GM.getValue('markAsReadEnabled', true);
if (enabled) {
markAsReadSystem = new MarkAsReadSystem();
}
}
// Initialize on page load and when navigating
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initMarkAsReadSystem);
} else {
initMarkAsReadSystem();
}
//------------------------ **Enhanced Opacity/Fade System** ------------------
/**
* Enhanced Opacity/Fade Configuration System
* Provides configurable opacity settings for non-English and read galleries
*/
class OpacityFadeSystem {
constructor() {
this.settings = {
nonEnglishOpacity: 0.2,
readGalleriesOpacity: 0.6,
showNonEnglish: 'show' // 'show', 'hide', 'fade'
};
this.init();
}
/**
* Initialize the opacity/fade system
*/
async init() {
await this.loadSettings();
this.addCSS();
this.applyOpacitySettings();
this.setupRealTimePreview();
}
/**
* Load settings from GM storage
*/
async loadSettings() {
this.settings.nonEnglishOpacity = await GM.getValue('nonEnglishOpacity', 0.2);
this.settings.readGalleriesOpacity = await GM.getValue('readGalleriesOpacity', 0.6);
this.settings.showNonEnglish = await GM.getValue('showNonEnglish', 'show');
}
/**
* Add CSS for opacity/fade effects
*/
addCSS() {
const css = `
/* Enhanced Opacity System */
:root {
--non-english-opacity: ${this.settings.nonEnglishOpacity};
--read-galleries-opacity: ${this.settings.readGalleriesOpacity};
}
/* Non-English gallery fading */
.gallery:not([data-tags~='12227']) .cover img,
.gallery:not([data-tags~='12227']) .cover .caption {
opacity: var(--non-english-opacity);
transition: opacity 0.3s ease;
}
.gallery:not([data-tags~='12227']):hover .cover img,
.gallery:not([data-tags~='12227']):hover .cover .caption {
opacity: 1;
}
/* Read galleries fading (handled by MarkAsReadSystem) */
.gallery.read-gallery .cover img,
.gallery.read-gallery .cover .caption {
opacity: var(--read-galleries-opacity) !important;
transition: opacity 0.3s ease;
}
.gallery.read-gallery:hover .cover img,
.gallery.read-gallery:hover .cover .caption {
opacity: 1 !important;
}
/* Hide non-English galleries when showNonEnglish is 'hide' */
body[data-show-non-english="hide"] .gallery:not([data-tags~='12227']) {
display: none !important;
}
/* Fade non-English galleries when showNonEnglish is 'fade' */
body[data-show-non-english="fade"] .gallery:not([data-tags~='12227']) .cover img,
body[data-show-non-english="fade"] .gallery:not([data-tags~='12227']) .cover .caption {
opacity: var(--non-english-opacity);
}
/* Show all galleries when showNonEnglish is 'show' */
body[data-show-non-english="show"] .gallery:not([data-tags~='12227']) .cover img,
body[data-show-non-english="show"] .gallery:not([data-tags~='12227']) .cover .caption {
opacity: 1;
}
/* Real-time preview styles */
.opacity-preview {
border: 2px solid #e63946 !important;
box-shadow: 0 0 10px rgba(230, 57, 70, 0.5) !important;
}
`;
GM.addStyle(css);
}
/**
* Apply opacity settings to the page
*/
applyOpacitySettings() {
// Update CSS custom properties
document.documentElement.style.setProperty('--non-english-opacity', this.settings.nonEnglishOpacity);
document.documentElement.style.setProperty('--read-galleries-opacity', this.settings.readGalleriesOpacity);
// Set body attribute for non-English display mode
document.body.setAttribute('data-show-non-english', this.settings.showNonEnglish);
}
/**
* Setup real-time preview for opacity sliders
*/
setupRealTimePreview() {
// Non-English opacity slider
const nonEnglishSlider = document.getElementById('nonEnglishOpacity');
if (nonEnglishSlider) {
nonEnglishSlider.addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
this.previewNonEnglishOpacity(value);
});
nonEnglishSlider.addEventListener('change', async (e) => {
const value = parseFloat(e.target.value);
this.settings.nonEnglishOpacity = value;
await GM.setValue('nonEnglishOpacity', value);
this.applyOpacitySettings();
this.clearPreview();
});
}
// Read galleries opacity slider
const readSlider = document.getElementById('readGalleriesOpacity');
if (readSlider) {
readSlider.addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
this.previewReadGalleriesOpacity(value);
});
readSlider.addEventListener('change', async (e) => {
const value = parseFloat(e.target.value);
this.settings.readGalleriesOpacity = value;
await GM.setValue('readGalleriesOpacity', value);
this.applyOpacitySettings();
this.clearPreview();
});
}
// Show non-English select
const showNonEnglishSelect = document.getElementById('showNonEnglishSelect');
if (showNonEnglishSelect) {
showNonEnglishSelect.addEventListener('change', async (e) => {
this.settings.showNonEnglish = e.target.value;
await GM.setValue('showNonEnglish', e.target.value);
this.applyOpacitySettings();
});
}
}
/**
* Preview non-English opacity changes
*/
previewNonEnglishOpacity(opacity) {
document.documentElement.style.setProperty('--non-english-opacity', opacity);
// Add preview styling to non-English galleries
const nonEnglishGalleries = document.querySelectorAll('.gallery:not([data-tags~="12227"])');
nonEnglishGalleries.forEach(gallery => {
gallery.classList.add('opacity-preview');
});
}
/**
* Preview read galleries opacity changes
*/
previewReadGalleriesOpacity(opacity) {
document.documentElement.style.setProperty('--read-galleries-opacity', opacity);
// Add preview styling to read galleries
const readGalleries = document.querySelectorAll('.gallery.read-gallery');
readGalleries.forEach(gallery => {
gallery.classList.add('opacity-preview');
});
}
/**
* Clear preview styling
*/
clearPreview() {
const previewElements = document.querySelectorAll('.opacity-preview');
previewElements.forEach(element => {
element.classList.remove('opacity-preview');
});
}
/**
* Reset to default values
*/
async resetToDefaults() {
this.settings.nonEnglishOpacity = 0.2;
this.settings.readGalleriesOpacity = 0.6;
await GM.setValue('nonEnglishOpacity', 0.2);
await GM.setValue('readGalleriesOpacity', 0.6);
// Update UI
const nonEnglishSlider = document.getElementById('nonEnglishOpacity');
const readSlider = document.getElementById('readGalleriesOpacity');
const nonEnglishValue = document.getElementById('nonEnglishOpacityValue');
const readValue = document.getElementById('readGalleriesOpacityValue');
if (nonEnglishSlider) {
nonEnglishSlider.value = 0.2;
if (nonEnglishValue) nonEnglishValue.textContent = '0.2';
}
if (readSlider) {
readSlider.value = 0.6;
if (readValue) readValue.textContent = '0.6';
}
this.applyOpacitySettings();
}
}
// Initialize Enhanced Opacity/Fade System
let opacityFadeSystem;
async function initOpacityFadeSystem() {
opacityFadeSystem = new OpacityFadeSystem();
}
// Initialize on page load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initOpacityFadeSystem);
} else {
initOpacityFadeSystem();
}
//------------------------ **Enhanced Opacity/Fade System** ------------------
//------------------------ **Improved Language Detection System** ------------------
/**
* Improved Language Detection System
* Uses DOM parsing of existing tag elements with fallback to title-based heuristics
*/
class LanguageDetectionSystem {
constructor() {
this.languageMap = {
'12227': 'english',
'6346': 'japanese',
'29963': 'chinese',
'19440': 'korean',
'16934': 'spanish',
'1': 'french',
'5973': 'german',
'21613': 'italian',
'11261': 'portuguese',
'18334': 'russian',
'22830': 'thai',
'28288': 'vietnamese'
};
this.titleLanguagePatterns = {
english: /^[a-zA-Z0-9\s\[\]\(\)\-_!@#$%^&*+={}|\\:";'<>?,./~`]+$/,
japanese: /[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF]/,
chinese: /[\u4E00-\u9FFF]/,
korean: /[\uAC00-\uD7AF\u1100-\u11FF\u3130-\u318F]/
};
this.init();
}
/**
* Initialize the language detection system
*/
init() {
this.detectAndMarkLanguages();
this.setupLanguageObserver();
}
/**
* Detect and mark languages for all galleries on the page
*/
detectAndMarkLanguages() {
const galleries = document.querySelectorAll('.gallery');
galleries.forEach(gallery => {
const language = this.detectGalleryLanguage(gallery);
if (language) {
gallery.setAttribute('data-detected-language', language);
this.addLanguageFlag(gallery, language);
}
});
}
/**
* Detect the language of a specific gallery
*/
detectGalleryLanguage(gallery) {
// Method 1: Parse existing data-tags attribute
const dataTags = gallery.getAttribute('data-tags');
if (dataTags) {
const language = this.detectLanguageFromTags(dataTags);
if (language) return language;
}
// Method 2: Parse tag elements in gallery listings
const tagElements = gallery.querySelectorAll('.tag');
if (tagElements.length > 0) {
const language = this.detectLanguageFromTagElements(tagElements);
if (language) return language;
}
// Method 3: Fallback to title-based heuristic
const titleElement = gallery.querySelector('.caption');
if (titleElement) {
const language = this.detectLanguageFromTitle(titleElement.textContent);
if (language) return language;
}
// Default to unknown if no language detected
return 'unknown';
}
/**
* Detect language from data-tags attribute
*/
detectLanguageFromTags(dataTags) {
const tags = dataTags.split(' ');
for (const [tagId, language] of Object.entries(this.languageMap)) {
if (tags.includes(tagId)) {
return language;
}
}
return null;
}
/**
* Detect language from tag elements
*/
detectLanguageFromTagElements(tagElements) {
for (const tagElement of tagElements) {
const tagText = tagElement.textContent.toLowerCase().trim();
// Check for language: prefix
if (tagText.startsWith('language:')) {
const language = tagText.replace('language:', '').trim();
if (Object.values(this.languageMap).includes(language)) {
return language;
}
}
// Check for direct language matches
if (Object.values(this.languageMap).includes(tagText)) {
return tagText;
}
}
return null;
}
/**
* Detect language from title using heuristics
*/
detectLanguageFromTitle(title) {
if (!title) return null;
// Check for Japanese characters
if (this.titleLanguagePatterns.japanese.test(title)) {
return 'japanese';
}
// Check for Chinese characters
if (this.titleLanguagePatterns.chinese.test(title)) {
return 'chinese';
}
// Check for Korean characters
if (this.titleLanguagePatterns.korean.test(title)) {
return 'korean';
}
// Check if it's primarily English (ASCII characters)
if (this.titleLanguagePatterns.english.test(title)) {
return 'english';
}
return null;
}
/**
* Add language flag to gallery
*/
addLanguageFlag(gallery, language) {
const caption = gallery.querySelector('.caption');
if (!caption) return;
// Remove all existing flag elements (both old and new systems)
const existingFlags = gallery.querySelectorAll('.language-flag, .overlayFlag');
existingFlags.forEach(flag => flag.remove());
// Remove inline flag images from caption text content
const captionTextNodes = Array.from(caption.childNodes).filter(node => node.nodeType === Node.TEXT_NODE);
captionTextNodes.forEach(textNode => {
// Check if this text node contains inline flag HTML
if (textNode.textContent && textNode.textContent.includes('<img')) {
// This shouldn't happen with text nodes, but let's be safe
textNode.remove();
}
});
// Remove inline flag images that might be direct children
const inlineFlags = caption.querySelectorAll('img[style*="margin-right: 5px"][style*="vertical-align: middle"]');
inlineFlags.forEach(flag => flag.remove());
// Clean up caption innerHTML to remove any remaining inline flag HTML
if (caption.innerHTML.includes('style="margin-right: 5px; vertical-align: middle; height: 12px"')) {
// Extract just the text content, removing any inline flag HTML
const tempDiv = document.createElement('div');
tempDiv.innerHTML = caption.innerHTML;
// Remove all img elements with the inline flag styling
const inlineImgs = tempDiv.querySelectorAll('img[style*="margin-right: 5px"]');
inlineImgs.forEach(img => img.remove());
// Get the cleaned text content
const cleanText = tempDiv.textContent || tempDiv.innerText || '';
// Preserve any matching-tags divs
const matchingTagsDiv = caption.querySelector('.matching-tags');
// Clear caption and add back clean text
caption.innerHTML = cleanText;
// Re-add matching tags if they existed
if (matchingTagsDiv) {
caption.appendChild(matchingTagsDiv);
}
}
const flagUrls = {
english: "https://i.imgur.com/vSnHmmi.gif",
japanese: "https://i.imgur.com/GlArpuS.gif",
chinese: "https://i.imgur.com/7B55DYm.gif",
korean: "https://i.imgur.com/placeholder-kr.gif", // Add actual Korean flag URL
spanish: "https://i.imgur.com/placeholder-es.gif", // Add actual Spanish flag URL
// Add more flag URLs as needed
};
if (flagUrls[language]) {
const flag = document.createElement('img');
flag.className = 'language-flag overlayFlag';
flag.src = flagUrls[language];
flag.alt = language;
flag.title = language.charAt(0).toUpperCase() + language.slice(1);
// Add the new flag to caption area
caption.appendChild(flag);
}
}
/**
* Setup observer for dynamically loaded content
*/
setupLanguageObserver() {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
// Check if the added node is a gallery
if (node.classList && node.classList.contains('gallery')) {
const language = this.detectGalleryLanguage(node);
if (language) {
node.setAttribute('data-detected-language', language);
this.addLanguageFlag(node, language);
}
}
// Check for galleries within the added node
const galleries = node.querySelectorAll && node.querySelectorAll('.gallery');
if (galleries) {
galleries.forEach(gallery => {
const language = this.detectGalleryLanguage(gallery);
if (language) {
gallery.setAttribute('data-detected-language', language);
this.addLanguageFlag(gallery, language);
}
});
}
}
});
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
/**
* Get language statistics for the current page
*/
getLanguageStats() {
const galleries = document.querySelectorAll('.gallery');
const stats = {};
galleries.forEach(gallery => {
const language = gallery.getAttribute('data-detected-language') || 'unknown';
stats[language] = (stats[language] || 0) + 1;
});
return stats;
}
/**
* Filter galleries by language
*/
filterByLanguage(language) {
const galleries = document.querySelectorAll('.gallery');
galleries.forEach(gallery => {
const galleryLanguage = gallery.getAttribute('data-detected-language');
if (language === 'all') {
gallery.style.display = '';
} else if (galleryLanguage === language) {
gallery.style.display = '';
} else {
gallery.style.display = 'none';
}
});
}
}
// Initialize Language Detection System
let languageDetectionSystem;
async function initLanguageDetectionSystem() {
languageDetectionSystem = new LanguageDetectionSystem();
}
// Initialize on page load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initLanguageDetectionSystem);
} else {
initLanguageDetectionSystem();
}
//------------------------ **Improved Language Detection System** ------------------
//------------------------ **Tag Warning & Blacklist System** ------------------
/**
* Tag Warning & Blacklist System
* Two-tier system with red blacklist badges and orange warning badges
*/
class TagWarningSystem {
constructor() {
this.settings = {
enabled: true,
blacklistTags: ['scat', 'guro', 'vore', 'ryona', 'snuff'],
warningTags: ['ntr', 'netorare', 'cheating', 'ugly bastard', 'mind break'],
favoriteTags: []
};
this.init();
}
/**
* Initialize the tag warning system
*/
async init() {
await this.loadSettings();
if (this.settings.enabled) {
this.addCSS();
this.processGalleries();
this.processGalleryPage();
this.setupObserver();
}
}
/**
* Load settings from GM storage
*/
async loadSettings() {
this.settings.enabled = await GM.getValue('tagWarningEnabled', true);
this.settings.blacklistTags = await GM.getValue('blacklistTagsList', ['scat', 'guro', 'vore', 'ryona', 'snuff']);
this.settings.warningTags = await GM.getValue('warningTagsList', ['ntr', 'netorare', 'cheating', 'ugly bastard', 'mind break']);
this.settings.favoriteTags = await GM.getValue('favoriteTagsList', []);
}
/**
* Add CSS for tag warning badges
*/
addCSS() {
const css = `
/* Tag Warning Badges */
.tag-warning-badge {
position: absolute;
bottom: 5px;
left: 5px;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
font-weight: bold;
color: white;
z-index: 10;
pointer-events: none;
text-transform: uppercase;
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
}
.tag-warning-badge.blacklist {
background: rgba(244, 67, 54, 0.9);
}
.tag-warning-badge.warning {
background: rgba(255, 152, 0, 0.9);
}
.tag-warning-badge.favorite {
background: rgba(33, 150, 243, 0.9);
}
/* Multiple badges stacking */
.tag-warning-badge:nth-child(2) {
bottom: 25px;
}
.tag-warning-badge:nth-child(3) {
bottom: 45px;
}
/* Gallery page tag highlighting */
.tag-container .tag.blacklist-tag {
background-color: rgba(244, 67, 54, 0.2) !important;
border: 1px solid #f44336 !important;
color: #f44336 !important;
}
.tag-container .tag.warning-tag {
background-color: rgba(255, 152, 0, 0.2) !important;
border: 1px solid #ff9800 !important;
color: #ff9800 !important;
}
.tag-container .tag.favorite-tag {
background-color: rgba(33, 150, 243, 0.2) !important;
border: 1px solid #2196f3 !important;
color: #2196f3 !important;
}
/* Star button for favoriting tags */
.tag-star-btn {
margin-left: 5px;
cursor: pointer;
color: #666;
transition: color 0.3s ease;
}
.tag-star-btn:hover {
color: #2196f3;
}
.tag-star-btn.favorited {
color: #2196f3;
}
/* Hover effects for badges */
.gallery:hover .tag-warning-badge {
opacity: 0.8;
}
`;
GM.addStyle(css);
}
/**
* Process all galleries on the page
*/
processGalleries() {
const galleries = document.querySelectorAll('.gallery');
galleries.forEach(gallery => {
this.processGallery(gallery);
});
}
/**
* Process a single gallery for tag warnings
*/
processGallery(gallery) {
const tags = this.extractGalleryTags(gallery);
if (!tags.length) return;
const warnings = this.analyzeTagsForWarnings(tags);
this.addWarningBadges(gallery, warnings);
}
/**
* Extract tags from gallery element
*/
extractGalleryTags(gallery) {
const tags = [];
// Method 1: From data-tags attribute
const dataTags = gallery.getAttribute('data-tags');
if (dataTags) {
// This contains tag IDs, we need to map them to tag names
// For now, we'll use other methods
}
// Method 2: From tag elements (if available)
const tagElements = gallery.querySelectorAll('.tag .name');
tagElements.forEach(tagElement => {
const tagName = tagElement.textContent.trim().toLowerCase();
if (tagName) tags.push(tagName);
});
// Method 3: From title analysis (basic)
if (tags.length === 0) {
const caption = gallery.querySelector('.caption');
if (caption) {
const title = caption.textContent.toLowerCase();
// Check for common tag patterns in titles
this.settings.blacklistTags.concat(this.settings.warningTags).forEach(tag => {
if (title.includes(tag.toLowerCase())) {
tags.push(tag);
}
});
}
}
return tags;
}
/**
* Analyze tags for warnings and favorites
*/
analyzeTagsForWarnings(tags) {
const warnings = {
blacklist: [],
warning: [],
favorite: []
};
tags.forEach(tag => {
const normalizedTag = tag.toLowerCase().trim();
if (this.settings.blacklistTags.includes(normalizedTag)) {
warnings.blacklist.push(normalizedTag);
} else if (this.settings.warningTags.includes(normalizedTag)) {
warnings.warning.push(normalizedTag);
}
if (this.settings.favoriteTags.includes(normalizedTag)) {
warnings.favorite.push(normalizedTag);
}
});
return warnings;
}
/**
* Add warning badges to gallery
*/
addWarningBadges(gallery, warnings) {
// Remove existing badges
const existingBadges = gallery.querySelectorAll('.tag-warning-badge');
existingBadges.forEach(badge => badge.remove());
// Add blacklist badge
if (warnings.blacklist.length > 0) {
const badge = this.createWarningBadge('blacklist', this.abbreviateTag(warnings.blacklist[0]));
gallery.appendChild(badge);
}
// Add warning badge
if (warnings.warning.length > 0) {
const badge = this.createWarningBadge('warning', this.abbreviateTag(warnings.warning[0]));
gallery.appendChild(badge);
}
// Add favorite badge
if (warnings.favorite.length > 0) {
const badge = this.createWarningBadge('favorite', '★');
gallery.appendChild(badge);
}
}
/**
* Create a warning badge element
*/
createWarningBadge(type, text) {
const badge = document.createElement('div');
badge.className = `tag-warning-badge ${type}`;
badge.textContent = text;
badge.title = `${type.charAt(0).toUpperCase() + type.slice(1)} tag detected`;
return badge;
}
/**
* Abbreviate tag names for badges
*/
abbreviateTag(tag) {
const abbreviations = {
'scat': 'SCAT',
'guro': 'GURO',
'vore': 'VORE',
'ryona': 'RYONA',
'snuff': 'SNUFF',
'ntr': 'NTR',
'netorare': 'NTR',
'cheating': 'CHEAT',
'ugly bastard': 'UB',
'mind break': 'MB'
};
return abbreviations[tag.toLowerCase()] || tag.substring(0, 4).toUpperCase();
}
/**
* Process individual gallery page for tag highlighting
*/
processGalleryPage() {
// Check if we're on a gallery page
if (!window.location.pathname.match(/\/g\/\d+\//)) return;
const tagContainers = document.querySelectorAll('.tag-container');
tagContainers.forEach(container => {
const tags = container.querySelectorAll('.tag');
tags.forEach(tagElement => {
const tagName = tagElement.querySelector('.name');
if (!tagName) return;
const tag = tagName.textContent.trim().toLowerCase();
if (this.settings.blacklistTags.includes(tag)) {
tagElement.classList.add('blacklist-tag');
} else if (this.settings.warningTags.includes(tag)) {
tagElement.classList.add('warning-tag');
} else if (this.settings.favoriteTags.includes(tag)) {
tagElement.classList.add('favorite-tag');
}
// Add star button for favoriting
this.addStarButton(tagElement, tag);
});
});
}
/**
* Add star button for favoriting tags
*/
addStarButton(tagElement, tag) {
// Skip if star button already exists
if (tagElement.querySelector('.tag-star-btn')) return;
const starBtn = document.createElement('span');
starBtn.className = 'tag-star-btn';
starBtn.innerHTML = this.settings.favoriteTags.includes(tag) ? '★' : '☆';
starBtn.title = 'Add to favorites';
if (this.settings.favoriteTags.includes(tag)) {
starBtn.classList.add('favorited');
}
starBtn.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
await this.toggleFavoriteTag(tag);
// Update star appearance
if (this.settings.favoriteTags.includes(tag)) {
starBtn.innerHTML = '★';
starBtn.classList.add('favorited');
tagElement.classList.add('favorite-tag');
} else {
starBtn.innerHTML = '☆';
starBtn.classList.remove('favorited');
tagElement.classList.remove('favorite-tag');
}
// Update settings form if open
this.updateFavoriteTagsDisplay();
});
tagElement.appendChild(starBtn);
}
/**
* Toggle favorite status of a tag
*/
async toggleFavoriteTag(tag) {
const normalizedTag = tag.toLowerCase().trim();
const index = this.settings.favoriteTags.indexOf(normalizedTag);
if (index > -1) {
this.settings.favoriteTags.splice(index, 1);
} else {
this.settings.favoriteTags.push(normalizedTag);
}
await GM.setValue('favoriteTagsList', this.settings.favoriteTags);
}
/**
* Update favorite tags display in settings
*/
updateFavoriteTagsDisplay() {
const favoriteTagsTextarea = document.getElementById('favoriteTags');
if (favoriteTagsTextarea) {
favoriteTagsTextarea.value = this.settings.favoriteTags.join(', ');
}
}
/**
* Setup observer for dynamically loaded content
*/
setupObserver() {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
// Check if the added node is a gallery
if (node.classList && node.classList.contains('gallery')) {
this.processGallery(node);
}
// Check for galleries within the added node
const galleries = node.querySelectorAll && node.querySelectorAll('.gallery');
if (galleries) {
galleries.forEach(gallery => {
this.processGallery(gallery);
});
}
}
});
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
}
// Initialize Tag Warning System
let tagWarningSystem;
async function initTagWarningSystem() {
const enabled = await GM.getValue('tagWarningEnabled', true);
if (enabled) {
tagWarningSystem = new TagWarningSystem();
}
}
// Initialize on page load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initTagWarningSystem);
} else {
initTagWarningSystem();
}
//------------------------ **Tag Warning & Blacklist System** ------------------
//------------------------ **Advanced Search URL Generation System** ------------------
/**
* Advanced Search URL Generation System
* Replaces existing English-only buttons with context-aware advanced search URLs
*/
class AdvancedSearchSystem {
constructor() {
this.init();
}
/**
* Initialize the advanced search system
*/
init() {
this.replaceEnglishOnlyButtons();
this.addAdvancedSearchButtons();
this.setupObserver();
}
/**
* Replace existing English-only buttons with advanced search URLs
*/
replaceEnglishOnlyButtons() {
// Find existing English filter buttons
const englishButtons = document.querySelectorAll('a[href*="language%3A%22english%22"], a[href*="language:english"]');
englishButtons.forEach(button => {
this.enhanceEnglishButton(button);
});
}
/**
* Enhance an existing English button with advanced search functionality
*/
enhanceEnglishButton(button) {
const currentUrl = button.getAttribute('href');
const enhancedUrl = this.generateAdvancedSearchUrl(currentUrl);
if (enhancedUrl !== currentUrl) {
button.setAttribute('href', enhancedUrl);
button.title = 'Advanced English search with current context';
}
}
/**
* Add advanced search buttons to relevant pages
*/
addAdvancedSearchButtons() {
// Add to tag pages
this.addTagPageSearchButtons();
// Add to artist pages
this.addArtistPageSearchButtons();
// Add to character pages
this.addCharacterPageSearchButtons();
// Add to parody pages
this.addParodyPageSearchButtons();
// Add to group pages
this.addGroupPageSearchButtons();
}
/**
* Add advanced search buttons to tag pages
*/
addTagPageSearchButtons() {
if (!window.location.pathname.startsWith('/tag/')) return;
const pathParts = window.location.pathname.split('/');
if (pathParts.length < 3) return;
const tagName = decodeURIComponent(pathParts[2]);
const searchUrl = this.buildTagSearchUrl('tag', tagName);
this.addSearchButton('English + This Tag', searchUrl, 'Search for English galleries with this tag');
}
/**
* Add advanced search buttons to artist pages
*/
addArtistPageSearchButtons() {
if (!window.location.pathname.startsWith('/artist/')) return;
const pathParts = window.location.pathname.split('/');
if (pathParts.length < 3) return;
const artistName = decodeURIComponent(pathParts[2]);
const searchUrl = this.buildTagSearchUrl('artist', artistName);
this.addSearchButton('English + This Artist', searchUrl, 'Search for English galleries by this artist');
}
/**
* Add advanced search buttons to character pages
*/
addCharacterPageSearchButtons() {
if (!window.location.pathname.startsWith('/character/')) return;
const pathParts = window.location.pathname.split('/');
if (pathParts.length < 3) return;
const characterName = decodeURIComponent(pathParts[2]);
const searchUrl = this.buildTagSearchUrl('character', characterName);
this.addSearchButton('English + This Character', searchUrl, 'Search for English galleries with this character');
}
/**
* Add advanced search buttons to parody pages
*/
addParodyPageSearchButtons() {
if (!window.location.pathname.startsWith('/parody/')) return;
const pathParts = window.location.pathname.split('/');
if (pathParts.length < 3) return;
const parodyName = decodeURIComponent(pathParts[2]);
const searchUrl = this.buildTagSearchUrl('parody', parodyName);
this.addSearchButton('English + This Parody', searchUrl, 'Search for English galleries of this parody');
}
/**
* Add advanced search buttons to group pages
*/
addGroupPageSearchButtons() {
if (!window.location.pathname.startsWith('/group/')) return;
const pathParts = window.location.pathname.split('/');
if (pathParts.length < 3) return;
const groupName = decodeURIComponent(pathParts[2]);
const searchUrl = this.buildTagSearchUrl('group', groupName);
this.addSearchButton('English + This Group', searchUrl, 'Search for English galleries by this group');
}
/**
* Build advanced search URL for tag-based searches
*/
buildTagSearchUrl(namespace, value) {
// Clean the value
const cleanValue = value.replace(/['"]/g, '').trim();
// Build the search query using nhentai's advanced search syntax
const query = `${namespace}:"${cleanValue}" language:"english"`;
// Encode the query
const encodedQuery = encodeURIComponent(query);
return `https://nhentai.net/search/?q=${encodedQuery}`;
}
/**
* Generate advanced search URL from existing URL
*/
generateAdvancedSearchUrl(currentUrl) {
try {
const url = new URL(currentUrl, window.location.origin);
const searchParams = new URLSearchParams(url.search);
let query = searchParams.get('q') || '';
// If it's already an advanced search, return as is
if (query.includes('language:"english"') || query.includes('language%3A%22english%22')) {
return currentUrl;
}
// Add English language filter if not present
if (query) {
query += ' language:"english"';
} else {
query = 'language:"english"';
}
searchParams.set('q', query);
url.search = searchParams.toString();
return url.toString();
} catch (error) {
console.error('Error generating advanced search URL:', error);
return currentUrl;
}
}
/**
* Add a search button to the page
*/
addSearchButton(text, url, title) {
// Find a suitable container for the button
const container = this.findButtonContainer();
if (!container) return;
const button = document.createElement('a');
button.className = 'btn btn-primary advanced-search-btn';
button.href = url;
button.textContent = text;
button.title = title;
button.style.marginLeft = '10px';
// Add icon
const icon = document.createElement('i');
icon.className = 'fa fa-search';
icon.style.marginRight = '5px';
button.insertBefore(icon, button.firstChild);
container.appendChild(button);
}
/**
* Find suitable container for search buttons
*/
findButtonContainer() {
// Try to find existing button containers
const containers = [
document.querySelector('.pagination'),
document.querySelector('.sort'),
document.querySelector('h1'),
document.querySelector('.container h2'),
document.querySelector('#content')
];
for (const container of containers) {
if (container) {
return container;
}
}
return null;
}
/**
* Create quick search buttons for favorite tags
*/
createFavoriteTagSearchButtons() {
// Get favorite tags from storage
GM.getValue('favoriteTagsList', []).then(favoriteTags => {
if (favoriteTags.length === 0) return;
const container = this.createFavoriteSearchContainer();
if (!container) return;
favoriteTags.forEach(tag => {
const searchUrl = this.buildTagSearchUrl('tag', tag);
const button = this.createQuickSearchButton(tag, searchUrl);
container.appendChild(button);
});
});
}
/**
* Create container for favorite tag search buttons
*/
createFavoriteSearchContainer() {
// Check if container already exists
let container = document.getElementById('favorite-tag-searches');
if (container) return container;
container = document.createElement('div');
container.id = 'favorite-tag-searches';
container.style.cssText = `
margin: 15px 0;
padding: 10px;
background: rgba(0,0,0,0.1);
border-radius: 5px;
`;
const title = document.createElement('h4');
title.textContent = 'Quick Search: Favorite Tags';
title.style.marginBottom = '10px';
container.appendChild(title);
// Find insertion point
const insertionPoint = document.querySelector('.container') || document.body;
insertionPoint.insertBefore(container, insertionPoint.firstChild);
return container;
}
/**
* Create quick search button for a tag
*/
createQuickSearchButton(tag, url) {
const button = document.createElement('a');
button.className = 'btn btn-secondary';
button.href = url;
button.textContent = tag;
button.title = `Search English galleries with tag: ${tag}`;
button.style.cssText = `
margin: 2px;
padding: 5px 10px;
font-size: 12px;
text-decoration: none;
`;
return button;
}
/**
* Setup observer for dynamically loaded content
*/
setupObserver() {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
// Check for new English buttons
const englishButtons = node.querySelectorAll &&
node.querySelectorAll('a[href*="language%3A%22english%22"], a[href*="language:english"]');
if (englishButtons) {
englishButtons.forEach(button => {
this.enhanceEnglishButton(button);
});
}
}
});
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
/**
* Generate search URL with multiple parameters
*/
generateMultiParameterSearch(params) {
const queryParts = [];
Object.entries(params).forEach(([key, value]) => {
if (value) {
queryParts.push(`${key}:"${value}"`);
}
});
const query = queryParts.join(' ');
const encodedQuery = encodeURIComponent(query);
return `https://nhentai.net/search/?q=${encodedQuery}`;
}
}
// Initialize Advanced Search System
let advancedSearchSystem;
async function initAdvancedSearchSystem() {
advancedSearchSystem = new AdvancedSearchSystem();
}
// Initialize on page load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initAdvancedSearchSystem);
} else {
initAdvancedSearchSystem();
}
//------------------------ **Advanced Search URL Generation System** ------------------
//------------------------ **Read Manga Page System** ------------------
/**
* Read Manga Page System
* Creates a dedicated page to view all read manga
*/
class ReadMangaPageSystem {
constructor() {
this.pageUrl = '/read-manga/';
this.init();
}
/**
* Initialize the read manga page system
*/
async init() {
this.handlePageRouting();
}
/**
* Add navigation link to the main menu using the existing pattern
*/
async addNavigationLink() {
// Check if read manga page is enabled
const enabled = await this.checkIfEnabled();
if (!enabled) return;
// Check if link already exists
if (document.querySelector('a[href="/read-manga/"]')) return;
// Create the read manga button following the same pattern as other nav items
const readMangaButtonHtml = `
<li>
<a href="/read-manga/">
<i class="fas fa-book-open"></i> Read Manga
</a>
</li>
`;
const readMangaButton = $(readMangaButtonHtml);
// Append to dropdown menu
const dropdownMenu = $('ul.dropdown-menu');
dropdownMenu.append(readMangaButton);
// Append to main menu
const menu = $('ul.menu.left');
menu.append(readMangaButton);
// Add click handler for navigation
readMangaButton.find('a').on('click', (e) => {
e.preventDefault();
this.navigateToReadMangaPage();
});
// Call updateMenuOrder to ensure proper tab order
setTimeout(() => {
if (typeof updateMenuOrder === 'function') {
updateMenuOrder();
}
}, 100);
}
/**
* Check if read manga page should be enabled
*/
async checkIfEnabled() {
// Check if mark as read system is enabled
const markAsReadEnabled = await GM.getValue('markAsReadEnabled', true);
const readMangaPageEnabled = await GM.getValue('readMangaPageEnabled', true);
return markAsReadEnabled && readMangaPageEnabled;
}
/**
* Handle page routing for the read manga page
*/
handlePageRouting() {
// Check if we're on the read manga page
if (window.location.pathname === this.pageUrl ||
window.location.hash === '#read-manga') {
this.renderReadMangaPage();
}
}
/**
* Navigate to the read manga page
*/
navigateToReadMangaPage() {
// Update URL without page reload
history.pushState({}, 'Read Manga - nhentai', this.pageUrl);
this.renderReadMangaPage();
}
/**
* Render the read manga page
*/
async renderReadMangaPage() {
// Get read galleries from storage
const readGalleries = await GM.getValue('readGalleries', []);
if (readGalleries.length === 0) {
this.renderEmptyState();
return;
}
// Fetch gallery data for read galleries
const galleryData = await this.fetchGalleryData(readGalleries);
// Apply sorting based on current selection
const sortedData = await this.sortGalleryData(galleryData);
await this.renderGalleryGrid(sortedData);
}
/**
* Render empty state when no read manga
*/
renderEmptyState() {
const content = `
<div class="container">
<h1>Read Manga</h1>
<div class="empty-state" style="text-align: center; padding: 60px 20px;">
<i class="fas fa-book" style="font-size: 64px; color: #666; margin-bottom: 20px;"></i>
<h2 style="color: #666; margin-bottom: 10px;">No Read Manga Yet</h2>
<p style="color: #999; margin-bottom: 30px;">
Start reading manga and they'll appear here automatically!
</p>
<a href="/" class="btn btn-primary">
<i class="fas fa-home"></i> Browse Manga
</a>
</div>
</div>
`;
this.replacePageContent(content);
}
/**
* Fetch gallery data for read galleries
*/
async fetchGalleryData(galleryIds) {
const galleryData = [];
// Try to get cached data from localStorage first
const cachedData = await GM.getValue('readGalleriesCache', {});
// Get the user-configured limit (default to 100 if not set)
const maxReadMangaDisplay = await GM.getValue('maxReadMangaDisplay', 100);
// Reverse the array to get the most recent reads first, then slice
const recentGalleryIds = galleryIds.slice().reverse();
for (const galleryId of recentGalleryIds.slice(0, maxReadMangaDisplay)) { // Use configurable limit
let galleryInfo = cachedData[galleryId];
if (!galleryInfo) {
// Create basic data structure with gallery ID
galleryInfo = {
id: galleryId,
title: `Gallery ${galleryId}`,
thumbnail: null,
pages: 'Unknown',
tags: [],
url: `/g/${galleryId}/`,
cached: false
};
// Try multiple sources for gallery data
await this.tryFetchGalleryInfo(galleryInfo);
}
galleryData.push(galleryInfo);
}
// Cache any newly found data
const updatedCache = { ...cachedData };
galleryData.forEach(gallery => {
if (gallery.cached && !cachedData[gallery.id]) {
updatedCache[gallery.id] = gallery;
}
});
await GM.setValue('readGalleriesCache', updatedCache);
return galleryData;
}
/**
* Sort gallery data based on selected sort order
*/
async sortGalleryData(galleryData) {
const sortOrder = await GM.getValue('readMangaSortOrder', 'recent');
const readGalleries = await GM.getValue('readGalleries', []);
// Create a map of gallery ID to its position in the readGalleries array (for recent/oldest sorting)
const readOrderMap = {};
readGalleries.forEach((id, index) => {
readOrderMap[id] = index;
});
switch (sortOrder) {
case 'recent':
// Most recently read first (reverse order of readGalleries array)
return galleryData.sort((a, b) => {
const aIndex = readOrderMap[a.id] ?? 999999;
const bIndex = readOrderMap[b.id] ?? 999999;
return bIndex - aIndex; // Reverse order for most recent first
});
case 'oldest':
// Oldest read first (original order of readGalleries array)
return galleryData.sort((a, b) => {
const aIndex = readOrderMap[a.id] ?? 999999;
const bIndex = readOrderMap[b.id] ?? 999999;
return aIndex - bIndex; // Original order for oldest first
});
case 'id-asc':
// ID ascending (lowest ID first)
return galleryData.sort((a, b) => {
const aId = parseInt(a.id) || 0;
const bId = parseInt(b.id) || 0;
return aId - bId;
});
case 'id-desc':
// ID descending (highest ID first)
return galleryData.sort((a, b) => {
const aId = parseInt(a.id) || 0;
const bId = parseInt(b.id) || 0;
return bId - aId;
});
default:
return galleryData;
}
}
/**
* Try to fetch gallery information from multiple sources
*/
async tryFetchGalleryInfo(galleryInfo) {
const galleryId = galleryInfo.id;
// Method 1: Check if we're currently on this gallery page
const galleryMatch = window.location.pathname.match(/\/g\/(\d+)\//);
if (galleryMatch && galleryMatch[1] === galleryId) {
// Get title from h1 or h2
const titleElement = document.querySelector('h1, h2');
if (titleElement) {
galleryInfo.title = titleElement.textContent.trim();
}
// Get cover image from #cover
const coverElement = document.querySelector('#cover img');
if (coverElement && coverElement.src) {
galleryInfo.thumbnail = coverElement.src;
}
// Get page count
const pageNumberButton = document.querySelector('.page-number.btn.btn-unstyled .num-pages');
if (pageNumberButton) {
galleryInfo.pages = pageNumberButton.textContent.trim();
}
galleryInfo.cached = true;
return;
}
// Method 2: Check current page for gallery listings
const existingGallery = document.querySelector(`[data-gallery-id="${galleryId}"], .gallery a[href*="/g/${galleryId}/"]`);
if (existingGallery) {
const galleryElement = existingGallery.closest('.gallery') || existingGallery;
const titleElement = galleryElement.querySelector('.caption');
const imgElement = galleryElement.querySelector('img');
if (titleElement) {
galleryInfo.title = titleElement.textContent.trim();
}
if (imgElement && imgElement.src) {
galleryInfo.thumbnail = imgElement.src;
}
galleryInfo.cached = true;
return;
}
// Method 3: Try to construct thumbnail URL from gallery ID
// nhentai thumbnail pattern: https://t.nhentai.net/galleries/{id}/thumb.jpg
const thumbnailUrl = `https://t.nhentai.net/galleries/${galleryId}/thumb.jpg`;
// Test if the thumbnail exists
try {
const response = await fetch(thumbnailUrl, { method: 'HEAD' });
if (response.ok) {
galleryInfo.thumbnail = thumbnailUrl;
galleryInfo.cached = true;
}
} catch (error) {
// Thumbnail doesn't exist or network error, leave as null
console.log(`Thumbnail not found for gallery ${galleryId}`);
}
}
/**
* Render gallery grid for read manga
*/
async renderGalleryGrid(galleryData) {
const totalCount = galleryData.length;
const content = `
<div class="container" style="min-height: 100vh; padding-bottom: 50px;">
<h1>Read Manga <span class="nobold">(${totalCount})</span></h1>
<div class="read-manga-controls" style="margin: 20px 0; padding: 15px; background: rgba(0,0,0,0.1); border-radius: 5px;">
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px;">
<div>
<button id="clear-all-read" class="btn btn-danger" style="margin-right: 10px;">
<i class="fas fa-trash"></i> Clear All Read
</button>
<button id="export-read-list" class="btn btn-secondary">
<i class="fas fa-download"></i> Export List
</button>
</div>
<div>
<select id="read-sort" class="form-control" style="width: auto; display: inline-block;">
<option value="recent">Recently Read</option>
<option value="oldest">Oldest First</option>
<option value="id-asc">ID Ascending</option>
<option value="id-desc">ID Descending</option>
</select>
</div>
</div>
</div>
<div class="gallery-grid read-manga-gallery-grid">
${galleryData.map(gallery => this.createGalleryCard(gallery)).join('')}
</div>
<div class="pagination-info" style="text-align: center; margin: 30px 0; color: #666;">
Showing ${totalCount} read manga
</div>
</div>
`;
this.replacePageContent(content);
await this.addReadPageEventListeners();
}
/**
* Create gallery card HTML
*/
createGalleryCard(gallery) {
// Handle thumbnail display
const thumbnailHtml = gallery.thumbnail
? `<img src="${gallery.thumbnail}" alt="${gallery.title}" loading="lazy"
style="width: 100%; height: 300px; object-fit: cover; display: block;"
onload="const placeholder = this.nextElementSibling; placeholder.style.display='none'; Array.from(placeholder.children).forEach(child => child.style.display='none');"
onerror="this.style.display='none'; const placeholder = this.nextElementSibling; placeholder.style.display='flex'; Array.from(placeholder.children).forEach(child => child.style.display='block');">`
: '';
const noImageHtml = `
<div class="no-image-placeholder" style="display: ${gallery.thumbnail ? 'none !important' : 'flex'};
width: 100%; height: 300px; background: linear-gradient(135deg, #333 0%, #555 100%);
align-items: center; justify-content: center; flex-direction: column; color: #999;">
<i class="fas fa-book" style="font-size: 48px; margin-bottom: 10px; opacity: 0.5; display: ${gallery.thumbnail ? 'none !important' : 'block'};"></i>
<span style="font-size: 14px; font-weight: 500; display: ${gallery.thumbnail ? 'none !important' : 'block'};">Gallery ${gallery.id}</span>
<span style="font-size: 12px; opacity: 0.7; display: ${gallery.thumbnail ? 'none !important' : 'block'};">No Preview Available</span>
</div>
`;
return `
<div class="gallery read-gallery read-manga-gallery" data-gallery-id="${gallery.id}"
style="position: relative; border-radius: 3px; overflow: visible; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);
transition: all 0.3s ease; background: #252525; height: auto; min-height: 350px; padding-bottom: 50px;">
<a href="${gallery.url}" class="cover" style="position: relative; display: block; height: 300px; overflow: hidden; border-radius: 3px 3px 0 0;">
${thumbnailHtml}
${noImageHtml}
<!-- Remove from read button (no read badge on read manga page) -->
<button class="remove-read-btn" data-gallery-id="${gallery.id}" title="Remove from read list"
style="position: absolute; top: 5px; right: 5px; width: 28px; height: 28px;
background: rgba(244, 67, 54, 0.9); border: none; border-radius: 50%; color: white;
font-size: 14px; cursor: pointer; display: flex; align-items: center;
justify-content: center; transition: all 0.3s ease; z-index: 100;">
<i class="fas fa-times"></i>
</button>
</a>
<!-- Caption positioned below the image, not overlapping -->
<div class="caption read-manga-caption" style="padding: 8px 10px; background: #1e1e1e; color: white;
font-size: 12px; line-height: 1.3; min-height: 40px; max-height: 50px; overflow: hidden; text-overflow: ellipsis;
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
position: absolute; bottom: 0; left: 0; right: 0; border-radius: 0 0 3px 3px;">${gallery.title}</div>
</div>
`;
}
/**
* Add event listeners for read page controls
*/
async addReadPageEventListeners() {
// Clear all read button
const clearAllBtn = document.getElementById('clear-all-read');
if (clearAllBtn) {
clearAllBtn.addEventListener('click', async () => {
if (confirm('Are you sure you want to clear all read manga? This action cannot be undone.')) {
await GM.setValue('readGalleries', []);
this.renderEmptyState();
// Show notification
this.showNotification('All read manga cleared!', 'success');
}
});
}
// Export read list button
const exportBtn = document.getElementById('export-read-list');
if (exportBtn) {
exportBtn.addEventListener('click', async () => {
const readGalleries = await GM.getValue('readGalleries', []);
const exportData = {
exported: new Date().toISOString(),
version: '9.0.0',
readGalleries: readGalleries
};
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `nhentai-read-list-${new Date().toISOString().split('T')[0]}.json`;
a.click();
URL.revokeObjectURL(url);
this.showNotification('Read list exported!', 'success');
});
}
// Remove individual read items
const removeButtons = document.querySelectorAll('.remove-read-btn');
removeButtons.forEach(btn => {
btn.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
const galleryId = btn.getAttribute('data-gallery-id');
if (confirm('Remove this manga from your read list?')) {
await this.removeFromReadList(galleryId);
// Remove the gallery card from the page
const galleryCard = btn.closest('.gallery');
if (galleryCard) {
galleryCard.style.opacity = '0';
galleryCard.style.transform = 'scale(0.8)';
setTimeout(() => galleryCard.remove(), 300);
}
this.showNotification('Removed from read list!', 'success');
}
});
});
// Sort functionality
const sortSelect = document.getElementById('read-sort');
if (sortSelect) {
// Set the current sort value from storage
const currentSort = await GM.getValue('readMangaSortOrder', 'recent');
sortSelect.value = currentSort;
sortSelect.addEventListener('change', async () => {
// Save the selected sort order
await GM.setValue('readMangaSortOrder', sortSelect.value);
// Re-render the page with new sorting
this.renderReadMangaPage();
});
}
}
/**
* Remove gallery from read list
*/
async removeFromReadList(galleryId) {
const readGalleries = await GM.getValue('readGalleries', []);
const updatedList = readGalleries.filter(id => id !== galleryId);
await GM.setValue('readGalleries', updatedList);
}
/**
* Replace page content
*/
replacePageContent(content) {
// Find the main content area
const mainContent = document.querySelector('#content') || document.body;
// Wrap content in read-manga-page class for styling
const wrappedContent = `<div class="read-manga-page">${content}</div>`;
mainContent.innerHTML = wrappedContent;
// Update page title
document.title = 'Read Manga - nhentai';
// Add body class for additional styling
document.body.classList.add('read-manga-active');
// Add specific CSS for read manga page
this.addReadMangaPageCSS();
}
/**
* Add CSS styling specific to read manga page
*/
addReadMangaPageCSS() {
const css = `
/* Read Manga Page Container */
.read-manga-page {
min-height: 100vh;
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
color: #fff;
position: relative;
}
.read-manga-page::before {
content: '';
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.1) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.1) 0%, transparent 50%),
radial-gradient(circle at 40% 40%, rgba(120, 219, 226, 0.1) 0%, transparent 50%);
pointer-events: none;
z-index: 0;
}
.read-manga-page .container {
position: relative;
z-index: 1;
}
/* Enhanced Header Styling */
.read-manga-page h1 {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
font-size: 2.5rem;
font-weight: 700;
text-align: center;
margin: 30px 0;
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
/* Gallery Grid Layout for Read Manga - Default Desktop */
.read-manga-page .gallery-grid,
.read-manga-page .read-manga-gallery-grid {
display: grid !important;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)) !important;
gap: 30px !important;
padding: 30px 20px !important;
margin: 0 auto !important;
max-width: 1400px !important;
min-height: 400px !important;
}
/* Individual Read Manga Gallery Item */
.read-manga-gallery {
position: relative !important;
border-radius: 12px !important;
overflow: hidden !important;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3) !important;
transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) !important;
background: linear-gradient(145deg, #2a2a2a 0%, #1e1e1e 100%) !important;
height: auto !important;
min-height: 380px !important;
border: 1px solid rgba(255, 255, 255, 0.1) !important;
display: flex !important;
flex-direction: column !important;
}
.read-manga-gallery:hover {
transform: translateY(-8px) scale(1.02) !important;
box-shadow: 0 20px 40px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.1) !important;
border-color: rgba(102, 126, 234, 0.3) !important;
}
.read-manga-gallery::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.05) 0%, rgba(118, 75, 162, 0.05) 100%);
opacity: 0;
transition: opacity 0.3s ease;
z-index: 1;
pointer-events: none;
}
.read-manga-gallery:hover::before {
opacity: 1;
}
/* Gallery Cover Image for Read Manga */
.read-manga-gallery .cover {
position: relative !important;
display: block !important;
height: 320px !important;
overflow: hidden !important;
border-radius: 12px 12px 0 0 !important;
z-index: 2 !important;
flex-shrink: 0 !important;
}
.read-manga-gallery .cover::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(180deg, transparent 0%, transparent 60%, rgba(0,0,0,0.8) 100%);
z-index: 3;
pointer-events: none;
}
.read-manga-gallery .cover img {
width: 100% !important;
height: 100% !important;
object-fit: cover !important;
transition: transform 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) !important;
filter: brightness(0.9) contrast(1.1) !important;
}
.read-manga-gallery:hover .cover img {
transform: scale(1.08) !important;
filter: brightness(1) contrast(1.2) !important;
}
/* Caption positioning below image, not overlapping */
.read-manga-caption {
padding: 12px 15px !important;
background: linear-gradient(135deg, #2a2a2a 0%, #1e1e1e 100%) !important;
color: #f0f0f0 !important;
font-size: 13px !important;
line-height: 1.4 !important;
min-height: 50px !important;
max-height: 60px !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
display: -webkit-box !important;
-webkit-line-clamp: 2 !important;
-webkit-box-orient: vertical !important;
border-radius: 0 0 12px 12px !important;
z-index: 5 !important;
font-weight: 500 !important;
border-top: 1px solid rgba(255, 255, 255, 0.1) !important;
backdrop-filter: blur(10px) !important;
flex-shrink: 0 !important;
margin-top: auto !important;
}
.read-manga-gallery:hover .read-manga-caption {
background: linear-gradient(135deg, #333 0%, #222 100%) !important;
color: #fff !important;
}
/* Remove from Read List Button */
.read-manga-gallery .remove-read-btn {
position: absolute !important;
top: 12px;
width: 32px !important;
height: 32px !important;
background: linear-gradient(135deg, rgba(244, 67, 54, 0.9) 0%, rgba(211, 47, 47, 0.9) 100%) !important;
border: none !important;
border-radius: 50% !important;
color: white !important;
font-size: 14px !important;
cursor: pointer !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1) !important;
z-index: 11 !important;
box-shadow: 0 4px 12px rgba(244, 67, 54, 0.3) !important;
backdrop-filter: blur(10px) !important;
border: 1px solid rgba(255, 255, 255, 0.2) !important;
opacity: 0.8 !important;
}
.read-manga-gallery .remove-read-btn:hover {
background: linear-gradient(135deg, rgba(244, 67, 54, 1) 0%, rgba(211, 47, 47, 1) 100%) !important;
transform: scale(1.15) !important;
box-shadow: 0 6px 20px rgba(244, 67, 54, 0.4) !important;
opacity: 1 !important;
}
.read-manga-gallery .remove-read-btn:active {
transform: scale(0.95) !important;
}
/* Read Badge */
.read-manga-gallery .read-badge {
position: absolute !important;
top: 5px !important;
left: 5px !important;
background: rgba(76, 175, 80, 0.9) !important;
color: white !important;
padding: 4px 8px !important;
border-radius: 3px !important;
font-size: 11px !important;
font-weight: bold !important;
z-index: 10 !important;
}
/* No Image Placeholder */
.read-manga-gallery .no-image-placeholder {
width: 100% !important;
height: 300px !important;
background: linear-gradient(135deg, #333 0%, #555 100%) !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
flex-direction: column !important;
color: #999 !important;
transition: all 0.3s ease !important;
}
.read-manga-gallery:hover .no-image-placeholder {
background: linear-gradient(135deg, #444 0%, #666 100%) !important;
}
/* Responsive Design for Read Manga */
@media (max-width: 768px) {
.read-manga-page .gallery-grid,
.read-manga-page .read-manga-gallery-grid {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)) !important;
gap: 12px !important;
padding: 15px 10px !important;
max-width: 100% !important;
}
.read-manga-gallery {
min-height: 240px !important;
max-width: 200px !important;
margin: 0 auto !important;
display: flex !important;
flex-direction: column !important;
}
.read-manga-gallery .cover {
height: 180px !important;
flex-shrink: 0 !important;
}
.read-manga-caption {
font-size: 11px !important;
padding: 8px 10px !important;
min-height: 40px !important;
max-height: 50px !important;
-webkit-line-clamp: 2 !important;
line-height: 1.3 !important;
flex-shrink: 0 !important;
margin-top: auto !important;
}
.read-manga-gallery .remove-read-btn {
width: 26px !important;
height: 26px !important;
font-size: 12px !important;
top: 8px;
right: 8px !important;
}
.read-manga-gallery .no-image-placeholder {
height: 180px !important;
}
.read-manga-gallery .no-image-placeholder i {
font-size: 36px !important;
margin-bottom: 8px !important;
}
.read-manga-gallery .no-image-placeholder span {
font-size: 11px !important;
}
/* Mobile Controls */
.read-manga-controls {
padding: 15px !important;
margin: 20px 10px !important;
border-radius: 12px !important;
}
.read-manga-controls > div {
flex-direction: column !important;
gap: 15px !important;
}
.read-manga-controls .btn {
min-width: 100% !important;
padding: 10px 16px !important;
font-size: 13px !important;
}
.read-manga-controls .form-control {
min-width: 100% !important;
padding: 10px 14px !important;
font-size: 13px !important;
}
}
/* Extra small mobile screens */
@media (max-width: 480px) {
.read-manga-page .gallery-grid,
.read-manga-page .read-manga-gallery-grid {
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)) !important;
gap: 10px !important;
padding: 12px 8px !important;
}
.read-manga-gallery {
min-height: 210px !important;
max-width: 170px !important;
display: flex !important;
flex-direction: column !important;
}
.read-manga-gallery .cover {
height: 160px !important;
flex-shrink: 0 !important;
}
.read-manga-caption {
font-size: 10px !important;
padding: 6px 8px !important;
min-height: 50px !important;
max-height: 45px !important;
line-height: 1.2 !important;
flex-shrink: 0 !important;
margin-top: auto !important;
}
.read-manga-gallery .remove-read-btn {
width: 24px !important;
height: 24px !important;
font-size: 11px !important;
top: 6px;
right: 6px !important;
}
.read-manga-gallery .no-image-placeholder {
height: 160px !important;
}
.read-manga-gallery .no-image-placeholder i {
font-size: 32px !important;
}
.read-manga-gallery .no-image-placeholder span {
font-size: 10px !important;
}
/* Extra small mobile controls */
.read-manga-controls {
padding: 12px !important;
margin: 15px 8px !important;
border-radius: 10px !important;
}
.read-manga-controls .btn {
padding: 8px 12px !important;
font-size: 12px !important;
}
.read-manga-controls .form-control {
padding: 8px 12px !important;
font-size: 12px !important;
}
}
@media (min-width: 1400px) {
.read-manga-page .gallery-grid,
.read-manga-page .read-manga-gallery-grid {
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)) !important;
gap: 30px !important;
}
.read-manga-gallery {
min-height: 400px !important;
}
.read-manga-gallery .cover {
height: 350px !important;
}
.read-manga-gallery .no-image-placeholder {
height: 350px !important;
}
}
/* Override conflicting gallery grid styles for read manga page */
.read-manga-page .gallery-grid.read-manga-gallery-grid,
.read-manga-page .gallery-grid {
display: grid !important;
flex-wrap: unset !important;
justify-content: unset !important;
align-items: unset !important;
}
.read-manga-page .gallery-grid.read-manga-gallery-grid .gallery,
.read-manga-page .gallery-grid .gallery {
width: unset !important;
margin: unset !important;
flex: unset !important;
}
/* Ensure proper container height */
.read-manga-page .container {
min-height: 100vh !important;
padding-bottom: 50px !important;
}
/* Fix any potential z-index issues */
.read-manga-page {
position: relative;
z-index: 1;
}
/* Hide mark-as-read buttons on read manga page */
.read-manga-page .mark-as-read-btn,
.read-manga-gallery .mark-as-read-btn,
body.read-manga-active .mark-as-read-btn {
display: none !important;
}
/* Enhanced Controls Styling */
.read-manga-controls {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.03) 100%) !important;
border: 1px solid rgba(255, 255, 255, 0.12) !important;
border-radius: 16px !important;
backdrop-filter: blur(20px) !important;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3) !important;
padding: 20px !important;
margin: 30px 0 !important;
}
.read-manga-controls .btn {
background: linear-gradient(135deg, #4a4a4a 0%, #333 100%) !important;
border: 1px solid rgba(255, 255, 255, 0.2) !important;
border-radius: 10px !important;
color: #f0f0f0 !important;
font-weight: 500 !important;
padding: 12px 20px !important;
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1) !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2) !important;
min-width: 140px !important;
line-height: 0px;
}
.read-manga-controls .btn:hover {
background: linear-gradient(135deg, #555 0%, #444 100%) !important;
border-color: rgba(255, 255, 255, 0.3) !important;
transform: translateY(-2px) !important;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3) !important;
color: #fff !important;
}
.read-manga-controls .btn:active {
transform: translateY(0) !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2) !important;
}
.read-manga-controls .btn-danger {
background: linear-gradient(135deg, #e53935 0%, #c62828 100%) !important;
border-color: rgba(255, 255, 255, 0.2) !important;
}
.read-manga-controls .btn-danger:hover {
background: linear-gradient(135deg, #f44336 0%, #d32f2f 100%) !important;
box-shadow: 0 6px 20px rgba(244, 67, 54, 0.4) !important;
}
.read-manga-controls .form-control {
background: linear-gradient(135deg, #4a4a4a 0%, #333 100%) !important;
border: 1px solid rgba(255, 255, 255, 0.2) !important;
border-radius: 10px !important;
color: #f0f0f0 !important;
padding: 12px 16px !important;
font-weight: 500 !important;
min-width: 160px !important;
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1) !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2) !important;
}
.read-manga-controls .form-control:hover,
.read-manga-controls .form-control:focus {
background: linear-gradient(135deg, #555 0%, #444 100%) !important;
border-color: rgba(255, 255, 255, 0.3) !important;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3) !important;
outline: none !important;
}
.read-manga-controls .form-control option {
background: #333 !important;
color: #f0f0f0 !important;
}
/* Enhanced Page Header */
.read-manga-page h1 {
background: linear-gradient(135deg, #fff 0%, #e0e0e0 100%) !important;
-webkit-background-clip: text !important;
-webkit-text-fill-color: transparent !important;
background-clip: text !important;
font-size: 2.5rem !important;
font-weight: 700 !important;
text-align: center !important;
margin: 30px 0 !important;
text-shadow: 0 4px 8px rgba(0, 0, 0, 0.3) !important;
letter-spacing: 1px !important;
}
.read-manga-page h1 .nobold {
font-weight: 400 !important;
opacity: 0.8 !important;
}
/* Enhanced pagination info */
.read-manga-page .pagination-info {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.02) 100%) !important;
border: 1px solid rgba(255, 255, 255, 0.1) !important;
border-radius: 12px !important;
padding: 15px 20px !important;
color: #ccc !important;
font-weight: 500 !important;
backdrop-filter: blur(10px) !important;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2) !important;
}
`;
GM.addStyle(css);
}
/**
* Show notification
*/
showNotification(message, type = 'success') {
const notification = document.createElement('div');
const backgroundColor = type === 'error' ? 'rgba(244, 67, 54, 0.9)' : 'rgba(46, 125, 50, 0.9)';
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: ${backgroundColor};
color: white;
padding: 12px 20px;
border-radius: 8px;
z-index: 10000;
font-size: 14px;
font-weight: 500;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.opacity = '0';
notification.style.transition = 'opacity 0.3s ease';
setTimeout(() => notification.remove(), 300);
}, 3000);
}
}
// Initialize Read Manga Page System
let readMangaPageSystem;
async function initReadMangaPageSystem() {
readMangaPageSystem = new ReadMangaPageSystem();
// Make it available globally for the navigation button
window.readMangaPageSystem = readMangaPageSystem;
}
// Initialize on page load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initReadMangaPageSystem);
} else {
initReadMangaPageSystem();
}
//------------------------ **Read Manga Page System** ------------------
//------------------------ **Enhanced CSS Styling and Visual Enhancements** ------------------
/**
* Enhanced CSS Styling System
* Provides comprehensive styling for all new UI elements
*/
function addEnhancedCSS() {
const css = `
/* Global Enhancements */
.gallery {
position: relative;
transition: all 0.3s ease;
}
/* Commented out as this was causing captions to appear under galleries
.gallery:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
*/
/* Gallery Grid Layout */
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 25px;
padding: 20px 10px;
margin: 0 auto;
max-width: 1200px;
}
@media (max-width: 768px) {
.gallery-grid {
grid-template-columns: repeat(auto-fill, minmax(115px, 1fr));
gap: 15px;
padding: 15px 5px;
}
}
@media (min-width: 1400px) {
.gallery-grid {
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 30px;
}
}
/* Advanced Search Buttons */
.advanced-search-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
color: white;
padding: 8px 16px;
border-radius: 20px;
text-decoration: none;
font-size: 12px;
font-weight: 600;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
margin: 2px;
}
.advanced-search-btn:hover {
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
color: white;
text-decoration: none;
}
.advanced-search-btn i {
margin-right: 5px;
}
/* Favorite Tag Search Container */
#favorite-tag-searches {
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
border: 1px solid rgba(102, 126, 234, 0.2);
border-radius: 8px;
padding: 15px;
margin: 15px 0;
}
#favorite-tag-searches h4 {
color: #667eea;
margin-bottom: 10px;
font-size: 14px;
font-weight: 600;
}
/* Enhanced Tag Warning Badges */
.tag-warning-badge {
position: absolute;
bottom: 5px;
left: 5px;
padding: 3px 8px;
border-radius: 12px;
font-size: 10px;
font-weight: bold;
color: white;
z-index: 10;
pointer-events: none;
text-transform: uppercase;
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
backdrop-filter: blur(4px);
transition: all 0.3s ease;
}
.tag-warning-badge.blacklist {
background: linear-gradient(135deg, #ff5252 0%, #f44336 100%);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.tag-warning-badge.warning {
background: linear-gradient(135deg, #ffb74d 0%, #ff9800 100%);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.tag-warning-badge.favorite {
background: linear-gradient(135deg, #42a5f5 0%, #2196f3 100%);
border: 1px solid rgba(255, 255, 255, 0.2);
}
/* Badge stacking with improved spacing */
.gallery .tag-warning-badge:nth-of-type(1) {
bottom: 5px;
}
.gallery .tag-warning-badge:nth-of-type(2) {
bottom: 28px;
}
.gallery .tag-warning-badge:nth-of-type(3) {
bottom: 51px;
}
/* Enhanced Mark as Read Button */
.mark-as-read-btn {
position: absolute;
top: 5px;
right: 5px;
width: 28px;
height: 28px;
background: linear-gradient(135deg, rgba(0, 0, 0, 0.7) 0%, rgba(0, 0, 0, 0.9) 100%);
border: 2px solid rgba(255, 255, 255, 0.2);
border-radius: 50%;
color: #fff;
cursor: pointer;
font-size: 14px;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
backdrop-filter: blur(4px);
}
.mark-as-read-btn:hover {
background: linear-gradient(135deg, rgba(0, 0, 0, 0.9) 0%, rgba(0, 0, 0, 1) 100%);
transform: scale(1.1);
border-color: rgba(255, 255, 255, 0.4);
}
.mark-as-read-btn.read {
background: linear-gradient(135deg, #4caf50 0%, #2e7d32 100%);
border-color: rgba(255, 255, 255, 0.3);
}
.mark-as-read-btn.read:hover {
background: linear-gradient(135deg, #2e7d32 0%, #1b5e20 100%);
}
/* Enhanced Read Badge */
.read-badge {
position: absolute;
top: 5px;
left: 5px;
background: linear-gradient(135deg, #4caf50 0%, #2e7d32 100%);
color: white;
padding: 4px 8px;
border-radius: 12px;
font-size: 10px;
font-weight: bold;
z-index: 10;
pointer-events: none;
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
backdrop-filter: blur(4px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
/* Language Flags Enhancement */
.language-flag {
border-radius: 2px;
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
border: 1px solid rgba(255, 255, 255, 0.2);
}
/* Gallery Page Tag Enhancements */
.tag-container .tag {
transition: all 0.3s ease;
position: relative;
}
.tag-container .tag.blacklist-tag {
background: linear-gradient(135deg, rgba(244, 67, 54, 0.2) 0%, rgba(244, 67, 54, 0.3) 100%) !important;
border: 1px solid #f44336 !important;
color: #f44336 !important;
box-shadow: 0 2px 4px rgba(244, 67, 54, 0.2);
}
.tag-container .tag.warning-tag {
background: linear-gradient(135deg, rgba(255, 152, 0, 0.2) 0%, rgba(255, 152, 0, 0.3) 100%) !important;
border: 1px solid #ff9800 !important;
color: #ff9800 !important;
box-shadow: 0 2px 4px rgba(255, 152, 0, 0.2);
}
.tag-container .tag.favorite-tag {
background: linear-gradient(135deg, rgba(33, 150, 243, 0.2) 0%, rgba(33, 150, 243, 0.3) 100%) !important;
border: 1px solid #2196f3 !important;
color: #2196f3 !important;
box-shadow: 0 2px 4px rgba(33, 150, 243, 0.2);
}
/* Enhanced Star Button */
.tag-star-btn {
margin-left: 8px;
cursor: pointer;
color: #666;
transition: all 0.3s ease;
font-size: 16px;
display: inline-block;
transform: scale(1);
}
.tag-star-btn:hover {
color: #2196f3;
transform: scale(1.2);
text-shadow: 0 0 8px rgba(33, 150, 243, 0.5);
}
.tag-star-btn.favorited {
color: #2196f3;
text-shadow: 0 0 8px rgba(33, 150, 243, 0.3);
}
/* Opacity and Fade Enhancements */
.gallery.read-gallery .cover img,
.gallery.read-gallery .cover .caption {
transition: opacity 0.4s ease, filter 0.4s ease;
filter: grayscale(0.3);
}
.gallery.read-gallery:hover .cover img,
.gallery.read-gallery:hover .cover .caption {
filter: grayscale(0);
}
.gallery:not([data-tags~='12227']) .cover img,
.gallery:not([data-tags~='12227']) .cover .caption {
transition: opacity 0.4s ease, filter 0.4s ease;
}
/* Settings Panel Enhancements */
.expand-icon {
cursor: pointer;
user-select: none;
transition: all 0.3s ease;
position: relative;
}
.expand-icon:hover {
color: #e63946;
}
.expand-icon::after {
content: '▼';
font-size: 12px;
margin-left: 10px;
transition: transform 0.3s ease;
}
.expand-icon.expanded::after {
transform: rotate(180deg);
}
/* Mobile Responsiveness */
@media (max-width: 768px) {
.mark-as-read-btn {
width: 24px;
height: 24px;
font-size: 12px;
}
.tag-warning-badge {
font-size: 8px;
padding: 2px 6px;
}
.advanced-search-btn {
padding: 6px 12px;
font-size: 11px;
}
#favorite-tag-searches {
padding: 10px;
margin: 10px 0;
}
}
@media (max-width: 480px) {
.tag-warning-badge:nth-of-type(3) {
display: none; /* Hide third badge on very small screens */
}
.mark-as-read-btn {
width: 20px;
height: 20px;
font-size: 10px;
}
}
/* Animation Enhancements */
@keyframes markAsReadAnimation {
0% { transform: scale(1) rotate(0deg); }
50% { transform: scale(1.3) rotate(180deg); }
100% { transform: scale(1) rotate(360deg); }
}
.mark-as-read-animation {
animation: markAsReadAnimation 0.6s ease;
}
@keyframes badgeAppear {
0% {
opacity: 0;
transform: scale(0.5) translateY(10px);
}
100% {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.tag-warning-badge {
animation: badgeAppear 0.4s ease;
}
@keyframes starFavorite {
0% { transform: scale(1); }
50% { transform: scale(1.5) rotate(72deg); }
100% { transform: scale(1) rotate(0deg); }
}
.tag-star-btn.favoriting {
animation: starFavorite 0.5s ease;
}
/* Notification Enhancements */
.auto-mark-notification {
position: fixed;
top: 20px;
right: 20px;
background: linear-gradient(135deg, #4caf50 0%, #2e7d32 100%);
color: white;
padding: 12px 20px;
border-radius: 8px;
z-index: 10000;
font-size: 14px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.2);
animation: slideInRight 0.4s ease;
}
@keyframes slideInRight {
0% {
opacity: 0;
transform: translateX(100px);
}
100% {
opacity: 1;
transform: translateX(0);
}
}
/* Hover Effects for Galleries */
.gallery:hover .tag-warning-badge {
transform: scale(1.05);
}
.gallery:hover .mark-as-read-btn {
border-color: rgba(255, 255, 255, 0.6);
}
.gallery:hover .read-badge {
transform: scale(1.05);
}
/* Navigation link styling */
#read-manga-nav-link {
border-radius: 3px;
margin: 0 5px;
}
/* No image placeholder styling */
.no-image-placeholder {
transition: all 0.3s ease;
}
.gallery:hover .no-image-placeholder {
background: linear-gradient(135deg, #444 0%, #666 100%);
}
.no-image-placeholder i {
transition: all 0.3s ease;
}
.gallery:hover .no-image-placeholder i {
transform: scale(1.1);
opacity: 0.7;
}
/* Dark Mode Compatibility */
@media (prefers-color-scheme: dark) {
.advanced-search-btn {
box-shadow: 0 2px 8px rgba(0,0,0,0.4);
}
#favorite-tag-searches {
background: linear-gradient(135deg, rgba(102, 126, 234, 0.15) 0%, rgba(118, 75, 162, 0.15) 100%);
border-color: rgba(102, 126, 234, 0.3);
}
}
`;
GM.addStyle(css);
}
// Override website's default gallery sizing for gallery-grid
GM.addStyle(`
@media screen and (min-width: 980px) {
.gallery-grid .gallery,
.gallery-grid .gallery-favorite,
.gallery-grid .thumb-container {
width: 19% !important;
margin: 3px !important;
}
}
/* Ensure gallery-grid uses flexbox layout for better control */
.gallery-grid {
display: flex !important;
flex-wrap: wrap !important;
justify-content: flex-start !important;
align-items: flex-start !important;
gap: 0 !important;
padding: 20px 10px !important;
margin: 0 auto !important;
max-width: 1200px !important;
}
@media (max-width: 979px) {
.gallery-grid .gallery,
.gallery-grid .gallery-favorite,
.gallery-grid .thumb-container {
width: 48% !important;
margin: 1% !important;
}
}
@media (max-width: 600px) {
.gallery-grid .gallery,
.gallery-grid .gallery-favorite,
.gallery-grid .thumb-container {
width: 100% !important;
margin: 5px 0 !important;
}
}
`);
// Function to adjust the position of a specific element
setInterval(function() {
const pageNumberDisplay = $('.page-number-display');
if (pageNumberDisplay.length > 0) {
$('.read-manga-gallery .remove-read-btn').css('top', '30px');
}
}, 100);
// Initialize enhanced CSS
addEnhancedCSS();
//------------------------ **Enhanced CSS Styling and Visual Enhancements** ------------------
//------------------------ **Feature Integration and Coordination** ------------------
/**
* Feature Integration System
* Coordinates all new features to work together seamlessly
*/
class FeatureIntegrationSystem {
constructor() {
this.systems = {};
this.initialized = false;
this.init();
}
/**
* Initialize the integration system
*/
async init() {
if (this.initialized) return;
try {
// Initialize all systems in the correct order
await this.initializeSystems();
// Set up cross-system communication
this.setupCommunication();
// Set up unified event handling
this.setupUnifiedEvents();
// Apply integrated styling
this.applyIntegratedStyling();
this.initialized = true;
console.log('Nhentai Plus+ Enhanced Features initialized successfully');
} catch (error) {
console.error('Error initializing enhanced features:', error);
}
}
/**
* Initialize all systems in the correct order
*/
async initializeSystems() {
// Initialize opacity system first (affects visual rendering)
if (await GM.getValue('markAsReadEnabled', true) || await GM.getValue('tagWarningEnabled', true)) {
this.systems.opacity = opacityFadeSystem;
}
// Initialize language detection (needed for other systems)
this.systems.language = languageDetectionSystem;
// Initialize tag warning system (depends on language detection)
if (await GM.getValue('tagWarningEnabled', true)) {
this.systems.tagWarning = tagWarningSystem;
}
// Initialize mark as read system (depends on opacity system)
if (await GM.getValue('markAsReadEnabled', true)) {
this.systems.markAsRead = markAsReadSystem;
}
// Initialize advanced search system (independent)
this.systems.advancedSearch = advancedSearchSystem;
// Initialize read manga page system (independent)
this.systems.readMangaPage = readMangaPageSystem;
}
/**
* Set up communication between systems
*/
setupCommunication() {
// When opacity settings change, update all systems
document.addEventListener('opacitySettingsChanged', (event) => {
if (this.systems.markAsRead) {
this.systems.markAsRead.settings.readOpacity = event.detail.readOpacity;
this.systems.markAsRead.settings.nonEnglishOpacity = event.detail.nonEnglishOpacity;
}
});
// When favorite tags change, update search system
document.addEventListener('favoriteTagsChanged', (event) => {
if (this.systems.advancedSearch) {
this.systems.advancedSearch.createFavoriteTagSearchButtons();
}
});
// When read status changes, update visual systems
document.addEventListener('readStatusChanged', (event) => {
if (this.systems.opacity) {
this.systems.opacity.applyOpacitySettings();
}
});
}
/**
* Set up unified event handling
*/
setupUnifiedEvents() {
// Handle page navigation
let currentUrl = window.location.href;
const observer = new MutationObserver(() => {
if (window.location.href !== currentUrl) {
currentUrl = window.location.href;
this.handlePageChange();
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
// Handle dynamic content loading
this.setupDynamicContentHandler();
}
/**
* Handle page changes
*/
async handlePageChange() {
// Small delay to let the page load
setTimeout(async () => {
// Re-initialize systems that need to process new content
if (this.systems.language) {
this.systems.language.detectAndMarkLanguages();
}
if (this.systems.tagWarning) {
this.systems.tagWarning.processGalleries();
this.systems.tagWarning.processGalleryPage();
}
if (this.systems.markAsRead) {
this.systems.markAsRead.addMarkAsReadButtons();
this.systems.markAsRead.applyReadStatus();
this.systems.markAsRead.setupAutoMark();
}
if (this.systems.advancedSearch) {
this.systems.advancedSearch.addAdvancedSearchButtons();
this.systems.advancedSearch.createFavoriteTagSearchButtons();
}
if (this.systems.opacity) {
this.systems.opacity.applyOpacitySettings();
}
}, 500);
}
/**
* Set up handler for dynamically loaded content
*/
setupDynamicContentHandler() {
const observer = new MutationObserver((mutations) => {
let hasNewGalleries = false;
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.classList && node.classList.contains('gallery')) {
hasNewGalleries = true;
} else if (node.querySelectorAll) {
const galleries = node.querySelectorAll('.gallery');
if (galleries.length > 0) {
hasNewGalleries = true;
}
}
}
});
});
if (hasNewGalleries) {
this.processNewGalleries();
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
/**
* Process newly added galleries
*/
processNewGalleries() {
// Process with all systems
if (this.systems.language) {
this.systems.language.detectAndMarkLanguages();
}
if (this.systems.tagWarning) {
this.systems.tagWarning.processGalleries();
}
if (this.systems.markAsRead) {
this.systems.markAsRead.addMarkAsReadButtons();
this.systems.markAsRead.applyReadStatus();
}
if (this.systems.opacity) {
this.systems.opacity.applyOpacitySettings();
}
}
/**
* Apply integrated styling that coordinates all systems
*/
applyIntegratedStyling() {
const css = `
/* Ensure proper z-index stacking */
.gallery .cover {
position: relative;
}
.gallery .caption {
z-index: 20;
}
.gallery .caption * {
z-index: 21;
}
.gallery .mark-as-read-btn { z-index: 15; }
.gallery .read-badge { z-index: 14; }
.gallery .tag-warning-badge { z-index: 13; }
.gallery .language-flag { z-index: 12; }
/* Ensure badges don't overlap with buttons */
.gallery .tag-warning-badge {
max-width: calc(100% - 40px); /* Leave space for mark-as-read button */
}
/* Smooth transitions for all interactive elements */
.gallery * {
transition: opacity 0.3s ease, transform 0.3s ease, filter 0.3s ease;
}
/* Ensure readability of overlaid elements */
.gallery .mark-as-read-btn,
.gallery .read-badge,
.gallery .tag-warning-badge {
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
}
`;
GM.addStyle(css);
}
/**
* Update settings for all systems
*/
async updateAllSettings() {
const settings = {
markAsReadEnabled: await GM.getValue('markAsReadEnabled', true),
autoMarkReadEnabled: await GM.getValue('autoMarkReadEnabled', true),
nonEnglishOpacity: await GM.getValue('nonEnglishOpacity', 0.2),
readGalleriesOpacity: await GM.getValue('readGalleriesOpacity', 0.6),
tagWarningEnabled: await GM.getValue('tagWarningEnabled', true),
blacklistTagsList: await GM.getValue('blacklistTagsList', []),
warningTagsList: await GM.getValue('warningTagsList', []),
favoriteTagsList: await GM.getValue('favoriteTagsList', [])
};
// Update each system with new settings
Object.values(this.systems).forEach(system => {
if (system && system.loadSettings) {
system.loadSettings();
}
});
// Trigger re-processing
this.handlePageChange();
}
/**
* Get status of all systems
*/
getSystemStatus() {
const status = {};
Object.entries(this.systems).forEach(([name, system]) => {
status[name] = {
initialized: !!system,
enabled: system && system.settings ? system.settings.enabled : false
};
});
return status;
}
}
// Global integration system instance
let featureIntegrationSystem;
/**
* Initialize all enhanced features
*/
async function initializeEnhancedFeatures() {
// Wait for DOM to be ready
if (document.readyState === 'loading') {
await new Promise(resolve => {
document.addEventListener('DOMContentLoaded', resolve);
});
}
// Initialize the integration system
featureIntegrationSystem = new FeatureIntegrationSystem();
}
// Initialize enhanced features
initializeEnhancedFeatures();
// Expose global functions for debugging and external access
window.nhentaiPlusEnhanced = {
systems: {
integration: () => featureIntegrationSystem,
markAsRead: () => markAsReadSystem,
opacity: () => opacityFadeSystem,
language: () => languageDetectionSystem,
tagWarning: () => tagWarningSystem,
advancedSearch: () => advancedSearchSystem
},
utils: {
refreshAllSystems: () => featureIntegrationSystem?.handlePageChange(),
updateSettings: () => featureIntegrationSystem?.updateAllSettings(),
getStatus: () => featureIntegrationSystem?.getSystemStatus()
}
};
//------------------------ **Feature Integration and Coordination** ------------------
//------------------------ **Validation and Testing** ------------------
/**
* Validation and Testing System
* Ensures all features work correctly and provides debugging information
*/
class ValidationSystem {
constructor() {
this.testResults = {};
this.performanceMetrics = {};
}
/**
* Run comprehensive validation tests
*/
async runValidationTests() {
console.log('🧪 Running Nhentai Plus+ Enhanced Features Validation...');
const tests = [
this.testSettingsIntegration,
this.testMarkAsReadSystem,
this.testOpacitySystem,
this.testLanguageDetection,
this.testTagWarningSystem,
this.testAdvancedSearch,
this.testBackwardCompatibility,
this.testPerformance
];
for (const test of tests) {
try {
await test.call(this);
} catch (error) {
console.error(`❌ Test failed: ${test.name}`, error);
this.testResults[test.name] = { status: 'failed', error: error.message };
}
}
this.reportResults();
}
/**
* Test settings integration
*/
async testSettingsIntegration() {
const testName = 'Settings Integration';
console.log(`🔧 Testing ${testName}...`);
// Test that all new settings exist
const requiredSettings = [
'markAsReadEnabled',
'autoMarkReadEnabled',
'nonEnglishOpacity',
'readGalleriesOpacity',
'tagWarningEnabled',
'blacklistTagsList',
'warningTagsList',
'favoriteTagsList'
];
for (const setting of requiredSettings) {
const value = await GM.getValue(setting);
if (value === undefined) {
throw new Error(`Setting ${setting} not found`);
}
}
// Test settings form elements exist
const formElements = [
'#markAsReadEnabled',
'#autoMarkReadEnabled',
'#nonEnglishOpacity',
'#readGalleriesOpacity',
'#tagWarningEnabled',
'#blacklistTags',
'#warningTags',
'#favoriteTags'
];
if (window.location.href.includes('/settings')) {
for (const selector of formElements) {
if (!document.querySelector(selector)) {
console.warn(`⚠️ Settings form element ${selector} not found`);
}
}
}
this.testResults[testName] = { status: 'passed' };
console.log(`✅ ${testName} passed`);
}
/**
* Test Mark as Read system
*/
async testMarkAsReadSystem() {
const testName = 'Mark as Read System';
console.log(`📖 Testing ${testName}...`);
const enabled = await GM.getValue('markAsReadEnabled', true);
if (!enabled) {
this.testResults[testName] = { status: 'skipped', reason: 'Feature disabled' };
return;
}
// Test that the system exists
if (!window.nhentaiPlusEnhanced?.systems?.markAsRead()) {
throw new Error('Mark as Read system not initialized');
}
// Test on gallery pages
const galleries = document.querySelectorAll('.gallery');
if (galleries.length > 0) {
const hasButtons = Array.from(galleries).some(gallery =>
gallery.querySelector('.mark-as-read-btn')
);
if (!hasButtons) {
console.warn('⚠️ No mark-as-read buttons found on galleries');
}
}
this.testResults[testName] = { status: 'passed' };
console.log(`✅ ${testName} passed`);
}
/**
* Test Opacity system
*/
async testOpacitySystem() {
const testName = 'Opacity System';
console.log(`🎨 Testing ${testName}...`);
// Test CSS custom properties are set
const rootStyle = getComputedStyle(document.documentElement);
const nonEnglishOpacity = rootStyle.getPropertyValue('--non-english-opacity');
const readOpacity = rootStyle.getPropertyValue('--read-galleries-opacity');
if (!nonEnglishOpacity || !readOpacity) {
throw new Error('CSS custom properties not set');
}
this.testResults[testName] = { status: 'passed' };
console.log(`✅ ${testName} passed`);
}
/**
* Test Language Detection
*/
async testLanguageDetection() {
const testName = 'Language Detection';
console.log(`🌐 Testing ${testName}...`);
if (!window.nhentaiPlusEnhanced?.systems?.language()) {
throw new Error('Language detection system not initialized');
}
// Test on galleries
const galleries = document.querySelectorAll('.gallery');
if (galleries.length > 0) {
const hasLanguageData = Array.from(galleries).some(gallery =>
gallery.getAttribute('data-detected-language')
);
if (!hasLanguageData) {
console.warn('⚠️ No language detection data found on galleries');
}
}
this.testResults[testName] = { status: 'passed' };
console.log(`✅ ${testName} passed`);
}
/**
* Test Tag Warning system
*/
async testTagWarningSystem() {
const testName = 'Tag Warning System';
console.log(`⚠️ Testing ${testName}...`);
const enabled = await GM.getValue('tagWarningEnabled', true);
if (!enabled) {
this.testResults[testName] = { status: 'skipped', reason: 'Feature disabled' };
return;
}
if (!window.nhentaiPlusEnhanced?.systems?.tagWarning()) {
throw new Error('Tag warning system not initialized');
}
this.testResults[testName] = { status: 'passed' };
console.log(`✅ ${testName} passed`);
}
/**
* Test Advanced Search system
*/
async testAdvancedSearch() {
const testName = 'Advanced Search';
console.log(`🔍 Testing ${testName}...`);
if (!window.nhentaiPlusEnhanced?.systems?.advancedSearch()) {
throw new Error('Advanced search system not initialized');
}
this.testResults[testName] = { status: 'passed' };
console.log(`✅ ${testName} passed`);
}
/**
* Test backward compatibility
*/
async testBackwardCompatibility() {
const testName = 'Backward Compatibility';
console.log(`🔄 Testing ${testName}...`);
// Test that existing features still work
const existingFeatures = [
'findSimilarEnabled',
'bookmarksEnabled',
'englishFilterEnabled',
'autoLoginEnabled'
];
for (const feature of existingFeatures) {
const value = await GM.getValue(feature);
if (value === undefined) {
console.warn(`⚠️ Existing feature ${feature} setting not found`);
}
}
this.testResults[testName] = { status: 'passed' };
console.log(`✅ ${testName} passed`);
}
/**
* Test performance
*/
async testPerformance() {
const testName = 'Performance';
console.log(`⚡ Testing ${testName}...`);
const startTime = performance.now();
// Simulate feature operations
if (window.nhentaiPlusEnhanced?.utils?.refreshAllSystems) {
await window.nhentaiPlusEnhanced.utils.refreshAllSystems();
}
const endTime = performance.now();
const duration = endTime - startTime;
this.performanceMetrics.refreshTime = duration;
if (duration > 1000) { // More than 1 second
console.warn(`⚠️ Performance warning: Refresh took ${duration.toFixed(2)}ms`);
}
this.testResults[testName] = { status: 'passed', metrics: { refreshTime: duration } };
console.log(`✅ ${testName} passed (${duration.toFixed(2)}ms)`);
}
/**
* Report test results
*/
reportResults() {
console.log('\n📊 Validation Results:');
console.table(this.testResults);
const passed = Object.values(this.testResults).filter(r => r.status === 'passed').length;
const failed = Object.values(this.testResults).filter(r => r.status === 'failed').length;
const skipped = Object.values(this.testResults).filter(r => r.status === 'skipped').length;
console.log(`\n✅ Passed: ${passed} | ❌ Failed: ${failed} | ⏭️ Skipped: ${skipped}`);
if (failed === 0) {
console.log('🎉 All tests passed! Nhentai Plus+ Enhanced Features are working correctly.');
} else {
console.log('⚠️ Some tests failed. Check the results above for details.');
}
}
}
// Run validation in development mode or when requested
if (window.location.search.includes('nhentai-plus-validate') ||
localStorage.getItem('nhentai-plus-debug') === 'true') {
setTimeout(() => {
const validator = new ValidationSystem();
validator.runValidationTests();
}, 2000);
}
// Add validation to global object
if (window.nhentaiPlusEnhanced) {
window.nhentaiPlusEnhanced.validation = {
run: () => {
const validator = new ValidationSystem();
return validator.runValidationTests();
}
};
}
//------------------------ **Validation and Testing** ------------------
//------------------------ **Mark as Read System** ------------------
// Function to add title attributes to gallery captions for better hover tooltips
async function addGalleryCaptionTooltips() {
const galleries = document.querySelectorAll('.gallery');
const galleryCaptionTooltipsEnabled = await GM.getValue('galleryCaptionTooltipsEnabled', true);
galleries.forEach(gallery => {
const caption = gallery.querySelector('.caption');
if (caption && !caption.hasAttribute('title')) {
// Get the title text (remove any extra elements like flags)
let titleText = caption.textContent.trim();
// Extract tags from data-tags attribute
const dataTags = gallery.getAttribute('data-tags');
let tagsText = '';
if (dataTags) {
// Convert tag IDs to readable format if possible
tagsText = '\n\nHover for details';
}
// Set the title attribute and tooltip-enabled class
caption.setAttribute('title', titleText + tagsText);
if (galleryCaptionTooltipsEnabled) {
caption.classList.add('tooltip-enabled');
}
}
});
}
// Initialize gallery caption tooltips
$(document).ready(function() {
// Add tooltips to existing galleries
addGalleryCaptionTooltips();
// Watch for new galleries being added dynamically
const observer = new MutationObserver(async function(mutations) {
const galleryCaptionTooltipsEnabled = await GM.getValue('galleryCaptionTooltipsEnabled', true);
mutations.forEach(function(mutation) {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(function(node) {
if (node.nodeType === 1) { // Element node
if (node.classList && node.classList.contains('gallery')) {
const caption = node.querySelector('.caption');
if (caption && !caption.hasAttribute('title')) {
let titleText = caption.textContent.trim();
caption.setAttribute('title', titleText);
if (galleryCaptionTooltipsEnabled) {
caption.classList.add('tooltip-enabled');
}
}
}
// Also check if the added node contains galleries
const galleries = node.querySelectorAll && node.querySelectorAll('.gallery');
if (galleries) {
galleries.forEach(gallery => {
const caption = gallery.querySelector('.caption');
if (caption && !caption.hasAttribute('title')) {
let titleText = caption.textContent.trim();
caption.setAttribute('title', titleText);
if (galleryCaptionTooltipsEnabled) {
caption.classList.add('tooltip-enabled');
}
}
});
}
}
});
}
});
});
// Start observing
observer.observe(document.body, {
childList: true,
subtree: true
});
});
// Initialize AutoSync on page load
$(document).ready(async function() {
// Wait a bit for the page to fully load before initializing autosync
setTimeout(async () => {
try {
await autoSyncManager.initialize();
console.log('AutoSync initialized on page load');
} catch (error) {
console.error('Failed to initialize AutoSync on page load:', error);
}
}, 3000); // 3 second delay to ensure everything is loaded
});
//----------------------------------------------------------**Manga-Sync**--------------------------------------------------------------------