Sleazy Fork is available in English.

NHentai Improved

Partially fade/remove non-english, HQ thumbnails, mark as read, subs, version grouping etc.

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