// ==UserScript==
// @name NHentai Improved
// @namespace Hentiedup
// @version 2.1.1
// @description Partially fade/remove non-english, HQ thumbnails, mark as read, subs, version grouping etc.
// @author Hentiedup
// @license unlicense
// @match https://nhentai.net/*
// @require https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_listValues
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @grant GM_openInTab
// @icon https://i.imgur.com/1lihxY2.png
// @noframes
// ==/UserScript==
(() => {
//#region - Assets
const readImg = '<svg style="vertical-align: middle;" width="15" height="15" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="book" class="svg-inline--fa fa-book fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M448 360V24c0-13.3-10.7-24-24-24H96C43 0 0 43 0 96v320c0 53 43 96 96 96h328c13.3 0 24-10.7 24-24v-16c0-7.5-3.5-14.3-8.9-18.7-4.2-15.4-4.2-59.3 0-74.7 5.4-4.3 8.9-11.1 8.9-18.6zM128 134c0-3.3 2.7-6 6-6h212c3.3 0 6 2.7 6 6v20c0 3.3-2.7 6-6 6H134c-3.3 0-6-2.7-6-6v-20zm0 64c0-3.3 2.7-6 6-6h212c3.3 0 6 2.7 6 6v20c0 3.3-2.7 6-6 6H134c-3.3 0-6-2.7-6-6v-20zm253.4 250H96c-17.7 0-32-14.3-32-32 0-17.6 14.4-32 32-32h285.4c-1.9 17.1-1.9 46.9 0 64z"></path></svg>';
const unreadImg = '<svg style="vertical-align: middle;" width="15" height="15" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="book-open" class="svg-inline--fa fa-book-open fa-w-18" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="currentColor" d="M542.22 32.05c-54.8 3.11-163.72 14.43-230.96 55.59-4.64 2.84-7.27 7.89-7.27 13.17v363.87c0 11.55 12.63 18.85 23.28 13.49 69.18-34.82 169.23-44.32 218.7-46.92 16.89-.89 30.02-14.43 30.02-30.66V62.75c.01-17.71-15.35-31.74-33.77-30.7zM264.73 87.64C197.5 46.48 88.58 35.17 33.78 32.05 15.36 31.01 0 45.04 0 62.75V400.6c0 16.24 13.13 29.78 30.02 30.66 49.49 2.6 149.59 12.11 218.77 46.95 10.62 5.35 23.21-1.94 23.21-13.46V100.63c0-5.29-2.62-10.14-7.27-12.99z"></path></svg>';
const flagEn = "https://i.imgur.com/vSnHmmi.gif";
const flagJp = "https://i.imgur.com/GlArpuS.gif";
const flagCh = "https://i.imgur.com/7B55DYm.gif";
//#endregion
//#region - Initialize settings
//Non-english settings
const remove_non_english = GM_getValue("remove_non_english", false);
const partially_fade_all_non_english = GM_getValue("partially_fade_all_non_english", true);
const non_english_fade_opacity = GM_getValue("non_english_fade_opacity", 0.3);
//Comic reader system
const comic_reader_improved_zoom = GM_getValue("comic_reader_improved_zoom", true);
const remember_zoom_level = GM_getValue("remember_zoom_level", true);
const zoom_level = Number(GM_getValue("zoom_level", 1.0));
//Browse sections
const browse_thumbnail_width = GM_getValue("browse_thumbnail_width", 0);
const browse_thumnail_container_width = GM_getValue("browse_thumnail_container_width", 0);
const load_high_quality_browse_thumbnails = GM_getValue("load_high_quality_browse_thumbnails", true);
const infinite_load = GM_getValue("infinite_load", true);
//Comic pages view
const pages_thumbnail_width = GM_getValue("pages_thumbnail_width", 0);
const pages_thumnail_container_width = GM_getValue("pages_thumnail_container_width", 0);
const load_high_quality_pages_thumbnails = GM_getValue("load_high_quality_pages_thumbnails", true);
const max_image_reload_attempts = 20; //for load_high_quality_browse_thumbnails and load_high_quality_pages_thumbnails
//Mark as read system
const mark_as_read_system_enabled = GM_getValue("mark_as_read_system_enabled", true);
const marked_as_read_fade_opacity = GM_getValue("marked_as_read_fade_opacity", 0.3);
const read_tag_font_size = GM_getValue("read_tag_font_size", 15);
//Subscription system
const subscription_system_enabled = GM_getValue("subscription_system_enabled", true);
//Alt version grouping
const version_grouping_enabled = GM_getValue("version_grouping_enabled", true);
const version_grouping_filter_brackets = GM_getValue("version_grouping_filter_brackets", false);
const auto_group_on_page_comics = GM_getValue("auto_group_on_page_comics", true);
//#endregion
//#region - native blacklist settings
const remove_native_blacklisted = GM_getValue("remove_native_blacklisted", true);
//#endregion
//#region - Initialize Sets (stored as strigified arrays)
//Mark as read system
let MARSet = new Set(TryParseJSON(GM_getValue("MARArrayString", "[]"), []));
//Subsription system
let SubsSet = new Set(TryParseJSON(GM_getValue("SubArrayString", "[]"), []));
//#endregion
//#region - Other script global scope variables
let infinite_load_isLoadingNextPage = false;
const nativeBlacklist = [];
//#endregion
//#region - Apply some initial stylesheets
AddNonEnglishStylesheets();
AddImprovedReaderZoomStylesheets();
AddBrowseThumbnailStylesheets();
AddPagesThumbnailStylesheets();
AddMARStylesheets();
AddSubscriptionStylesheets();
AddAltVersionStylesheets();
AddInfiniteLoadStylesheets();
AddSettingsStylesheets();
//#endregion
//#region - code for all pages
//add tampermonkey menu item to access NHI settings
GM_registerMenuCommand("Settings", () => {
GM_openInTab("https://nhentai.net/nhi-settings/", { active: true });
});
if (subscription_system_enabled) {
//Add Subscriptions button in the header to take user to custom Subscriptions page
$(".menu.right").prepend('<li title="Subscriptions"><a id="header-subs-button" href="/subscriptions/"><i class="fa fa-heartbeat"></i><span> Subscriptions</span></a></li>');
}
const currentPagePath = window.location.pathname;
//#endregion
//#region - code for all pages with list of comics
if ($(".container.index-container, #favcontainer.container, #recent-favorites-container, #related-container").length !== 0) {
//Blacklisted removal
HandleBlacklistedRemoval();
//Non-english handling
HandleAllNonEnglishOnPage();
//Mark as read system
if (mark_as_read_system_enabled) {
//Mark all comics on page that have been read with READ visual tag
//possible problems with too long selectors? - splitting it up to chunks of 50
const parts = [];
for (let i = 0, count = MARSet.size; i < count; i += 50)
parts.push([...MARSet].slice(i, i + 50));
for (let i = 0, count = parts.length; i < count; i++) {
const readPartSelector = parts[i].join("'], .cover[href='");
$(`.cover[href='${readPartSelector}']`).addClass("marked-as-read").append("<div class='readTag'>READ</div>");
}
}
//Load HQ thumbnails for all comics on page
if (load_high_quality_browse_thumbnails) {
$(".container.index-container > .gallery > .cover > img, #favcontainer.container > .gallery-favorite > .gallery > .cover > img, #related-container.container > .gallery > .cover > img").on("load", OnLoadCoverReplaceHQ);
}
//Add alt version buttons and auto group on-page comics
if (version_grouping_enabled) {
AddVersionGroupingButtonsToJQuerySelector($(".gallery"));
if (auto_group_on_page_comics)
GroupAltVersionsOnPage();
}
InfiniteLoadHandling();
}
//#endregion
//#region - url path specific code
//Comic info page
if (/^\/g\/\d+?\/$/.test(currentPagePath)) {
//Mark as read system
if (mark_as_read_system_enabled) {
if (MARSet.has(currentPagePath)) //if item is marked as read (path == item. e.g. "/g/1234/")
$(".buttons").append(`<a href="#" id="nhi-mar-button" class="btn btn-secondary btn-nhi-mark-as-unread">${unreadImg} <span style="vertical-align: middle;">Mark as unread</span></a>`); //..add unmark button
else
$(".buttons").append(`<a href="#" id="nhi-mar-button" class="btn btn-secondary btn-nhi-mark-as-read">${readImg} <span style="vertical-align: middle;">Mark as read</span></a>`); //...add mark button
AddMARClickListeners();
}
//Subscriptions system
if (subscription_system_enabled) {
$(".tag").each((i, el) => {
if (SubsSet.has($(el).attr("href")))
$(el).addClass("subbedTag");
});
}
//HQ thumbnails
if (load_high_quality_pages_thumbnails) {
$("#thumbnail-container .thumb-container > .gallerythumb > img")?.on("load", OnLoadCoverReplaceHQ);
}
}
//Comic reader page
else if (comic_reader_improved_zoom && /^\/g\/\d+?\/\d+?\/$/.test(currentPagePath)) {
HandleReaderImprovedZoom();
}
//User pages
else if (subscription_system_enabled && currentPagePath.startsWith("/users/")) {
//User page
const pageUserName = $(".user-info > h1")?.text()?.trim();
//current page user == current logged in user
if (pageUserName && pageUserName === $("nav ul.menu.right a[href^='/users/']")?.text()?.replace(/<.+?>/g, "")?.trim()) {
$(".user-info > div").before(`<p><b>Comics marked as read: </b>${MARSet.size}</p>`); //add number of comics read to page
$("#user-container > .user-info a[href$='/edit']").before(`<a href="/nhi-settings/" id="nhi-nh-settings-button" type="button" class="btn btn-primary"><img src="https://i.imgur.com/TI45bnl.png" /><span>NHI Settings<span/></a>`);
}
}
//Subscriptions page
else if (currentPagePath === "/subscriptions/") {
RenderSubscriptionsPage();
}
//NHI Settings page
else if (currentPagePath === "/nhi-settings/") {
RenderNHISettingsPage();
}
//artist/group/tag/language/category pages
else if (currentPagePath.startsWith("/artist/") || currentPagePath.startsWith("/group/") || currentPagePath.startsWith("/tag/") || currentPagePath.startsWith("/language/") || currentPagePath.startsWith("/category/")) {
//if subscribed
if (SubsSet.has(currentPagePath.split("popular")[0])) //on these pages, the path before anything starting with "popular" should always match the saved value
$("h1:first").append('<a href="#" id="nhi-subscribe-button" class="btn btn-secondary btn-nhi-unsub"><span style="vertical-align: middle;">Unsubscribe</span></a>'); //...add unsub button
else
$("h1:first").append('<a href="#" id="nhi-subscribe-button" class="btn btn-secondary btn-nhi-sub"><span style="vertical-align: middle;">Subscribe</span></a>'); //...add sub button
AddSubsClickListeners();
}
//#endregion
//#region - FUNCTIONS
//#region - Misc functions
function TryParseJSON(jsonString, defaultValue = null) {
try {
return JSON.parse(jsonString);
} catch {
return defaultValue;
}
}
//#endregion
//#region - Blacklist related functions
function HandleBlacklistedRemoval() {
if (remove_native_blacklisted) {
//remove all on-page comics marked as blacklisted
$(".gallery.blacklisted").remove();
//Record list of blacklisted tags. Needed for infinite load.
if (infinite_load) {
//The native blacklisted tags can be found on one of the on page inline scripts...
const scripts = $("script:not([src])");
let tags = [];
for (script in scripts) {
tags = $(script)?.html()?.split("blacklisted_tags: [")?.[1]?.split("]")?.[0];
if (tags)
break;
}
if (tags) {
try {
nativeBlacklist.push(...JSON.parse(`[${tags}]`));
console.log(nativeBlacklist);
} catch {
console.log("NHI: failed to parse blacklisted tags");
}
}
else {
console.log("NHI: failed find blacklisted tags");
}
}
}
}
//#endregion
//#region - Settings related functions
function AddSettingsStylesheets() {
GM_addStyle(`
#nhi-nh-settings-button {
background-color: #1b2a37;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 8px 16px;
cursor: pointer;
transition: color 0.3s ease, filter 0.3s ease;
}
#nhi-nh-settings-button:hover,
#nhi-nh-settings-button:active {
color: #ed2553;
filter: brightness(1.4);
}
#nhi-nh-settings-button > img {
height: 25px;
}
.nhi-settings-page {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 10px;
max-width: 100%;
}
.nhi-settings-page .nhi-settings-section {
text-align: left;
display: block;
padding: 5px;
}
.nhi-settings-page .nhi-settings-section.nhi-settings-save-section {
padding-top: 15px;
display: flex;
align-items: center;
}
.nhi-settings-page #saveButt {
background-color: green;
margin-right: 10px;
}
.nhi-settings-page .nhi-settings-export-container .nhi-settings-section {
padding: 0;
}
.nhi-settings-page .nhi-settings-section h2 {
margin: 5px 0;
}
.nhi-settings-page .nhi-settings-section label {
margin: 5px;
}
.nhi-settings-page .nhi-settings-section label input {
margin-right: 5px;
}
.nhi-settings-page .nhi-settings-section input[type="number"],
.nhi-settings-page .nhi-settings-section input[type="text"] {
width: 80px;
height: 25px;
border-radius: 3px;
text-align: center;
padding: 0;
}
.nhi-settings-page .nhi-settings-section sub {
display: block;
line-height: 1em;
}
.nhi-settings-page a {
color: lightblue !important;
text-decoration: underline;
cursor: pointer;
}
.nhi-settings-export-container p {
margin: 0;
}
#NHIImportFile {
display: block;
margin-top: 5px;
}
`);
}
function RenderNHISettingsPage() {
//create page skeleton
$("head title").html('NHI Settings').prop("style", "font-size: 3.5em;");
$("#content").html(`
<div class="nhi-settings-main">
<form id="nhi-settings-form">
<div class="nhi-settings-section">
<h2>Non-English Settings</h2>
<label><input name="remove_non_english" type="checkbox" ${remove_non_english ? "checked" : ""}><span>Remove Non-English</span></label>
<label><input name="partially_fade_all_non_english" type="checkbox" ${partially_fade_all_non_english ? "checked" : ""}><span>Partially Fade Non-English</span></label>
<label><input name="non_english_fade_opacity" type="number" min="0" max="1" step="0.1" placeholder="0.3 - 1.0" value="${non_english_fade_opacity}"><span>Fade Opacity</span></label>
</div>
<div class="nhi-settings-section">
<h2>Browse Section Settings</h2>
<label><input name="infinite_load" type="checkbox" ${infinite_load ? "checked" : ""}><span>Dynamically load more comics as you scroll</span></label>
<label><input name="browse_thumbnail_width" type="number" min="0" placeholder="0" value="${browse_thumbnail_width}"><span>Thumbnail Width</span></label>
<label><input name="browse_thumnail_container_width" type="number" min="0" placeholder="0" value="${browse_thumnail_container_width}"><span>Thumbnail Container Width</span></label>
<label><input name="load_high_quality_browse_thumbnails" type="checkbox" ${load_high_quality_browse_thumbnails ? "checked" : ""}><span>HQ Thumbnails</span></label>
</div>
<div class="nhi-settings-section">
<h2>Comic Pages Section Settings</h2>
<label><input name="pages_thumbnail_width" type="number" min="0" placeholder="0" value="${pages_thumbnail_width}"><span>Thumbnail Width</span></label>
<label><input name="pages_thumnail_container_width" type="number" min="0" placeholder="0" value="${pages_thumnail_container_width}"><span>Thumbnail Container Width</span></label>
<label><input name="load_high_quality_pages_thumbnails" type="checkbox" ${load_high_quality_pages_thumbnails ? "checked" : ""}><span>HQ Thumbnails</span></label>
</div>
<div class="nhi-settings-section">
<h2>Comic Reader Settings</h2>
<label><input name="comic_reader_improved_zoom" type="checkbox" ${comic_reader_improved_zoom ? "checked" : ""}><span>Improved Zoom</span></label>
<label><input name="remember_zoom_level" type="checkbox" ${remember_zoom_level ? "checked" : ""}><span>Remember Zoom Level</span></label>
</div>
<div class="nhi-settings-section">
<h2>Mark As Read Settings</h2>
<label><input name="mark_as_read_system_enabled" type="checkbox" ${mark_as_read_system_enabled ? "checked" : ""}><span>Enabled</span></label>
<label><input name="marked_as_read_fade_opacity" type="number" min="0" max="1" step="0.1" placeholder="0.3 - 1.0" value="${marked_as_read_fade_opacity}"><span>Fade Opacity</span></label>
<label><input name="read_tag_font_size" type="number" min="0" placeholder="15" value="${read_tag_font_size}"><span>Read Tag Font Size</span></label>
</div>
<div class="nhi-settings-section">
<h2>Subscription Settings</h2>
<label><input name="subscription_system_enabled" type="checkbox" ${subscription_system_enabled ? "checked" : ""}><span>Enabled</span></label>
</div>
<div class="nhi-settings-section">
<h2>Version Grouping Settings</h2>
<label><input name="version_grouping_enabled" type="checkbox" ${version_grouping_enabled ? "checked" : ""}><span>Enabled</span></label>
<label style="margin-bottom: 0;"><input name="version_grouping_filter_brackets" type="checkbox" ${version_grouping_filter_brackets ? "checked" : ""}><span>Filter out normal brackets for version searches</span></label>
<sub>(square brackets are always filtered out regardless of this setting)</sub>
<label style="margin-bottom: 0;"><input name="auto_group_on_page_comics" type="checkbox" ${auto_group_on_page_comics ? "checked" : ""}><span>Automatically group on-page comics</span></label>
<sub>(Doesn't search the site, just current page)</sub>
</div>
<div class="nhi-settings-section">
<h2>Native Blacklist Settings</h2>
<a id="adjust-blacklist-link" href="#" target="_blank">Adjust Blacklist</a>
<label><input name="remove_native_blacklisted" type="checkbox" ${remove_native_blacklisted ? "checked" : ""}><span>Remove blacklisted comics</span></label>
</div>
<div class="nhi-settings-section nhi-settings-save-section">
<input id="saveButt" style="line-height: 30px; height: 30px;" type="submit" class="btn btn-primary" value="Save" />
<span id="savedIndicator" style="display: none; color: green; font-weight: 600;">Saved!</span>
</div>
</form>
<hr />
<div class="nhi-settings-export-container">
<div class="nhi-settings-section">
<h2 style="margin-bottom: 0;">Import/Export Settings</h2>
<p>Also Imports/Exports data</p>
<p>(comics marked as read and subscriptions)</p>
</br>
<button id="exportButt" style="line-height: 30px; height: 30px;" type="button" class="btn btn-primary">Export</button>
<button id="importButt" style="line-height: 30px; height: 30px;" type="button" class="btn btn-primary">Import</button>
<input id="NHIImportFile" type="file" style="display: none;">
</div>
</div>
</div>
`).addClass("nhi-settings-page").before("<h1>NHentai Improved Settings</h1>");
//set adjust-blacklist-link href
$("#adjust-blacklist-link").attr("href", `${$("nav .menu.right [href^='/users/']").attr("href")}/blacklist`);
//Handle disables/fadeouts
//set onChange event handlers
$(".nhi-settings-section input[name='remove_non_english']").on("change", (e) => {
$(".nhi-settings-section input[name='partially_fade_all_non_english']").prop("disabled", e.target.checked).closest("label").fadeTo(200, e.target.checked ? 0.5 : 1);
$(".nhi-settings-section input[name='non_english_fade_opacity']").prop("disabled", e.target.checked).closest("label").fadeTo(200, e.target.checked ? 0.5 : 1);
});
$(".nhi-settings-section input[name='comic_reader_improved_zoom']").on("change", (e) => {
$(".nhi-settings-section input[name='remember_zoom_level']").prop("disabled", !e.target.checked).closest("label").fadeTo(200, !e.target.checked ? 0.5 : 1);
});
$(".nhi-settings-section input[name='mark_as_read_system_enabled']").on("change", (e) => {
$(".nhi-settings-section input[name='marked_as_read_fade_opacity']").prop("disabled", !e.target.checked).closest("label").fadeTo(200, !e.target.checked ? 0.5 : 1);
$(".nhi-settings-section input[name='read_tag_font_size']").prop("disabled", !e.target.checked).closest("label").fadeTo(200, !e.target.checked ? 0.5 : 1);
});
$(".nhi-settings-section input[name='version_grouping_enabled']").on("change", (e) => {
$(".nhi-settings-section input[name='version_grouping_filter_brackets']").prop("disabled", !e.target.checked).closest("label").fadeTo(200, !e.target.checked ? 0.5 : 1).parent().find("sub").fadeTo(200, !e.target.checked ? 0.5 : 1);
$(".nhi-settings-section input[name='auto_group_on_page_comics']").prop("disabled", !e.target.checked).closest("label").fadeTo(200, !e.target.checked ? 0.5 : 1).parent().find("sub").fadeTo(200, !e.target.checked ? 0.5 : 1);
});
//trigger all the onChange event handlers to setup initial states
$(`
.nhi-settings-section input[name='remove_non_english'],
.nhi-settings-section input[name='comic_reader_improved_zoom'],
.nhi-settings-section input[name='mark_as_read_system_enabled'],
.nhi-settings-section input[name='version_grouping_enabled']
`).trigger("change");
//Handle save
$("#nhi-settings-form").on("submit", (e) => {
e.preventDefault();
const data = e.target;
GM_setValue("remove_non_english", data.remove_non_english.checked);
GM_setValue("partially_fade_all_non_english", data.partially_fade_all_non_english.checked);
GM_setValue("non_english_fade_opacity", data.non_english_fade_opacity.value);
GM_setValue("infinite_load", data.infinite_load.checked);
GM_setValue("browse_thumbnail_width", data.browse_thumbnail_width.value);
GM_setValue("browse_thumnail_container_width", data.browse_thumnail_container_width.value);
GM_setValue("load_high_quality_browse_thumbnails", data.load_high_quality_browse_thumbnails.checked);
GM_setValue("pages_thumbnail_width", data.pages_thumbnail_width.value);
GM_setValue("pages_thumnail_container_width", data.pages_thumnail_container_width.value);
GM_setValue("load_high_quality_pages_thumbnails", data.load_high_quality_pages_thumbnails.checked);
GM_setValue("comic_reader_improved_zoom", data.comic_reader_improved_zoom.checked);
GM_setValue("remember_zoom_level", data.remember_zoom_level.checked);
GM_setValue("mark_as_read_system_enabled", data.mark_as_read_system_enabled.checked);
GM_setValue("marked_as_read_fade_opacity", data.marked_as_read_fade_opacity.value);
GM_setValue("read_tag_font_size", data.read_tag_font_size.value);
GM_setValue("subscription_system_enabled", data.subscription_system_enabled.checked);
GM_setValue("version_grouping_enabled", data.version_grouping_enabled.checked);
GM_setValue("version_grouping_filter_brackets", data.version_grouping_filter_brackets.checked);
GM_setValue("auto_group_on_page_comics", data.auto_group_on_page_comics.checked);
GM_setValue("remove_native_blacklisted", data.remove_native_blacklisted.checked);
$("#savedIndicator").hide().fadeIn(200, function () {
setTimeout(() => {
$(this).fadeOut(200);
}, 5000);
});
});
//Handle Export
$("#exportButt").click((e) => {
e.preventDefault();
const jsonObj = {
"remove_non_english": GM_getValue("remove_non_english", false),
"partially_fade_all_non_english": GM_getValue("partially_fade_all_non_english", true),
"non_english_fade_opacity": GM_getValue("non_english_fade_opacity", 0.3),
"infinite_load": GM_getValue("infinite_load", true),
"browse_thumbnail_width": GM_getValue("browse_thumbnail_width", 0),
"browse_thumnail_container_width": GM_getValue("browse_thumnail_container_width", 0),
"load_high_quality_browse_thumbnails": GM_getValue("load_high_quality_browse_thumbnails", true),
"pages_thumbnail_width": GM_getValue("pages_thumbnail_width", 0),
"pages_thumnail_container_width": GM_getValue("pages_thumnail_container_width", 0),
"load_high_quality_pages_thumbnails": GM_getValue("load_high_quality_pages_thumbnails", true),
"comic_reader_improved_zoom": GM_getValue("comic_reader_improved_zoom", true),
"remember_zoom_level": GM_getValue("remember_zoom_level", true),
"mark_as_read_system_enabled": GM_getValue("mark_as_read_system_enabled", true),
"marked_as_read_fade_opacity": GM_getValue("marked_as_read_fade_opacity", 0.3),
"read_tag_font_size": GM_getValue("read_tag_font_size", 15),
"subscription_system_enabled": GM_getValue("subscription_system_enabled", true),
"version_grouping_enabled": GM_getValue("version_grouping_enabled", true),
"version_grouping_filter_brackets": GM_getValue("version_grouping_filter_brackets", false),
"auto_group_on_page_comics": GM_getValue("auto_group_on_page_comics", true),
"remove_native_blacklisted": GM_getValue("remove_native_blacklisted", true),
"MARArrayString": GM_getValue("MARArrayString", "[]"),
"SubArrayString": GM_getValue("SubArrayString", "[]")
};
SaveToFile(`NHI-Backup_${new Date().toISOString().replace(/:/g, "-")}.nhi2`, JSON.stringify(jsonObj, null, 2));
});
//Handle Import
$("#importButt").click((e) => {
e.preventDefault();
$("#NHIImportFile").animate({ height: 'toggle' }, 200);
});
$("#NHIImportFile").change((event) => {
if (typeof window.FileReader !== 'function')
throw ("The file API isn't supported on this browser.");
const input = event.target;
if (!input)
throw ("The browser does not properly implement the event object");
if (!input.files)
throw ("This browser does not support the `files` property of the file input.");
const file = input.files[0];
if (!file.name.endsWith(".nhi") && !file.name.endsWith(".nhi2")) {
$("#NHIImportFile").val('');
throw ("Invalid file extension");
}
const fr = new FileReader();
fr.onload = (ev) => {
const importedData = ev.target.result;
if (importedData != null && confirm("File received. Import this file?")) {
if (file.name.endsWith(".nhi2"))
ImportNewNHIFile(importedData);
else
ImportOldNHIFile(importedData);
}
else
$("#NHIImportFile").val('');
};
fr.readAsText(file);
});
}
function ImportNewNHIFile(dataAsString) {
const jsonObj = JSON.parse(dataAsString);
GM_setValue("remove_non_english", jsonObj.remove_non_english);
GM_setValue("partially_fade_all_non_english", jsonObj.partially_fade_all_non_english);
GM_setValue("non_english_fade_opacity", jsonObj.non_english_fade_opacity);
GM_setValue("infinite_load", jsonObj.infinite_load);
GM_setValue("browse_thumbnail_width", jsonObj.browse_thumbnail_width);
GM_setValue("browse_thumnail_container_width", jsonObj.browse_thumnail_container_width);
GM_setValue("load_high_quality_browse_thumbnails", jsonObj.load_high_quality_browse_thumbnails);
GM_setValue("pages_thumbnail_width", jsonObj.pages_thumbnail_width);
GM_setValue("pages_thumnail_container_width", jsonObj.pages_thumnail_container_width);
GM_setValue("load_high_quality_pages_thumbnails", jsonObj.load_high_quality_pages_thumbnails);
GM_setValue("comic_reader_improved_zoom", jsonObj.comic_reader_improved_zoom);
GM_setValue("remember_zoom_level", jsonObj.remember_zoom_level);
GM_setValue("mark_as_read_system_enabled", jsonObj.mark_as_read_system_enabled);
GM_setValue("marked_as_read_fade_opacity", jsonObj.marked_as_read_fade_opacity);
GM_setValue("read_tag_font_size", jsonObj.read_tag_font_size);
GM_setValue("subscription_system_enabled", jsonObj.subscription_system_enabled);
GM_setValue("version_grouping_enabled", jsonObj.version_grouping_enabled);
GM_setValue("version_grouping_filter_brackets", jsonObj.version_grouping_filter_brackets);
GM_setValue("auto_group_on_page_comics", jsonObj.auto_group_on_page_comics);
GM_setValue("remove_native_blacklisted", jsonObj.remove_native_blacklisted);
GM_setValue("MARArrayString", jsonObj.MARArrayString);
GM_setValue("SubArrayString", jsonObj.SubArrayString);
location.reload();
}
function ImportOldNHIFile(dataAsString) {
const dataString = dataAsString.replace(/(\r?\n|\r)/g, "").trim(); //remove newlines and trim
if (dataString.indexOf("|||||") < 0) {
alert("invalid data");
return;
}
const dataArr = dataString.split("|||||");
if (dataArr.length > 0) {
GM_setValue("SubArrayString", dataArr[0]);
console.log("NHI - SubArrayString imported");
}
if (dataArr.length > 1) {
GM_setValue("MARArrayString", dataArr[1]);
console.log("NHI - MARArrayString imported");
}
if (dataArr.length > 2) {
GM_setValue("BlockTagArrayString", dataArr[2]);
console.log("NHI - BlockTagArrayString imported");
}
if (dataArr.length > 3) {
GM_setValue("remove_non_english", (String(dataArr[3]) == "true"));
console.log("NHI - remove_non_english imported");
}
if (dataArr.length > 4) {
GM_setValue("partially_fade_all_non_english", (String(dataArr[4]) == "true"));
console.log("NHI - partially_fade_all_non_english imported");
}
if (dataArr.length > 5) {
GM_setValue("non_english_fade_opacity", dataArr[5]);
console.log("NHI - non_english_fade_opacity imported");
}
if (dataArr.length > 6) {
GM_setValue("load_high_quality_browse_thumbnails", (String(dataArr[6]) == "true"));
console.log("NHI - load_high_quality_browse_thumbnails imported");
}
if (dataArr.length > 7) {
GM_setValue("browse_thumbnail_width", dataArr[7]);
console.log("NHI - browse_thumbnail_width imported");
}
if (dataArr.length > 8) {
GM_setValue("browse_thumnail_container_width", dataArr[8]);
console.log("NHI - browse_thumnail_container_width imported");
}
if (dataArr.length > 9) {
GM_setValue("load_high_quality_pages_thumbnails", (String(dataArr[9]) == "true"));
console.log("NHI - load_high_quality_pages_thumbnails imported");
}
if (dataArr.length > 10) {
GM_setValue("pages_thumbnail_width", dataArr[10]);
console.log("NHI - pages_thumbnail_width imported");
}
if (dataArr.length > 11) {
GM_setValue("pages_thumnail_container_width", dataArr[11]);
console.log("NHI - pages_thumnail_container_width imported");
}
if (dataArr.length > 12) {
GM_setValue("max_image_reload_attempts", dataArr[12]);
console.log("NHI - max_image_reload_attempts imported");
}
if (dataArr.length > 13) {
GM_setValue("mark_as_read_system_enabled", (String(dataArr[13]) == "true"));
console.log("NHI - mark_as_read_system_enabled imported");
}
if (dataArr.length > 14) {
GM_setValue("marked_as_read_fade_opacity", dataArr[14]);
console.log("NHI - marked_as_read_fade_opacity imported");
}
if (dataArr.length > 15) {
GM_setValue("read_tag_font_size", dataArr[15]);
console.log("NHI - read_tag_font_size imported");
}
if (dataArr.length > 16) {
GM_setValue("subscription_system_enabled", (String(dataArr[16]) == "true"));
console.log("NHI - subscription_system_enabled imported");
}
if (dataArr.length > 17) {
GM_setValue("version_grouping_enabled", (String(dataArr[17]) == "true"));
console.log("NHI - version_grouping_enabled imported");
}
if (dataArr.length > 18) {
GM_setValue("version_grouping_filter_brackets", (String(dataArr[18]) == "true"));
console.log("NHI - version_grouping_filter_brackets imported");
}
if (dataArr.length > 19) {
GM_setValue("auto_group_on_page_comics", (String(dataArr[19]) == "true"));
console.log("NHI - auto_group_on_page_comics imported");
}
if (dataArr.length > 20) {
GM_setValue("comic_reader_improved_zoom", (String(dataArr[20]) == "true"));
console.log("NHI - comic_reader_improved_zoom imported");
}
if (dataArr.length > 21) {
GM_setValue("remember_zoom_level", (String(dataArr[21]) == "true"));
console.log("NHI - remember_zoom_level imported");
}
if (dataArr.length > 22) {
GM_setValue("zoom_level", Number(dataArr[22]));
console.log("NHI - zoom_level imported");
}
if (dataArr.length > 23) {
GM_setValue("tag_blocking_enabled", (String(dataArr[23]) == "true"));
console.log("NHI - tag_blocking_enabled imported");
}
if (dataArr.length > 24) {
GM_setValue("tag_blocking_fade_only", (String(dataArr[24]) == "true"));
console.log("NHI - tag_blocking_fade_only imported");
}
if (dataArr.length > 25) {
GM_setValue("infinite_load", (String(dataArr[25]) == "true"));
console.log("NHI - infinite_load imported");
}
location.reload();
}
function SaveToFile(filename, dataString) {
const blob = new Blob([dataString], { type: 'application/json' });
if (window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveBlob(blob, filename);
}
else {
const elem = window.document.createElement('a');
elem.href = window.URL.createObjectURL(blob);
elem.download = filename;
document.body.appendChild(elem);
elem.click();
document.body.removeChild(elem);
}
}
//#endregion
//#region - Mark as read system related functions
function AddMARStylesheets() {
if (mark_as_read_system_enabled) {
GM_addStyle(`
.marked-as-read > img, .marked-as-read > .caption {
opacity: ${marked_as_read_fade_opacity};
}
.readTag {
border-radius: 10px;
padding: 5px 10px;
position: absolute;
background-color: rgba(0,0,0,.7);
color: rgba(255,255,255,.8);
bottom: 7.5px;
right: 7.5px;
font-size: ${read_tag_font_size}px;
font-weight: 900;
opacity: 1;
}
#info >.buttons > .btn-nhi-mark-as-read, #info >.buttons > .btn-nhi-mark-as-read:visited {
background-color: #3d9e48;
}
#info >.buttons > .btn-nhi-mark-as-read:active, #info >.buttons > .btn-nhi-mark-as-read:hover {
background-color: #52bc5e;
}
#info >.buttons > .btn-nhi-mark-as-unread, #info >.buttons > .btn-nhi-mark-as-unread:visited {
background-color: rgb(218, 53, 53);
}
#info >.buttons > .btn-nhi-mark-as-unread:active, #info >.buttons > .btn-nhi-mark-as-unread:hover {
background-color: #e26060;
}
.gallery {
position: relative;
}
`);
}
}
function AddMARClickListeners() {
$("#nhi-mar-button").click(function () {
//check if we are adding or deleting
const markingAsRead = $(this).hasClass("btn-nhi-mark-as-read");
//get the array again to make sure we have an up to date array (since other tabs could have modified it since loading this page)
MARSet = new Set(TryParseJSON(GM_getValue("MARArrayString", "[]"), []));
//add or delete
if (markingAsRead)
MARSet.add(currentPagePath);
else
MARSet.delete(currentPagePath);
//save changes
const MARArrayString = JSON.stringify([...MARSet]); //covert set to array string
GM_setValue("MARArrayString", MARArrayString); //save string
//update button
if (markingAsRead)
$(this).html(`${unreadImg} <span style="vertical-align: middle;">Mark as unread</span>`).removeClass('btn-nhi-mark-as-read').addClass('btn-nhi-mark-as-unread');
else
$(this).html(`${readImg} <span style="vertical-align: middle;">Mark as read</span>`).removeClass('btn-nhi-mark-as-unread').addClass('btn-nhi-mark-as-read');
});
}
//#endregion
//#region - Infinite load related functions
function AddInfiniteLoadStylesheets() {
if (infinite_load) {
GM_addStyle(`
#NHI_loader_icon {
height: 355px;
line-height: 355px;
}
#NHI_loader_icon > div {
display: inline-flex;
}
.loader {
color: #ed2553;
font-size: 10px;
width: 1em;
height: 1em;
border-radius: 50%;
position: relative;
text-indent: -9999em;
animation: mulShdSpin 1.3s infinite linear;
transform: translateZ(0);
}
@keyframes mulShdSpin {
0%,
100% {
box-shadow: 0 -3em 0 0.2em,
2em -2em 0 0em, 3em 0 0 -1em,
2em 2em 0 -1em, 0 3em 0 -1em,
-2em 2em 0 -1em, -3em 0 0 -1em,
-2em -2em 0 0;
}
12.5% {
box-shadow: 0 -3em 0 0, 2em -2em 0 0.2em,
3em 0 0 0, 2em 2em 0 -1em, 0 3em 0 -1em,
-2em 2em 0 -1em, -3em 0 0 -1em,
-2em -2em 0 -1em;
}
25% {
box-shadow: 0 -3em 0 -0.5em,
2em -2em 0 0, 3em 0 0 0.2em,
2em 2em 0 0, 0 3em 0 -1em,
-2em 2em 0 -1em, -3em 0 0 -1em,
-2em -2em 0 -1em;
}
37.5% {
box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,
3em 0em 0 0, 2em 2em 0 0.2em, 0 3em 0 0em,
-2em 2em 0 -1em, -3em 0em 0 -1em, -2em -2em 0 -1em;
}
50% {
box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,
3em 0 0 -1em, 2em 2em 0 0em, 0 3em 0 0.2em,
-2em 2em 0 0, -3em 0em 0 -1em, -2em -2em 0 -1em;
}
62.5% {
box-shadow: 0 -3em 0 -1em, 2em -2em 0 -1em,
3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 0,
-2em 2em 0 0.2em, -3em 0 0 0, -2em -2em 0 -1em;
}
75% {
box-shadow: 0em -3em 0 -1em, 2em -2em 0 -1em,
3em 0em 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em,
-2em 2em 0 0, -3em 0em 0 0.2em, -2em -2em 0 0;
}
87.5% {
box-shadow: 0em -3em 0 0, 2em -2em 0 -1em,
3em 0 0 -1em, 2em 2em 0 -1em, 0 3em 0 -1em,
-2em 2em 0 0, -3em 0em 0 0, -2em -2em 0 0.2em;
}
}
`);
}
}
function InfiniteLoadHandling() {
/*TODO:
- Handle auto version grouping...
Sometimes there are comics that are named something overly simple that causes tons of others to get grouped with it erroneously.
Infinite load would make this issue way worse... How to handle this...
Could group comcis only on a page by page basis.
- Handle Favorites page -> infinite load disabled there for now
*/
if (infinite_load) {
//if found paginator on page (also, specifically not enabled on favorites page for now)
const paginator = $(".pagination");
if (paginator?.length && currentPagePath !== "/favorites/") {
const lastPageNum = Number.parseInt(paginator.find(".last").attr("href").split("page=")[1]);
//build the fetch url without the page number
const queryWithNoPage = window.location.search.replace(/[\?\&]page=\d+/, "").replace(/^\&/, "?");
const finalUrlWithoutPageNum = `${window.location.pathname + queryWithNoPage + (queryWithNoPage.length ? "&" : "?")}page=`;
//on scroll event,
$(window).scroll(() => {
//if we are near page bottom,
if ($(window).scrollTop() + $(window).height() >= $(document).height() - 15) {
const loadingPageNum = Number.parseInt($(".pagination > .page.current:last").attr("href").split("page=")[1]) + 1;
TryLoadInNextPageComics(loadingPageNum, lastPageNum, finalUrlWithoutPageNum);
}
});
//While page is smaller than viewport, i.e. can't be scrolled, keep loading in pages
const autoLoadWhileScrollNotAvailableInterval = setInterval(() => {
//clear this interval if all pages are loaded
const loadingPageNum = Number.parseInt($(".pagination > .page.current:last").attr("href").split("page=")[1]) + 1;
if (loadingPageNum > lastPageNum) {
clearInterval(autoLoadWhileScrollNotAvailableInterval);
}
else {
const doc = document.documentElement;
if (doc.scrollHeight <= doc.clientHeight) {
TryLoadInNextPageComics(loadingPageNum, lastPageNum, finalUrlWithoutPageNum);
}
}
}, 200);
}
}
}
function TryLoadInNextPageComics(pageNumToLoad, lastPageNum, fetchUrlWithoutPageNum, retryNum = 0, maxFetchAttempts = 5) {
//Do not start another load if one is already running (retry attempts are let through)
if (retryNum === 0 && infinite_load_isLoadingNextPage)
return;
//don't try to load from a page > lastpage
if (pageNumToLoad > lastPageNum)
return;
//console.log(`NHI infinite load - starting page ${pageNumToLoad} load`);
infinite_load_isLoadingNextPage = true;
//add loader icon visual
$(".index-container:not(.advertisement, .index-popular)").first().append('<div id="NHI_loader_icon" class="gallery"><div><span class="loader"></span></div></div>');
$.get({
url: fetchUrlWithoutPageNum + pageNumToLoad,
dataType: "html"
}, (data) => {
//for each comic fetched
$(data).find("div.gallery").each((i, el) => {
//blacklist handling
if (remove_native_blacklisted) {
//probably won't work, but if the comic has blacklisted class, don't add it
if ($(el).hasClass("blacklisted"))
return;
//check if this comic has any blacklisted tags and if so, don't add it
const tags = $(el).attr("data-tags").trim().split(" ");
if (nativeBlacklist.some(nblTag => tags.includes(String(nblTag)))) {
return;
}
}
//if already on page (excluding the page 1 popular section), don't add again
if ($(`.container:not(.index-popular) .cover[href='${$(el).find(".cover").attr("href")}']`).length)
return;
//If removing or fading non-english
if (remove_non_english || partially_fade_all_non_english) {
//Mark with english class/tag + if removing non-english...
if (!MarkIfEnglish($(el)) && remove_non_english) {
return; //...don't insert this comic
}
}
//set thumbnail src = data-src
$(el).find("img").attr("src", $(el).find("img").attr("data-src"));
//HQ thumbnail onLoad
if (load_high_quality_browse_thumbnails)
$(el).find(".cover > img").on("load", OnLoadCoverReplaceHQ);
//check if read, and mark as such
if (mark_as_read_system_enabled) {
const cover = $(el).find("a.cover");
const item = cover.attr("href");
if (MARSet.has(item))
cover.addClass("marked-as-read").append("<div class='readTag'>READ</div>");
}
//add version grouping buttons
if (version_grouping_enabled)
AddVersionGroupingButtonsToJQuerySelector($(el));
//finally add the modified comic on to page
$(".index-container:not(.advertisement, .index-popular)").first().append(el);
});
//after adding all comics from fetched page, mark that page as "current" in the paginator to clearly show the user all the pages currently loaded
const paginatorItem = $(`.pagination > .page[href$='page=${pageNumToLoad}']`);
if (paginatorItem?.length)
paginatorItem.addClass("current");
else
$(".pagination > .next").before(`<a href="${fetchUrlWithoutPageNum}${pageNumToLoad}" class="page current">${pageNumToLoad}</a>`);
$("#NHI_loader_icon").remove();
//console.log(`NHI infinite load - page ${pageNumToLoad} load successful`);
infinite_load_isLoadingNextPage = false;
}).fail((jqXHR, textStatus, errorThrown) => {
if (retryNum < maxFetchAttempts) {
console.log(`NHI: Infinite load - Failed loading page ${pageNumToLoad} - ${textStatus} | ${errorThrown} - retrying... (retry ${retryNum + 1})`);
TryLoadInNextPageComics(pageNumToLoad, lastPageNum, fetchUrlWithoutPageNum, retryNum + 1, 5);
}
else {
$("#NHI_loader_icon").remove();
console.log(`NHI: Infinite load - Failed loading page ${pageNumToLoad} - ${textStatus} | ${errorThrown} - Giving up after ${retryNum} retries.`);
infinite_load_isLoadingNextPage = false;
}
});
}
//#endregion
//#region - Alt version related functions
function AddAltVersionStylesheets() {
if (version_grouping_enabled) {
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;
}
.findVersionButton:focus, .findVersionButton:hover, .findVersionButton:active,
.versionNextButton:focus, .versionNextButton:hover, .versionNextButton:active,
.versionPrevButton:focus, .versionPrevButton:hover, .versionPrevButton:active
{
background-color: rgba(50,50,50,1);
}
`);
}
}
function AddVersionGroupingButtonsToJQuerySelector(JQSelector) {
JQSelector.append([
"<div class='findVersionButton'>Find Alt Versions</div>",
"<div class='numOfVersions'>1/1</div>",
"<div class='versionNextButton'>►</div>",
"<div class='versionPrevButton'>◄</div>"
]);
$(JQSelector).find(".findVersionButton").click(AddAltVersionsToThis);
$(JQSelector).find(".versionPrevButton").click(PrevAltVersion);
$(JQSelector).find(".versionNextButton").click(NextAltVersion);
}
function AddAltVersionsToThis(e) {
e.preventDefault();
const altVBtn = $(this);
const originalComic = $(this).parent(); //.gallery
const originalTitle = originalComic.find(".cover:visible > .caption").text();
$.get(BuildUrl(originalTitle), (data) => {
const found = $(data).find(".container > .gallery");
if (!found || found.length <= 0) {
alert("error reading data");
return;
}
originalComic.find(".cover").remove();
try {
//iterate over each alt comic found
for (let i = 0; i < found.length; i++) {
//fade or remove non-english
if (partially_fade_all_non_english || remove_non_english) {
const isEnglish = MarkIfEnglish($(found[i]));
if (!isEnglish && remove_non_english)
continue;
}
//add some missing flags
if ($(found[i]).attr("data-tags").split(" ").includes("12227")) //en
$(found[i]).find(".caption").append(`<img class="overlayFlag" src="${flagEn}">`);
else if ($(found[i]).attr("data-tags").split(" ").includes("6346")) //jp
$(found[i]).find(".caption").append(`<img class="overlayFlag" src="${flagJp}">`);
else if ($(found[i]).attr("data-tags").split(" ").includes("29963")) //ch
$(found[i]).find(".caption").append(`<img class="overlayFlag" src="${flagCh}">`);
//MAR
if (mark_as_read_system_enabled) {
//re-load MARSet to have up to date data
MARSet = new Set(TryParseJSON(GM_getValue("MARArrayString", "[]"), []));
const cover = $(found[i]).find(".cover");
//if MARSet has this comic, mark it as read
if (MARSet.has(cover.attr("href"))) {
cover.append("<div class='readTag'>READ</div>");
cover.addClass("marked-as-read");
}
}
//HQ thumbnail == Always using HQ thumbnails with Alt covers, because using normal covers would still require work to handle.
//TODO: Could add handling for non HQ covers later if I can be bothered
const coverImg = $(found[i]).find(".cover > img");
//if gallery item is missing data-src, add src to to data-src so that the generic method "ReplaceCoverImage" that relies on data-src can handle it
if (!$(coverImg).attr("data-src"))
$(coverImg).attr("data-src", $(coverImg).attr("src"));
ReplaceCoverImage($(coverImg));
originalComic.append($(found[i]).find(".cover"));
}
}
catch (er) {
alert(`error modifying data: ${er}`);
return;
}
originalComic.find(".cover:not(:first)").css("display", "none");
originalComic.find(".versionPrevButton, .versionNextButton, .numOfVersions").show(200);
originalComic.find(".numOfVersions").text(`1/${found.length}`);
altVBtn.hide(200);
}).fail((e) => {
alert(`error getting data: ${e}`);
});
}
function GroupAltVersionsOnPage() {
let i = 0;
let found = $(".container > .gallery");
while (!!found && i < found.length) {
AddAltVersionsToThisFromPage(found[i]);
i++;
found = $(".container > .gallery");
}
}
function AddAltVersionsToThisFromPage(target) {
const targetComic = $(target); //.gallery
targetComic.addClass("nhi-on-page-alt-ignoreThis");
const targetTitle = targetComic.find(".cover > .caption").text();
if (!targetTitle || targetTitle.length <= 0)
return;
const otherGalleries = $(".container > .gallery:not(.nhi-on-page-alt-ignoreThis)");
let numOfValid = 0;
for (let i = 0; i < otherGalleries.length; i++) //loop through galleries
{
const comicsInGallery = $(otherGalleries[i]).find(".cover");
let addAll = false;
for (let j = 0; j < comicsInGallery.length; j++) //loop through comics in the gallery
{
if (StringIncludesAllSearchTermsAfterCleanup($(comicsInGallery[j]).find(".caption").text(), targetTitle)) {
addAll = true; //if any of them match
break;
}
}
if (addAll) //if any matched
{
for (let j = 0; j < comicsInGallery.length; j++)
targetComic.append($(comicsInGallery[j])); //add all
$(otherGalleries[i]).addClass("nhi-on-page-alt-deleteThis");
numOfValid += comicsInGallery.length;
}
}
numOfValid++; //+1 because of the original target comic
targetComic.removeClass("nhi-on-page-alt-deleteThis"); //ensure the original target comic gallery doesn't get removed
targetComic.removeClass("nhi-on-page-alt-ignoreThis");
$(".nhi-on-page-alt-deleteThis").remove(); //remove all the galleries whose comics got inserted to target comic's gallery
//setup alt switching buttons if there is more than 1 comic in gallery
if (numOfValid > 1) {
targetComic.find(".cover:not(:first)").css("display", "none");
targetComic.find(".versionPrevButton, .versionNextButton, .numOfVersions").show(200);
targetComic.find(".numOfVersions").text(`1/${numOfValid}`);
}
}
function NextAltVersion(e) { SwitchAltVersion(e, this, true) }
function PrevAltVersion(e) { SwitchAltVersion(e, this, false) }
function SwitchAltVersion(ev, htmlEl, next) {
ev.preventDefault();
const toHide = $(htmlEl).parent().find(".cover").filter(":visible");
let toShow = next ? toHide.next() : toHide.prev();
if (!toShow || toShow.length <= 0)
return;
if (!toShow.is(".cover"))
toShow = next ? toHide.nextUntil(".cover", ":last").next() : toHide.prevUntil(".cover", ":last").prev();
if (!toShow || toShow.length <= 0)
return;
toHide.hide(100);
toShow.show(100);
const n = $(htmlEl).parent().find(".numOfVersions");
n.text(`${Number(n.text().split("/")[0]) + (next ? 1 : -1)}/${n.text().split("/")[1]}`);
}
function StringIncludesAllSearchTermsAfterCleanup(string, search) {
const cleanString = CleanupSearchString(string);
const cleanSearch = CleanupSearchString(search);
if (cleanString.length === 0 || cleanSearch.length === 0)
return false;
const searches = cleanSearch.split(" ");
//console.log(cleanString + " ::: includes all::: " + searches);
for (let i = 0; i < searches.length; i++)
if (!!searches[i] && searches[i].length > 0 && !cleanString.includes(searches[i]))
return false
return true;
}
function CleanupSearchString(title) {
let cleanTitle = title.replace(/\[.*?\]/g, "");
cleanTitle = cleanTitle.replace(/\【.*?\】/g, "");
if (version_grouping_filter_brackets)
cleanTitle = cleanTitle.replace(/\(.*?\)/g, "");
return cleanTitle.trim();
}
function BuildUrl(title) {
let url = CleanupSearchString(title);
url = url.trim();
//replace all instances of one or more consecutive symbol charactes padded by either whitespace or string start/end with a single space (except kanji)
url = url.replace(/(^|\s){1}([^\w\s\d\u3000-\u303F\u3040-\u309F\u30A0-\u30FF\uFF00-\uFFEF\u4E00-\u9FAF\u2605-\u2606\u2190-\u2195\u203B]|\_)+?(\s|$){1}/g, " ");
url = url.replace(/\s+/g, '" "'); //wrap all terms with ""
url = `"${url}"`;
url = encodeURIComponent(url);
url = `https://nhentai.net/search/?q=${url}`;
return url;
}
//#endregion
//#region - Subscription system related functions
function AddSubscriptionStylesheets() {
if (subscription_system_enabled) {
GM_addStyle(`
#tags .subbedTag * {
background: unset;
}
#tags .subbedTag, #tags .subbedTag:visited {
background-color: #2c5030;
}
#tags .subbedTag:active, #tags .subbedTag:hover {
background-color: #416144;
}
#nhi-subscribe-button.btn-nhi-sub, #nhi-subscribe-button.btn-nhi-sub:visited {
background-color: #3d9e48;
}
#nhi-subscribe-button.btn-nhi-sub:active, #nhi-subscribe-button.btn-nhi-sub:hover {
background-color: #52bc5e;
}
#nhi-subscribe-button.btn-nhi-unsub, #nhi-subscribe-button.btn-nhi-unsub:visited {
background-color: rgb(218, 53, 53);
}
#nhi-subscribe-button.btn-nhi-unsub:active, #nhi-subscribe-button.btn-nhi-unsub:hover {
background-color: #e26060;
}
#sub-content ul {
text-align: left;
}
#sub-content li {
box-sizing: border-box;
display: inline-block;
width: 25%;
text-align: center;
padding: 5px 20px;
}
#nhi-subscribe-button {
height: auto;
line-height: initial;
cursor: pointer;
font-size: 0.5em;
padding: 6px 12px;
}
@media only screen and (max-width: 1345px) {
.menu.right a[href^='/logout/'] span { display: none; }
}
@media only screen and (max-width: 1270px) {
.menu.right a[href^='/users/'] span { display: none; }
}
@media only screen and (max-width: 670px) {
.menu.right a[href='/favorites/'] span { display: none; }
}
@media only screen and (max-width: 600px) {
.menu.right a[href^='/logout/'] span { display: inline; }
.menu.right a[href^='/users/'] span { display: inline; }
.menu.right a[href='/favorites/'] span { display: inline; }
}
.nhi-subscriptions-page > .tag-container {
margin-bottom: 40px;
}
`);
}
}
function AddSubsClickListeners() {
$("#nhi-subscribe-button").click(function () {
//get proper sub item value
const subItem = currentPagePath.split("popular")[0];
//check if we are subbing or unsubbing
const isSubbing = $(this).hasClass("btn-nhi-sub");
//get the array again to make sure we have an up to date array (since other tabs could have modified it since loading this page)
SubsSet = new Set(TryParseJSON(GM_getValue("SubArrayString", "[]"), []));
//sub or unsub
if (isSubbing)
SubsSet.add(subItem);
else
SubsSet.delete(subItem);
//save changes
const SubArrayString = JSON.stringify([...SubsSet]); //covert set to array string
GM_setValue("SubArrayString", SubArrayString); //save string
//update button
if (isSubbing)
$(this).html('<span style="vertical-align: middle;">Unsubscribe</span>').removeClass('btn-nhi-sub').addClass('btn-nhi-unsub');
else
$(this).html('<span style="vertical-align: middle;">Subscribe</span>').removeClass('btn-nhi-unsub').addClass('btn-nhi-sub');
});
}
function RenderSubscriptionsPage() {
//create page skeleton
$("head title").html('Subscriptions').prop("style", "font-size: 3.5em;");
$("#content").html(
`<h1>Subscriptions</h1>
<h2>Artists</h2>
<div class="container tag-container artist-section"></div>
<h2>Groups</h2>
<div class="container tag-container group-section"></div>
<h2>Tags</h2>
<div class="container tag-container tag-section"></div>
<h2>Languages</h2>
<div class="container tag-container language-section"></div>
<h2>Categories</h2>
<div class="container tag-container category-section"></div>`
).addClass("nhi-subscriptions-page");
//add subs to page
for (const subItem of SubsSet) {
$(`.container.${subItem.split("/")[1]}-section`).append(`<a class="tag" href="${subItem}"><span class="name">${subItem.split("/")[2]}</span><span class="count">...</span></a>`);
}
//hide empty sections
$(".nhi-subscriptions-page > .tag-container").each((i, el) => {
if ($(el).is(":empty")) {
$(el).hide();
$(el).prev("h2").hide();
}
});
//load counts
$(".tag > .count").each((i, el) => {
SubsPageLoadTagCountWithDelay($(el), i * 200);
});
$(".tag > .count").hover(function () {
SubsPageLoadTagCountWithDelay($(this), 0);
});
}
function SubsPageLoadTagCountWithDelay(elem, delay) {
setTimeout(() => {
if (!elem.hasClass("count-fetch-in-progress") && !elem.hasClass("count-fetched")) {
elem.addClass("count-fetch-in-progress");
$.ajax({
url: elem.parent().prop("href"),
method: "GET"
}).done((data) => {
const found = $(data).find("h1 .tag > .count").text();
if (found != null && found.length > 0)
elem.text(found);
else
console.log(`failed finding tag from: ${elem.parent().prop("href")}`);
elem.addClass("count-fetched");
}).fail(() => {
console.log(`failed getting page: ${elem.parent().prop("href")}`);
}).always(() => { elem.removeClass("count-fetch-in-progress"); });
}
}, delay);
}
//#endregion
//#region - Non-english related functions
function AddNonEnglishStylesheets() {
if (!remove_non_english && partially_fade_all_non_english) {
GM_addStyle(`
.gallery > .cover:not(.is-english) > img,
.gallery > .cover:not(.is-english) > .caption
{
opacity: ${non_english_fade_opacity};
}
`);
}
}
function HandleAllNonEnglishOnPage() {
if (remove_non_english || partially_fade_all_non_english) {
$(".gallery").each((i, el) => {
if (!MarkIfEnglish($(el)) && remove_non_english)
$(el).remove();
});
}
}
function MarkIfEnglish(JQGalleryElement) {
if (JQGalleryElement?.length) {
//check for english tag
let isEnglish = JQGalleryElement.attr("data-tags").split(" ").includes("12227");
//if tag not found, check the title, since NH has started leaving out tags randomly...
if (!isEnglish) {
const title = JQGalleryElement.find(".cover > .caption").text();
if (/[\(\[]english[\)\]]/ig.test(title)) {
console.log(`NHI: Found comic that was not tagged as english (${title}), but includes '(English)' or '[English]' in the title. Adding missing tag and is-english class...`);
JQGalleryElement.attr("data-tags", `${JQGalleryElement.attr("data-tags")} 12227`);
isEnglish = true;
}
}
if (isEnglish) {
JQGalleryElement.find(".cover").addClass("is-english");
return true;
}
return false;
}
}
//#endregion
//#region - Reader related functions
function HandleReaderImprovedZoom() {
let prevVal = 1.0;
if (remember_zoom_level)
prevVal = zoom_level;
let curVal = prevVal;
SetReaderImageScale(curVal);
//make sure the current zoom-level stays between pages
new MutationObserver((mutations) => {
for (let i = 0; i < mutations.length; i++)
if (mutations[i].type === 'attributes')
SetReaderImageScale(curVal);
}).observe($("#image-container > a")[0], { attributes: true, childList: false, characterData: false });
const zoomOut = () => {
curVal = prevVal - 0.1;
if (curVal < 0.1)
curVal = 0.1;
SetReaderImageScale(curVal);
prevVal = curVal;
};
const zoomIn = () => {
curVal = prevVal + 0.1;
if (curVal > 3)
curVal = 3;
SetReaderImageScale(curVal);
prevVal = curVal;
}
$('body').on('keydown', (e) => {
if (e.key === '+') { zoomIn(); }
else if (e.key === '-') { zoomOut() }
});
$("section.reader-bar button.reader-zoom-out").click((e) => {
e.preventDefault();
e.stopPropagation();
zoomOut();
});
$("section.reader-bar button.reader-zoom-in").click((e) => {
e.preventDefault();
e.stopPropagation();
zoomIn();
});
}
function SetReaderImageScale(scale) {
$("section.reader-bar .zoom-level > .value").html(scale.toFixed(1));
$("#image-container img").css("width", 1280 * scale);
GM_setValue("zoom_level", scale);
}
//#endregion
//#region - HQ cover related functions
function AddImprovedReaderZoomStylesheets() {
if (comic_reader_improved_zoom) {
GM_addStyle("html.reader #image-container img { max-width: unset !important; }");
}
}
function AddBrowseThumbnailStylesheets() {
//#region - Apparently fixes issues with too long cover images +more?
//TODO: This is old code. Not sure if this is actually needed and not entirely sure what it does. Should test.
if (browse_thumbnail_width > 0) {
GM_addStyle(`
.gallery, .gallery > .cover {
max-height: ${browse_thumbnail_width * 1.42}px;
}
`);
}
GM_addStyle(`
.gallery > .cover {
overflow: hidden;
padding: 0 !important;
}
.gallery > .cover > img {
position: relative;
min-height: 100%;
}
.container.index-container, #favcontainer {
text-align: center;
}
.gallery > .cover > img {
width: 100%;
}
`);
//#endregion
if (browse_thumnail_container_width > 0) {
GM_addStyle(`
/*browsing comics*/
.container.index-container, #favcontainer {
width: ${browse_thumnail_container_width}px;
}
`);
}
if (browse_thumbnail_width > 0) {
GM_addStyle(`
/*browsing comics*/
.container.index-container > div.gallery, #favcontainer > .gallery-favorite {
width: ${browse_thumbnail_width}px;
}
`);
}
if (browse_thumnail_container_width > 0) {
GM_addStyle(`
/*browsing comics*/
.container.index-container, #favcontainer {
max-width: 100%;
}
`);
}
}
function AddPagesThumbnailStylesheets() {
if (pages_thumnail_container_width > 0) {
GM_addStyle(`
/*view comic pages*/
#thumbnail-container {
max-width: 100%;
width: ${pages_thumnail_container_width}px;
}
`);
}
if (pages_thumbnail_width > 0) {
GM_addStyle(`
/*view comic pages*/
div.thumb-container img {
width: ${pages_thumbnail_width}px;
}
`);
}
//#region
//TODO: This is old code. Not sure if this is actually needed and not entirely sure what it does. Should test.
GM_addStyle(`
/*view comic pages*/
div.thumb-container {
width: auto;
}
#thumbnail-container {
text-align: center;
}
`);
//#endregion
}
function OnLoadCoverReplaceHQ() {
//TODO: Throws errors all over the place... Seems to works for most images though. if it fails the original lower res image remains so failing is okay-ish
$(this).off("load");
ReplaceCoverImage($(this));
}
function ReplaceCoverImage(coverImg, addMultiTry = true) {
//TODO: Throws errors all over the place... Seems to works for most images though. if it fails the original lower res image remains so failing is okay-ish
if (addMultiTry)
AddMultiTryErrorHandlingForCoverImageLoad(coverImg);
//set src and try to load
const iNum = (Number.parseInt($(coverImg).attr("img-reloads") || 0) % 4) + 1;
const newsrc = ConvertThumbnailURL($(coverImg).attr("data-src"), iNum);
$(coverImg).attr("src", newsrc);
}
function AddMultiTryErrorHandlingForCoverImageLoad(coverImg) {
$(coverImg).off("error");
$(coverImg).on("error", function () {
//count reload attempts
let attempts = Number.parseInt($(this).attr("img-reloads") || 1);
if (attempts >= max_image_reload_attempts) //after x attempts, give up
{
$(this).off("error");
console.log(`gave up on: ${$(this).attr("src")}`);
return;
}
const iNum = (Number.parseInt($(this).attr("img-reloads") || 0) % 4) + 1;
const newsrc = ConvertThumbnailURL($(this).attr("data-src"), iNum);
$(this).attr("src", newsrc); //reload
attempts++;
$(this).attr("img-reloads", attempts);
console.log(`image reload attempt ${attempts} for: ${$(this).attr("src")}`);
});
}
function ConvertThumbnailURL(url, iNum = 1) {
return url?.replace(/\/\/t\d*?\./g, `//i${iNum}.`).replace("thumb.jpg", "1.jpg").replace("thumb.png", "1.png").replace("t.jpg", ".jpg").replace("t.png", ".png").replace("thumb.webp", "1.webp").replace("t.webp", ".webp");
}
//#endregion
//#endregion
})();