Hitomi Enhancer

QoL changes to Hitomi, e.g., show page number, long-strip reader mode, "my favourites" page, ...

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Hitomi Enhancer
// @namespace    http://tampermonkey.net/
// @version      8.0
// @description  QoL changes to Hitomi, e.g., show page number, long-strip reader mode, "my favourites" page, ...
// @match        *://hitomi.la/*
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        unsafeWindow
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // ==========================================
    // MODULE 1: Global Configuration & Core
    // Purpose: Defines all static variables, CSS selectors, storage keys, default settings,
    // and responsive breakpoints used across the entire userscript.
    // ==========================================
    const CONFIG = {
        selectors: {
            galleryParent: '.gallery-preview',
            galleryThumbs: '.thumbnail-list',
            desktopReader: '#comicImages',
            mobileReader: '#mobileImages',
            pagination: '.simplePagerNav',
            listTitles: '.gallery-content > div > h1.lillie > a',
            pageNumberInjection: {
                container: '.dj-desc tbody',
                placeAfter: 'tr:last-child'
            },
            galleryPageInjection: {
                container: '.gallery-info tbody',
                placeAfter: 'tr:last-child'
            },
            galleryHeader: '.gallery h1',
            coverImage: 'div.cover > a > picture > img',
        },
        breakpoints: {
            mobile: 768
        },
        storageKeys: {
            showPageCount: 'hitomi_show_page_count',
            showGalleryPageCount: 'hitomi_show_gallery_page_count',
            galleryEnabled: 'hitomi_auto_gallery',
            readerEnabled: 'hitomi_auto_reader',
            gapSize: 'hitomi_gap_size',
            gapColor: 'hitomi_gap_color',
            devLogEnabled: 'hitomi_dev_log',
            gridColumnsDesktop: 'gallery_grid_columns_desktop',
            gridColumnsMobile: 'gallery_grid_columns_mobile',
            scrollableGallery: 'hitomi_scrollable_gallery',
            gridGapV: 'hitomi_grid_gap_v',
            gridGapH: 'hitomi_grid_gap_h',
            scrollGalleryHeight: 'hitomi_scroll_h',
            showFloatingPageIndicator: 'hitomi_floating_page_indicator',
            wrapTitles: 'hitomi_wrap_titles',
            scrollTitles: 'hitomi_scroll_titles',
            favorites: 'hitomi_favorites',
            favEntryWidth: 'hitomi_fav_width'
        },
        defaults: {
            showPageCount: true,
            showGalleryPageCount: true,
            galleryEnabled: false,
            readerEnabled: true,
            gapSize: 15,
            gapColor: '#000000',
            devLogEnabled: false,
            gridColumnsDesktop: 7,
            gridColumnsMobile: 2,
            scrollableGallery: true,
            gridGapV: 10,
            gridGapH: 10,
            scrollGalleryHeight: 700,
            showFloatingPageIndicator: true,
            wrapTitles: false,
            scrollTitles: true,
            favEntryWidth: 180
        },
        ui: {
            modalId: 'hitomi-ui-modal'
        }
    };

    // ==========================================
    // MODULE 2: Template Helper Module
    // Purpose: Centralized repository for all HTML structures, dynamically generated
    // CSS blocks, and UI template functions injected into the page.
    // ==========================================
    const TemplateModule = {
        CSS: {
            uiStyles: `
                #hitomi-ui-modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(250, 250, 255, 0.95); border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 20px; padding: 24px; width: 90%; max-width: 380px; box-sizing: border-box; z-index: 2000000; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15); font-family: 'Segoe UI', sans-serif; color: #1a1b26; display: none; max-height: 90vh; overflow-y: auto;}
                #hitomi-ui-modal input, #hitomi-ui-modal button { cursor: default; }
                .ui-card { background: rgba(0, 0, 0, 0.03); border-radius: 15px; padding: 16px; margin-top: 10px; border: 1px solid rgba(0, 0, 0, 0.05); }
                .ui-title { font-size: 1.2rem;
                    font-weight: bold;
                    margin-top: -10px;
                    margin-bottom: 15px;
                    text-align: center;
                    color: #1a1b26;
                    position: sticky;
                    top: -24px;
                    background: rgba(250, 250, 255, 1);
                    z-index: 10;
                    padding: 10px 0;
                    cursor: move;
                    user-select: none;
                    }
                .ui-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; font-weight: 500; }
                .ui-input { width: 60px; padding: 4px 8px; border-radius: 8px; border: 1px solid rgba(0,0,0,0.1); text-align: center; font-family: inherit; font-weight: 600; background: white; }
                .ui-color-input { width: 40px; height: 30px; padding: 0; border: 1px solid rgba(0,0,0,0.1); border-radius: 5px; cursor: pointer; background: none; }
                .switch { position: relative; display: inline-block; width: 44px; height: 22px; flex-shrink: 0; }
                .switch input { opacity: 0; width: 0; height: 0; }
                .slider { position: absolute; cursor: pointer; inset: 0; background-color: rgba(0, 0, 0, 0.1); transition: .4s; border-radius: 34px; }
                .slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 3px; bottom: 3px; background-color: white; transition: .4s; border-radius: 50%; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
                input:checked + .slider { background-color: #007bff; }
                input:checked + .slider:before { transform: translateX(22px); }
                .ui-btn-group { display: flex; gap: 8px; margin-top: 20px; flex-wrap: wrap; position: sticky; bottom: -24px; background: rgba(250, 250, 255, 1); padding: 15px 0 24px 0; margin-bottom: -24px; z-index: 10; border-top: 1px solid rgba(0,0,0,0.08); }
                .ui-btn { flex: 1; min-width: 80px; padding: 10px; border-radius: 10px; border: none; cursor: pointer; font-weight: 600; transition: all 0.2s; font-size: 0.85rem; }
                .ui-close-btn { background: #007bff; color: white; }
                .ui-close-btn:hover { background: #0056b3; }
                .ui-reset-btn { background: rgba(0, 0, 0, 0.05); color: #1a1b26; border: 1px solid rgba(0, 0, 0, 0.1); }
                .ui-reset-btn:hover { background: rgba(0, 0, 0, 0.1); }
                .ui-cancel-btn { background: rgba(255, 0, 0, 0.05); color: #d00; border: 1px solid rgba(255, 0, 0, 0.1); }
                .ui-cancel-btn:hover { background: rgba(255, 0, 0, 0.1); }
                .ui-input:disabled { background: #eeeeee !important; cursor: not-allowed; color: #999; }
            `,
            mobileListWrap: (mobileWidth) => `
                @media (max-width: ${mobileWidth}px) {
                    .gallery-content > div { display: flex !important; flex-direction: column !important; }
                    h1.lillie { order: 1 !important; }
                    .artist-list { order: 2 !important; margin-bottom: 5px !important; }
                    a.lillie { order: 3 !important; position: relative !important; float: none !important; margin-left: 0 !important; width: fit-content !important; }
                    .dj-content { order: 4 !important; padding-top: 10px !important; }
                    .dj-img-cont { top: auto !important; position: relative !important; }
                }
            `,
            desktopListWrap: (shiftValue) => `; position: relative !important; top: ${shiftValue}px !important;`,
            scrollTitles: `
                h1.lillie {
                    overflow-x: auto !important;
                    white-space: nowrap !important;
                    scrollbar-width: none;
                }
                h1.lillie::-webkit-scrollbar { height: 4px; }
                h1.lillie::-webkit-scrollbar-thumb { background: #888; border-radius: 4px; }
            `,
            galleryGrid: (cols, gapH, gapV) => `
                .thumbnail-list { display: flex !important; flex-wrap: wrap !important; justify-content: flex-start !important; padding: 0 !important; margin: 0 !important; }
                .thumbnail-list li { display: block; width: calc(100% / ${cols} - ${gapH}px) !important; margin: ${gapV / 2}px ${gapH / 2}px !important; height: auto !important; }
                .thumbnail-container { width: 100% !important; height: unset !important; display: block !important; }
                .thumbnail-container img { width: 100% !important; height: auto !important; max-width: none !important; max-height: none !important; display: block !important; object-fit: contain !important; }
                div.content { padding-right: 10px !important; }
            `,
            galleryScrollable: (height) => `
                ul.thumbnail-list { max-height: ${height}px !important; overflow-y: auto !important; overflow-x: hidden !important; border: 1px solid rgba(0,0,0,0.1); border-radius: 5px; }
                ul.thumbnail-list li { display: block !important; }
                .simplePagerNav { display: none !important; }
                ul.thumbnail-list::-webkit-scrollbar { width: 8px; }
                ul.thumbnail-list::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 4px; }
                ul.thumbnail-list::-webkit-scrollbar-thumb { background: #888; border-radius: 4px; }
            `,
            readerBox: `display:flex;flex-direction:column;width:100%;background:#ffffff;`,
            readerWrapper: (maxWidth, gapColor, topPadding) => `display: grid; grid-template-areas: "content"; width: 100%; max-width: ${maxWidth}; margin: 0 auto; background-color: ${gapColor}; min-height: 80vh; padding-top: ${topPadding}; align-items: center; justify-items: center; color: #888; font-family: sans-serif; font-size: 14px;`,
            readerImage: `grid-area: "content"; width: auto; max-width: 100%; height: auto; display: block; opacity: 0; transition: opacity 0.3s; z-index: 1;`,
            floatingIndicator: `position: fixed; bottom: 0.3vh; left: 50%; transform: translateX(-50%); color: rgba(255, 255, 255, 0.5); font-size: clamp(10px, 3.5vw, 20px); font-family: sans-serif; font-weight: bold; z-index: 9999; pointer-events: none; text-shadow: 0px 1px 2px rgba(0,0,0,0.5);`,
            favBtnLink: `display: block; margin-top: 5px; text-decoration: none;`,
            favBtnHeader: `text-align: center; transition: background 0.2s; margin: 0; padding: 0;`,
            favOverlay: `position: fixed; inset: 0; background: rgba(0,0,0,0.9); z-index: 1000000; overflow-y: auto; padding: 20px; display: none; font-family: sans-serif;`,
            favContent: `display: flex; flex-wrap: wrap; gap: 15px; margin-top: 60px; justify-content: center; max-width: 1200px; margin-left: auto; margin-right: auto;`,
            favCard: (customWidth) => `width: ${customWidth}px; background: #1a1a1a; padding: 0; border-radius: 4px; display: flex; flex-direction: column; border: 1px solid #333; overflow: hidden;`,
            favImg: (imgHeight) => `width: 100%; height: ${imgHeight}px; object-fit: contain; background: #000;`,
            favCardBody: `padding: 8px; text-align: left;`,
            favCardTitle: `font-size: 12px; margin: 0; height: 32px; overflow: hidden; color: #ddd; line-height: 1.3; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;`,
            favCardDate: `font-size: 10px; margin-top: 6px; color: #666; font-style: italic;`,
            favRemoveBtn: `background: #e83e8c; color: white; border: none; padding: 5px; cursor: pointer; font-weight: bold; font-size: 11px;`,
            favControls: `position: fixed; top: 20px; right: 20px; display: flex; gap: 10px; z-index: 1000001;`,
            favCloseBtn: `padding: 10px 20px; background: #333; color: white; border: none; cursor: pointer; border-radius: 5px; font-weight: bold;`,
            favActionBtn: `padding: 10px 20px; background: #007bff; color: white; border: none; cursor: pointer; border-radius: 5px; font-weight: bold; transition: background 0.2s;`,
            exportChoiceOverlay: `
                position: fixed;
                inset: 0;
                background: rgba(0,0,0,0.5);
                z-index: 1000005;
                display: flex;
                align-items: center;
                justify-content: center;
                backdrop-filter: blur(4px);
            `,
            exportChoiceBox: `
                background: #1a1b26;
                padding: 30px;
                border-radius: 20px;
                border: 1px solid rgba(255, 255, 255, 0.1);
                width: 90%;
                max-width: 360px;
                text-align: center;
                box-sizing: border-box;
                box-shadow: 0 20px 50px rgba(0,0,0,0.5);
                color: #fff;
                font-family: 'Segoe UI', sans-serif;
            `,
        },
        HTML: {
            uiSettingsTemplate: (conf) => `
                <div class="ui-title">Hitomi enhancer Settings</div>

                <div class="ui-card">
                    <div style="font-weight:700; margin-bottom:12px; color: #007bff;">Front & Search Page UI Enhance</div>
                    <div class="ui-row"><span>Show Page Number</span><label class="switch"><input type="checkbox" id="set-pagecount" ${conf.pageCount ? 'checked' : ''}><span class="slider"></span></label></div>

                    <div style="height: 1px; background: rgba(0,0,0,0.08); margin: 12px 0;"></div>

                    <div class="ui-row">
                        <span>Wrap Gallery Titles</span>
                        <label class="switch">
                            <input type="checkbox" id="set-wrap-titles" ${conf.wrapTitles ? 'checked' : ''}>
                            <span class="slider"></span>
                        </label>
                    </div>
                    <div class="ui-row" id="row-scroll-titles">
                        <span>Scroll Titles (Horizontal)</span>
                        <label class="switch">
                            <input type="checkbox" id="set-scroll-titles" ${conf.scrollTitles ? 'checked' : ''}>
                            <span class="slider"></span>
                        </label>
                    </div>
                </div>

                <div class="ui-card">
                    <div style="font-weight:700; margin-bottom:12px; color: #007bff;">Gallery Page UI Enhance</div>
                    <div class="ui-row"><span>Show Page Number</span><label class="switch"><input type="checkbox" id="set-gal-pagecount" ${conf.galPageCount ? 'checked' : ''}><span class="slider"></span></label></div>
                    <div style="height: 1px; background: rgba(0,0,0,0.08); margin: 12px 0;"></div>
                    <div class="ui-row"><span>Scrollable Thumbnails</span><label class="switch"><input type="checkbox" id="set-scrollable-gal" ${conf.scrollableGal ? 'checked' : ''}><span class="slider"></span></label></div>
                    <div class="ui-row" id="row-scroll-h"><span>Scroll Area Height (px)</span><input type="number" id="set-scroll-h" class="ui-input" value="${conf.scrollH}" min="100" max="2000"></div>

                    <div style="height: 1px; background: rgba(0,0,0,0.08); margin: 12px 0;"></div>

                    <div class="ui-row"><span>Horizontal Gap | | | (px) </span><input type="number" id="set-gap-h" class="ui-input" value="${conf.gapH}" min="0" max="100"></div>
                    <div class="ui-row"><span>Vertical Gap ☰ (px)  </span><input type="number" id="set-gap-v" class="ui-input" value="${conf.gapV}" min="0" max="100"></div>

                    <div class="ui-row" style="flex-direction: column; align-items: flex-start; gap: 8px;">
                        <div style="display: flex; justify-content: space-between; width: 100%;">
                            <span>Grid Columns (Desktop)</span>
                            <span id="grid-cols-desktop-display" style="font-weight: bold; color: #007bff;">${conf.colsDesktop}</span>
                        </div>
                        <input type="range" id="set-grid-cols-desktop" style="width: 100%; cursor: pointer;" value="${conf.colsDesktop}" min="2" max="12" step="1">
                    </div>

                    <div class="ui-row" style="flex-direction: column; align-items: flex-start; gap: 8px; margin-top: 10px;">
                        <div style="display: flex; justify-content: space-between; width: 100%;">
                            <span>Grid Columns (Mobile)</span>
                            <span id="grid-cols-mobile-display" style="font-weight: bold; color: #007bff;">${conf.colsMobile}</span>
                        </div>
                        <input type="range" id="set-grid-cols-mobile" style="width: 100%; cursor: pointer;" value="${conf.colsMobile}" min="1" max="5" step="1">
                    </div>
                </div>

                <div class="ui-card">
                    <div style="font-weight:700; margin-bottom:12px; color: #007bff;">Long-Strip Reader Mode</div>
                    <div class="ui-row"><span>Enable on Gallery Page</span><label class="switch"><input type="checkbox" id="set-gallery" ${conf.gallery ? 'checked' : ''}><span class="slider"></span></label></div>
                    <div class="ui-row"><span>Enable on Reader Page</span><label class="switch"><input type="checkbox" id="set-reader" ${conf.reader ? 'checked' : ''}><span class="slider"></span></label></div>

                    <div style="height: 1px; background: rgba(0,0,0,0.08); margin: 12px 0;"></div>

                    <div class="ui-row" id="row-floating-page"><span>Floating Page Indicator <br>(Reader page)</span><label class="switch"><input type="checkbox" id="set-floating-page" ${conf.floatingPage ? 'checked' : ''}><span class="slider"></span></label></div>

                    <div style="height: 1px; background: rgba(0,0,0,0.08); margin: 12px 0;"></div>

                    <div class="ui-row"><span>Gap Size (px)</span><input type="number" id="set-gap" class="ui-input" value="${conf.gap}" min="0" max="500"></div>
                    <div class="ui-row"><span>Gap Color</span><input type="color" id="set-gap-color" class="ui-color-input" value="${conf.color}"></div>
                </div>

                <div class="ui-card">
                    <div style="font-weight:700; margin-bottom:12px; color: #ff4b4b;">Favorite Page Settings</div>
                    <div class="ui-row" style="flex-direction: column; align-items: flex-start; gap: 8px;">
                        <div style="display: flex; justify-content: space-between; width: 100%;">
                            <span>Entry Width (px)</span>
                            <span id="fav-entry-width-display" style="font-weight: bold; color: #ff4b4b;">${conf.favWidth}</span>
                        </div>
                        <input type="range" id="set-fav-entry-width" style="width: 100%; cursor: pointer;" value="${conf.favWidth}" min="100" max="400" step="5">
                    </div>
                </div>

                <div class="ui-card">
                    <div style="font-weight:700; margin-bottom:12px; color: #666;">Developer</div>
                    <div class="ui-row"><span>Show Console Log</span><label class="switch"><input type="checkbox" id="set-devlog" ${conf.devlog ? 'checked' : ''}><span class="slider"></span></label></div>
                </div>

                <div class="ui-btn-group">
                    <button class="ui-btn ui-reset-btn">Reset</button>
                    <button class="ui-btn ui-cancel-btn">Cancel</button>
                    <button class="ui-btn ui-close-btn">Save & Close</button>
                </div>
            `,
            pageCountRow: (count, className) => `
                <tr class="${className}">
                    <td style="padding-right: 10px;">Pages</td>
                    <td style="color: #007bff; font-weight: bold;">${count}</td>
                </tr>
            `,
            favNavBtn: `<li style="padding: 10px 15px;"><a href="javascript:void(0);" id="hitomi-nav-fav-link" style="color: #ff4b4b; font-weight: bold; text-decoration: none;">favorites</a></li>`,
            favEmptyState: `<h2 style="color:#aaa;">No favorites yet.</h2>`,
            favCardInner: (fav, imgHeight) => `
                <button class="remove-fav-btn" style="${TemplateModule.CSS.favRemoveBtn}">— Remove</button>
                <a href="${fav.url}" target="_blank">
                    <img src="${fav.thumb}" style="${TemplateModule.CSS.favImg(imgHeight)}">
                </a>
                <div style="${TemplateModule.CSS.favCardBody}">
                    <p style="${TemplateModule.CSS.favCardTitle}">
                        <a href="${fav.url}" target="_blank" style="color: #ddd; text-decoration: none;">${fav.title}</a>
                    </p>
                    <p style="${TemplateModule.CSS.favCardDate}">Added: ${fav.addedAt ? new Date(fav.addedAt).toLocaleDateString() : 'Unknown'}</p>
                </div>
            `,
            exportChoiceTemplate: `
                <div style="margin-bottom: 25px;">
                    <h2 style="font-size: 1.4rem; margin: 0 0 8px 0; color: #fff; font-weight: 700; white-space: nowrap;">Export Favorites</h2>
                    <p style="color: #9499b8; font-size: 0.9rem; margin: 0;">Choose the export format</p>
                </div>

                <button id="btn-export-json" style="width:100%; padding: 14px; margin: 8px 0; background: #007bff; color: white; border: none; border-radius: 12px; font-weight: 600; cursor: pointer; transition: transform 0.1s, background 0.2s;">
                   {} Export as JSON (Full Backup)
                </button>

                <button id="btn-export-txt" style="width:100%; padding: 14px; margin: 8px 0; background: #3b3d4d; color: white; border: none; border-radius: 12px; font-weight: 600; cursor: pointer; transition: transform 0.1s, background 0.2s;">
                    📄 Export as TXT (URLs Only)
                </button>

                <p style="color: #565f89; font-size: 0.75rem; margin: 10px 0 20px 0; font-style: italic;">
                    ⚠️ TXT files contain URLs only and cannot be imported.
                </p>

                <button id="btn-export-cancel" style="width: 100%; background: rgba(255, 75, 75, 0.1); color: #ff4b4b; border: 1px solid rgba(255, 75, 75, 0.2); border-radius: 12px; padding: 12px; cursor: pointer; font-weight: 600; transition: all 0.2s;">
                    Cancel
                </button>
            `
        },
        createFavControls: function(onImport, onExport, onClose) {
            const container = document.createElement('div');
            container.style.cssText = this.CSS.favControls;

            // Hidden File Input
            const fileInput = document.createElement('input');
            fileInput.type = 'file';
            fileInput.accept = '.json';
            fileInput.style.display = 'none';
            fileInput.onchange = (e) => {
                onImport(e);
                fileInput.value = ''; // Reset input after use
            };

            // Import Button
            const importBtn = document.createElement('button');
            importBtn.innerText = '📥 Import';
            importBtn.style.cssText = this.CSS.favActionBtn;
            importBtn.onclick = () => fileInput.click();

            // Export Button
            const exportBtn = document.createElement('button');
            exportBtn.innerText = '📤 Export';
            exportBtn.style.cssText = this.CSS.favActionBtn;
            exportBtn.onclick = onExport;

            // Close Button
            const closeBtn = document.createElement('button');
            closeBtn.innerText = '✖ Close';
            closeBtn.style.cssText = this.CSS.favCloseBtn;
            closeBtn.onclick = onClose;

            // Assemble
            container.appendChild(fileInput);
            container.appendChild(importBtn);
            container.appendChild(exportBtn);
            container.appendChild(closeBtn);

            return container;
        }
    };

    // ==========================================
    // MODULE 3: Data Persistence & Logging
    // Purpose: Wraps Tampermonkey's GM_getValue and GM_setValue to handle storing user configuration,
    // and provides a centralized logging utility to optionally pipe debug output.
    // ==========================================
    const StorageModule = {
        get: (key) => {
            const defaultKey = Object.keys(CONFIG.storageKeys).find(k => CONFIG.storageKeys[k] === key);
            return GM_getValue(key, CONFIG.defaults[defaultKey]);
        },
        set: (key, val) => GM_setValue(key, val)
    };

    const Logger = {
        log: (...args) => { if (StorageModule.get(CONFIG.storageKeys.devLogEnabled)) console.log("[Hitomi enhancer Log]", ...args); },
        table: (data) => { if (StorageModule.get(CONFIG.storageKeys.devLogEnabled)) { console.log("[Hitomi enhancer Table]:"); console.table(data); } },
        error: (...args) => { if (StorageModule.get(CONFIG.storageKeys.devLogEnabled)) console.error("[Hitomi enhancer Error]", ...args); }
    };

    // ==========================================
    // MODULE 4: UI Effects Submodule
    // Purpose: Applies visual states (like disabling dependent inputs) directly in the DOM
    // without altering the user's stored settings until saved.
    // ==========================================
    const UIEffectModule = {
        els: {},
        init(modal) {
            this.els = {
                scrollToggle: modal.querySelector('#set-scrollable-gal'),
                scrollInput: modal.querySelector('#set-scroll-h'),
                scrollRow: modal.querySelector('#row-scroll-h'),
                readerToggle: modal.querySelector('#set-reader'),
                indicatorToggle: modal.querySelector('#set-floating-page'),
                indicatorRow: modal.querySelector('#row-floating-page'),
            };
        },
        applyState(input, row, isEnabled) {
            if (!input || !row) return;
            input.disabled = !isEnabled;
            row.style.opacity = isEnabled ? "1" : "0.5";
            row.style.pointerEvents = isEnabled ? "auto" : "none";
        },
        syncScrollHeight() { this.applyState(this.els.scrollInput, this.els.scrollRow, this.els.scrollToggle?.checked); },
        syncReaderOptions() { this.applyState(this.els.indicatorToggle, this.els.indicatorRow, this.els.readerToggle?.checked); }
    };

    // ==========================================
    // MODULE 5: UI Management (Global)
    // Purpose: Bootstraps the settings modal, handles drag-and-drop operations, and implements
    // the core logic to intercept user inputs for live previews and mutual exclusion of options.
    // ==========================================
    const UIModule = {
        init() {
            GM_addStyle(TemplateModule.CSS.uiStyles);
            this.createElements();
            GM_registerMenuCommand("⚙️ Hitomi Enhancer Settings", () => this.toggle(true));
        },

        createElements() {

            const modal = document.createElement('div');
            modal.id = CONFIG.ui.modalId;

            const currentConf = {
                pageCount: StorageModule.get(CONFIG.storageKeys.showPageCount),
                galPageCount: StorageModule.get(CONFIG.storageKeys.showGalleryPageCount),
                gallery: StorageModule.get(CONFIG.storageKeys.galleryEnabled),
                reader: StorageModule.get(CONFIG.storageKeys.readerEnabled),
                gap: StorageModule.get(CONFIG.storageKeys.gapSize),
                color: StorageModule.get(CONFIG.storageKeys.gapColor),
                devlog: StorageModule.get(CONFIG.storageKeys.devLogEnabled),
                colsDesktop: StorageModule.get(CONFIG.storageKeys.gridColumnsDesktop),
                colsMobile: StorageModule.get(CONFIG.storageKeys.gridColumnsMobile),
                scrollableGal: StorageModule.get(CONFIG.storageKeys.scrollableGallery),
                scrollH: StorageModule.get(CONFIG.storageKeys.scrollGalleryHeight),
                gapH: StorageModule.get(CONFIG.storageKeys.gridGapH),
                gapV: StorageModule.get(CONFIG.storageKeys.gridGapV),
                floatingPage: StorageModule.get(CONFIG.storageKeys.showFloatingPageIndicator),
                wrapTitles: StorageModule.get(CONFIG.storageKeys.wrapTitles),
                scrollTitles: StorageModule.get(CONFIG.storageKeys.scrollTitles),
                favWidth: StorageModule.get(CONFIG.storageKeys.favEntryWidth)
            };

            modal.innerHTML = TemplateModule.HTML.uiSettingsTemplate(currentConf);

            document.body.appendChild(modal);

            // Pass both modal and overlay to bindEvents
            this.bindEvents(modal);
        },

        bindEvents(modal) {

        // --- DRAGGABLE LOGIC for DESKTOP DEVICE ONLY ---
            let isDragging = false;
            let offset = { x: 0, y: 0 };

            modal.addEventListener('mousedown', (e) => {
                // Only allow dragging if the clicked element is the title
                if (!e.target.classList.contains('ui-title')) return;

                isDragging = true;
                // Calculate offset from the top-left of the modal
                const rect = modal.getBoundingClientRect();
                offset.x = e.clientX - rect.left;
                offset.y = e.clientY - rect.top;
                modal.style.transform = 'none'; // Remove the translate(-50%, -50%) once dragged
            });

            document.addEventListener('mousemove', (e) => {
                if (!isDragging) return;
                modal.style.left = (e.clientX - offset.x) + 'px';
                modal.style.top = (e.clientY - offset.y) + 'px';
            });

            document.addEventListener('mouseup', () => {
                isDragging = false;
            });

            // 1. Create a cache to hold changes before saving
            const tempSettings = {};

            const applyLiveUpdate = (key) => {
                // 2. Temporarily mock StorageModule.get so external modules read the unsaved values during the preview
                const originalGet = StorageModule.get;
                StorageModule.get = (k) => tempSettings.hasOwnProperty(k) ? tempSettings[k] : originalGet(k);

                const value = StorageModule.get(key);

                if (key === CONFIG.storageKeys.showPageCount) {
                    document.querySelectorAll('.hitomi-page-count-row').forEach(el => el.style.display = value ? '' : 'none');
                    if (value) ListPageExecution.processLinks(document.querySelectorAll(CONFIG.selectors.listTitles), window);
                }
                if (key === CONFIG.storageKeys.showGalleryPageCount) {
                    document.querySelectorAll('.hitomi-gal-page-count-row').forEach(el => el.style.display = value ? '' : 'none');
                    if (value) GalleryPageExecution.init();
                }
                if (key === CONFIG.storageKeys.wrapTitles) {
                    let styleEl = document.getElementById('hitomi-live-wrap-style');
                    if (!styleEl) {
                        styleEl = document.createElement('style');
                        styleEl.id = 'hitomi-live-wrap-style';
                        document.head.appendChild(styleEl);
                    }
                    if (value) {
                        styleEl.textContent = `${CONFIG.selectors.listTitles} { white-space: normal !important; overflow: visible !important; }`;
                        if (window.innerWidth > CONFIG.breakpoints.mobile) setTimeout(() => ListWrapLayoutDesktopModule.run(), 50);
                    } else {
                        styleEl.textContent = '';
                        document.querySelectorAll('.gallery-content > div a.lillie').forEach(el => el.style.top = '');
                    }
                }
                if (key === CONFIG.storageKeys.scrollTitles) {
                    let scrollStyle = document.getElementById('hitomi-scroll-title-style');
                    if (!scrollStyle) {
                        scrollStyle = document.createElement('style');
                        scrollStyle.id = 'hitomi-scroll-title-style';
                        document.head.appendChild(scrollStyle);
                    }
                    if (value) {
                        scrollStyle.textContent = TemplateModule.CSS.scrollTitles;
                    } else {
                        scrollStyle.textContent = '';
                    }
                }
                const gridKeys = [ CONFIG.storageKeys.gridColumnsDesktop, CONFIG.storageKeys.gridColumnsMobile, CONFIG.storageKeys.gridGapH, CONFIG.storageKeys.gridGapV, CONFIG.storageKeys.scrollableGallery, CONFIG.storageKeys.scrollGalleryHeight ];
                if (gridKeys.includes(key)) { if (document.querySelector(CONFIG.selectors.galleryParent)) GalleryGridExecution.init(); }
                if (key === CONFIG.storageKeys.gapSize || key === CONFIG.storageKeys.gapColor) {
                    const gap = StorageModule.get(CONFIG.storageKeys.gapSize);
                    const color = StorageModule.get(CONFIG.storageKeys.gapColor);
                    document.querySelectorAll('.hitomi-reader-image-wrapper').forEach(el => { el.style.paddingTop = `${gap}px`; el.style.backgroundColor = color; });
                }
                if (key === CONFIG.storageKeys.favEntryWidth) {
                    if (document.getElementById('hitomi-fav-overlay')?.style.display === 'block') { FavoriteModule.Page.renderItems(); }
                }

                // Restore the original StorageModule.get function after the visual preview finishes executing
                StorageModule.get = originalGet;
            };

            modal.addEventListener('input', (e) => {
                const target = e.target;
                const id = target.id;
                let key, value;

                if (id === 'set-grid-cols-desktop') { key = CONFIG.storageKeys.gridColumnsDesktop; document.getElementById('grid-cols-desktop-display').innerText = target.value; }
                else if (id === 'set-grid-cols-mobile') { key = CONFIG.storageKeys.gridColumnsMobile; document.getElementById('grid-cols-mobile-display').innerText = target.value; }
                else if (id === 'set-fav-entry-width') { key = CONFIG.storageKeys.favEntryWidth; document.getElementById('fav-entry-width-display').innerText = target.value; }
                else if (id === 'set-gap-h') { key = CONFIG.storageKeys.gridGapH; }
                else if (id === 'set-gap-v') { key = CONFIG.storageKeys.gridGapV; }
                else if (id === 'set-gap') { key = CONFIG.storageKeys.gapSize; }
                else if (id === 'set-gap-color') { key = CONFIG.storageKeys.gapColor; }
                else if (id === 'set-scroll-h') { key = CONFIG.storageKeys.scrollGalleryHeight; }
                else if (target.type === 'checkbox') {
                    if (id === 'set-pagecount') { key = CONFIG.storageKeys.showPageCount; }
                    if (id === 'set-gal-pagecount') { key = CONFIG.storageKeys.showGalleryPageCount; }
                    if (id === 'set-gallery') { key = CONFIG.storageKeys.galleryEnabled; }
                    if (id === 'set-reader') { key = CONFIG.storageKeys.readerEnabled; UIEffectModule.syncReaderOptions(); }
                    if (id === 'set-floating-page') { key = CONFIG.storageKeys.showFloatingPageIndicator; }
                    if (id === 'set-devlog') { key = CONFIG.storageKeys.devLogEnabled; }
                    if (id === 'set-scroll-titles') {
                        key = CONFIG.storageKeys.scrollTitles;
                        if (target.checked) {
                            const wrapToggle = document.getElementById('set-wrap-titles');
                            if (wrapToggle && wrapToggle.checked) {
                                wrapToggle.checked = false;
                                tempSettings[CONFIG.storageKeys.wrapTitles] = false;
                                applyLiveUpdate(CONFIG.storageKeys.wrapTitles);
                            }
                        }
                    }
                    if (id === 'set-wrap-titles') {
                        key = CONFIG.storageKeys.wrapTitles;
                        if (target.checked) {
                            const scrollToggle = document.getElementById('set-scroll-titles');
                            if (scrollToggle && scrollToggle.checked) {
                                scrollToggle.checked = false;
                                tempSettings[CONFIG.storageKeys.scrollTitles] = false;
                                applyLiveUpdate(CONFIG.storageKeys.scrollTitles);
                            }
                        }
                    }
                    if (id === 'set-scrollable-gal') { key = CONFIG.storageKeys.scrollableGallery; UIEffectModule.syncScrollHeight(); }
                }

                if (key) {
                    value = target.type === 'checkbox' ? target.checked : (target.type === 'number' || target.type === 'range' ? parseInt(target.value) : target.value);

                    // 3. Store in the temporary cache instead of writing to storage immediately
                    tempSettings[key] = value;
                    applyLiveUpdate(key);
                }
            });

            UIEffectModule.init(modal);
            UIEffectModule.syncScrollHeight();
            UIEffectModule.syncReaderOptions();

            // 4. Save & Close logic: Commit tempSettings to GM_setValue
            modal.querySelector('.ui-close-btn').onclick = () => {
                // Save all temporary changes to permanent storage
                Object.keys(tempSettings).forEach(k => StorageModule.set(k, tempSettings[k]));

                // Refresh the page to apply all changes cleanly
                location.reload();
            };

            // 5. Cancel logic: If changes were made, reload the page to safely discard visual changes
            const closeWithoutSaving = () => {
                if (Object.keys(tempSettings).length > 0) {
                    location.reload(); // Refresh if changes were discarded
                } else {
                    this.toggle(false); // Just close if nothing changed
                }
            };

            modal.querySelector('.ui-cancel-btn').onclick = closeWithoutSaving;
            modal.querySelector('.ui-reset-btn').onclick = () => this.reset();
        },
        reset() {
            if (confirm("Reset all settings to default?")) {
                Object.keys(CONFIG.storageKeys).forEach(k => { StorageModule.set(CONFIG.storageKeys[k], CONFIG.defaults[k]); });
                location.reload();
            }
        },
        toggle(show) {
            const modal = document.getElementById(CONFIG.ui.modalId);
            if (modal) modal.style.display = show ? 'block' : 'none';
        }
    };

    // ==========================================
    // MODULE 6: Feature - Long-Strip Reader
    // Purpose: Connects to Hitomi's internal JS files metadata to intercept and layout
    // images into a seamless vertical long-strip scrolling experience.
    // ==========================================
    const ReaderExtraction = {
        async getImages() {
            const win = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
            const match = window.location.pathname.match(/-?([0-9]+)(?:\.html)?$/);
            if (!match || !match[1]) return null;
            const galleryid = parseInt(match[1]);

            for (let i = 0; i < 20; i++) {
                if (win.galleryinfo && typeof win.url_from_url_from_hash === 'function') break;
                await new Promise(r => setTimeout(r, 500));
            }

            if (!win.galleryinfo) return null;

            return win.galleryinfo.files.map((image, index) => {
                try {
                    let url;
                    if (image.hasavif) url = win.url_from_url_from_hash(galleryid, image, 'avif', 'avif');
                    else if (image.haswebp) url = win.url_from_url_from_hash(galleryid, image, 'webp', 'webp');
                    else {
                        const ext = image.name.split('.').pop();
                        url = win.url_from_url_from_hash(galleryid, image, 'images', ext);
                    }
                    return { url, index };
                } catch(e) { return null; }
            }).filter(item => item !== null);
        }
    };

    const ReaderExecution = {
        async run() {
            const data = await ReaderExtraction.getImages();
            if (!data || data.length === 0) return;

            const hash = window.location.hash.match(/#([0-9]+)/);
            const startIndex = hash ? parseInt(hash[1]) - 1 : 0;
            const gap = StorageModule.get(CONFIG.storageKeys.gapSize);
            const gapColor = StorageModule.get(CONFIG.storageKeys.gapColor);
            const isMobileViewport = window.innerWidth <= CONFIG.breakpoints.mobile;
            let readerTarget = isMobileViewport ? document.querySelector(CONFIG.selectors.mobileReader) : document.querySelector(CONFIG.selectors.desktopReader);

            if (readerTarget) {
                readerTarget.innerHTML = '';
                this.appendImages(readerTarget, data, '100%', startIndex, gap, gapColor);
                window.scrollTo(0, 0);
            } else {
                const galleryTarget = document.querySelector(CONFIG.selectors.galleryParent);
                if (galleryTarget) {
                    const thumbs = galleryTarget.querySelector(CONFIG.selectors.galleryThumbs);
                    if (thumbs) thumbs.style.display = 'none';
                    document.querySelectorAll(CONFIG.selectors.pagination).forEach(el => el.style.display = 'none');

                    const webtoonBox = document.createElement('div');
                    webtoonBox.style.cssText = TemplateModule.CSS.readerBox;
                    galleryTarget.appendChild(webtoonBox);
                    galleryTarget.style.padding = '0';
                    this.appendImages(webtoonBox, data, '100%', 0, gap, gapColor);
                }
            }

            if (StorageModule.get(CONFIG.storageKeys.showFloatingPageIndicator)) {
                this.createFloatingIndicator(data.length);
            }
        },

        appendImages(container, data, maxWidth, startIndex, gap, gapColor) {
            data.forEach(item => {
                if (item.index < startIndex) return;
                const wrapper = document.createElement('div');
                const topPadding = `${gap}px`;

                wrapper.className = 'hitomi-reader-image-wrapper';
                wrapper.dataset.pageIndex = item.index + 1;
                wrapper.style.cssText = TemplateModule.CSS.readerWrapper(maxWidth, gapColor, topPadding);

                const loadingText = document.createElement('span');
                loadingText.innerText = `Loading Image ${item.index + 1}...`;
                loadingText.style.gridArea = "content";
                wrapper.appendChild(loadingText);

                const img = document.createElement('img');
                img.src = item.url;
                img.style.cssText = TemplateModule.CSS.readerImage;
                img.loading = 'lazy';

                img.onload = () => {
                    img.style.opacity = '1';
                    wrapper.style.minHeight = 'auto';
                    loadingText.style.display = 'none';
                };

                wrapper.appendChild(img);
                container.appendChild(wrapper);
            });
        },

        createFloatingIndicator(total) {
            const existing = document.getElementById('hitomi-floating-page-indicator');
            if (existing) existing.remove();

            const indicator = document.createElement('div');
            indicator.id = 'hitomi-floating-page-indicator';
            indicator.style.cssText = TemplateModule.CSS.floatingIndicator;
            indicator.innerText = `1 / ${total}`;
            document.body.appendChild(indicator);

            this.setupScrollObserver(total, indicator);
        },

        setupScrollObserver(total, indicator) {
            const observer = new IntersectionObserver((entries) => {
                entries.forEach(entry => {
                    if (entry.isIntersecting) {
                        indicator.innerText = `${entry.target.dataset.pageIndex} / ${total}`;
                    }
                });
            }, { rootMargin: "-49% 0px -49% 0px" });

            document.querySelectorAll('.hitomi-reader-image-wrapper').forEach(wrapper => observer.observe(wrapper));
        }
    };

    // ==========================================
    // MODULE 7: Feature - List Page Enhancements
    // Purpose: Modifies the layout on category/search lists, fetching gallery page lengths
    // via remote metadata files, and styles the layout blocks to support text wrapping/scrolling.
    // ==========================================
    const ListTitleWrapModule = {
        init() {
            const wrapEnabled = StorageModule.get(CONFIG.storageKeys.wrapTitles);
            const scrollEnabled = StorageModule.get(CONFIG.storageKeys.scrollTitles);

            if (wrapEnabled) {
                let styleEl = document.getElementById('hitomi-live-wrap-style');
                if (!styleEl) {
                    styleEl = document.createElement('style');
                    styleEl.id = 'hitomi-live-wrap-style';
                    styleEl.textContent = `${CONFIG.selectors.listTitles} { white-space: normal !important; overflow: visible !important; }`;
                    document.head.appendChild(styleEl);
                }
                if (window.innerWidth > CONFIG.breakpoints.mobile) {
                    setTimeout(() => ListWrapLayoutDesktopModule.run(), 50);
                } else {
                    ListWrapLayoutMobileModule.init();
                }
            } else if (scrollEnabled) {
                // Inject the scroll styling if enabled
                let scrollStyle = document.getElementById('hitomi-scroll-title-style');
                if (!scrollStyle) {
                    scrollStyle = document.createElement('style');
                    scrollStyle.id = 'hitomi-scroll-title-style';
                    scrollStyle.textContent = TemplateModule.CSS.scrollTitles;
                    document.head.appendChild(scrollStyle);
                }
            }
        }
    };

    const ListWrapLayoutMobileModule = {
        init() {
            GM_addStyle(TemplateModule.CSS.mobileListWrap(CONFIG.breakpoints.mobile));
            Logger.log("Mobile layout fix applied for wrapped titles.");
        }
    };

    const ListWrapLayoutDesktopModule = {
        run() {
            document.querySelectorAll('.gallery-content > div').forEach(entry => {
                const titleBar = entry.querySelector('h1.lillie');
                const coverLink = entry.querySelector('a.lillie');
                if (titleBar && coverLink) {
                    coverLink.style.cssText += TemplateModule.CSS.desktopListWrap(titleBar.offsetHeight / 3);
                }
            });
            Logger.log("Desktop layout fix applied for wrapped titles.");
        }
    };

    const ListExtraction = {
        async getPageCount(galleryId, metaDomain) {
            const url = `//${metaDomain}/galleries/${galleryId}.js`;
            Logger.log(`Fetching metadata from: ${url}`);
            try {
                const response = await fetch(url);
                if (!response.ok) throw new Error(`HTTP ${response.status}`);
                const text = await response.text();
                const extractFunc = new Function(`
                    let galleryinfo;
                    ${text.replace(/var\s+galleryinfo\s*=/, 'galleryinfo =')}
                    return galleryinfo ? galleryinfo.files.length : 0;
                `);
                return extractFunc();
            } catch (e) {
                Logger.error(`Failed for ${galleryId}: ${e.message}`);
                return 0;
            }
        }
    };

    const ListInjection = {
        injectPageCount(link, filesLength) {
            const injectCfg = CONFIG.selectors.pageNumberInjection;
            const galleryItem = link.closest('.gallery-content > div');
            if (!galleryItem) return;

            const container = galleryItem.querySelector(injectCfg.container);
            if (container && !container.querySelector('.hitomi-page-count-row')) {
                const finalHtml = TemplateModule.HTML.pageCountRow(filesLength, 'hitomi-page-count-row');
                const sibling = container.querySelector(injectCfg.placeAfter);
                if (sibling) sibling.insertAdjacentHTML('afterend', finalHtml);
                else container.insertAdjacentHTML('beforeend', finalHtml);
            }
        }
    };

    const ListPageExecution = {
        init() {
            ListTitleWrapModule.init();
            if (!StorageModule.get(CONFIG.storageKeys.showPageCount)) return;
            this.hijackListRender();
        },
        hijackListRender() {
            const win = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
            let originalLimitLists = win.limitLists;
            const runPipeline = () => {
                const links = document.querySelectorAll(CONFIG.selectors.listTitles);
                if (links && links.length > 0) this.processLinks(links, win);
            };

            if (win.limitLists) {
                win.limitLists = function(...args) {
                    if (originalLimitLists) originalLimitLists.apply(this, args);
                    runPipeline();
                };
            } else {
                Object.defineProperty(win, "limitLists", {
                    get: () => function(...args) { runPipeline(); },
                    set(val) { originalLimitLists = val; }
                });
            }
            setTimeout(runPipeline, 500);
        },
        processLinks(links, win) {
            const metaDomain = win.domain || 'ltn.hitomi.la';
            links.forEach(async (link) => {
                if (link.dataset.pageCountAdded) return;
                link.dataset.pageCountAdded = "true";
                const match = link.href.match(/-([0-9]+)\.html/);
                if (!match) return;

                const filesLength = await ListExtraction.getPageCount(match[1], metaDomain);
                if (filesLength > 0) ListInjection.injectPageCount(link, filesLength);
                else link.dataset.pageCountAdded = "false";
            });
        }
    };

    // ==========================================
    // MODULE 8: Feature - Gallery Page Enhancements
    // Purpose: Injects total page counts into the detail tables of single gallery items
    // and modifies the default thumbnail layout to act as a customized flex grid box.
    // ==========================================
    const GalleryPageExecution = {
        async init() {
            if (document.querySelector('.hitomi-gal-page-count-row')) {
                document.querySelector('.hitomi-gal-page-count-row').style.display = '';
                return;
            }
            if (!StorageModule.get(CONFIG.storageKeys.showGalleryPageCount)) return;
            const win = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;

            for (let i = 0; i < 20; i++) {
                if (win.galleryinfo && win.galleryinfo.files) break;
                await new Promise(r => setTimeout(r, 500));
            }

            if (!win.galleryinfo || !win.galleryinfo.files) {
                Logger.error("Could not find galleryinfo on this page.");
                return;
            }
            this.injectPageCount(win.galleryinfo.files.length);
        },
        injectPageCount(count) {
            const injectCfg = CONFIG.selectors.galleryPageInjection;
            const container = document.querySelector(injectCfg.container);
            if (container && !document.querySelector('.hitomi-gal-page-count-row')) {
                const finalHtml = TemplateModule.HTML.pageCountRow(count, 'hitomi-gal-page-count-row');
                const sibling = container.querySelector(injectCfg.placeAfter);
                if (sibling) sibling.insertAdjacentHTML('afterend', finalHtml);
                else container.insertAdjacentHTML('beforeend', finalHtml);
            }
        }
    };

    const GalleryGridExecution = {
        init() {
            const isMobile = window.innerWidth <= CONFIG.breakpoints.mobile;
            const cols = isMobile ? StorageModule.get(CONFIG.storageKeys.gridColumnsMobile) : StorageModule.get(CONFIG.storageKeys.gridColumnsDesktop);
            const gapH = StorageModule.get(CONFIG.storageKeys.gridGapH);
            const gapV = StorageModule.get(CONFIG.storageKeys.gridGapV);

            this.applyStyles(cols, gapH, gapV);
            if (StorageModule.get(CONFIG.storageKeys.scrollableGallery)) {
                this.applyScrollableStyles(StorageModule.get(CONFIG.storageKeys.scrollGalleryHeight));
            }
        },
        applyStyles(cols, gapH, gapV) { GM_addStyle(TemplateModule.CSS.galleryGrid(cols, gapH, gapV)); },
        applyScrollableStyles(height) { GM_addStyle(TemplateModule.CSS.galleryScrollable(height)); }
    };

    // ==========================================
    // MODULE 9: Feature - Favorite System
    // Purpose: Entire lifecycle implementation of saving, backing up, importing and exploring
    // user's saved galleries locally by managing a simulated SPA modal view overlay.
    // ==========================================
    const FavoriteModule = {
        Storage: {
            get() { try { return JSON.parse(GM_getValue(CONFIG.storageKeys.favorites, "[]")); } catch(e) { return []; } },
            save(array) { GM_setValue(CONFIG.storageKeys.favorites, JSON.stringify(array)); Logger.table(array); },
            add(mangaObj) {
                const list = this.get();
                if (!list.find(m => m.id === mangaObj.id)) { list.unshift(mangaObj); this.save(list); }
            },
            remove(id) { this.save(this.get().filter(m => m.id !== id)); },
            isFavorited(id) { return this.get().some(m => m.id === id); },
        },
        UI: {
            injectGalleryButton() {
                const match = window.location.pathname.match(/-?([0-9]+)(?:\.html)?$/);
                if (!match) return;
                const currentId = match[1];

                const container = document.querySelector('.cover-column.lillie');
                const dlButton = document.getElementById('dl-button');
                if (!container || !dlButton || document.getElementById('hitomi-fav-btn')) return;

                const titleNode = document.querySelector(CONFIG.selectors.galleryHeader + ' a');
                const title = titleNode ? titleNode.innerText : document.title;
                let coverNode = document.querySelector(CONFIG.selectors.coverImage);
                let thumb = coverNode ? coverNode.src : '';

                if (thumb && thumb.startsWith('//')) thumb = window.location.protocol + thumb;

                const btn = document.createElement('a');
                btn.id = 'hitomi-fav-btn';
                btn.href = 'javascript:void(0);';
                btn.style.cssText = TemplateModule.CSS.favBtnLink;
                btn.innerHTML = `<h1 style="${TemplateModule.CSS.favBtnHeader}"></h1>`;

                const btnHeader = btn.querySelector('h1');
                const updateButtonState = () => {
                    if (FavoriteModule.Storage.isFavorited(currentId)) {
                        btnHeader.innerText = "♡ Unfavourite";
                        btnHeader.style.background = "#ff4b4b";
                        btnHeader.style.color = "white";
                    } else {
                        btnHeader.innerText = "♥︎ Add to Favorite";
                        btnHeader.style.background = "#5cb85c";
                        btnHeader.style.color = "white";
                    }
                };

                btn.onclick = (e) => {
                    e.preventDefault();
                    if (FavoriteModule.Storage.isFavorited(currentId)) FavoriteModule.Storage.remove(currentId);
                    else FavoriteModule.Storage.add({ id: currentId, title: title, url: window.location.href, thumb: thumb, addedAt: Date.now() });
                    updateButtonState();
                };

                updateButtonState();
                dlButton.after(btn);
            }
        },
        Page: {
            injectNavButton() {
                if (document.getElementById('hitomi-nav-fav-link')) return;
                const navUl = document.querySelector('.navbar nav ul');
                if (navUl) {
                    const li = document.createElement('li');
                    li.innerHTML = TemplateModule.HTML.favNavBtn;
                    navUl.appendChild(li);
                    li.addEventListener('click', (e) => { e.preventDefault(); this.toggleModal(true); });
                }
            },

            // NEW: Added missing downloadFile helper
            downloadFile(blob, filename) {
                const link = document.createElement('a');
                link.href = URL.createObjectURL(blob);
                link.download = filename;
                document.body.appendChild(link);
                link.click();
                document.body.removeChild(link);
                URL.revokeObjectURL(link.href);
            },

            toggleModal(show, fromHistory = false) {
                let overlay = document.getElementById('hitomi-fav-overlay');

                if (show) {
                    if (!overlay) {
                        overlay = document.createElement('div');
                        overlay.id = 'hitomi-fav-overlay';
                        overlay.style.cssText = TemplateModule.CSS.favOverlay;

                        // FIXED: Implemented handleImport logic
                        const handleImport = (e) => {
                            const file = e.target.files[0];
                            if (!file) return;

                            const reader = new FileReader();
                            reader.onload = (event) => {
                                try {
                                    const importedFavs = JSON.parse(event.target.result);
                                    if (!Array.isArray(importedFavs)) throw new Error("Not an array");

                                    const currentFavs = FavoriteModule.Storage.get();
                                    const existingIds = new Set(currentFavs.map(f => f.id));
                                    let addedCount = 0;

                                    importedFavs.forEach(fav => {
                                        if (fav.id && !existingIds.has(fav.id)) {
                                            currentFavs.push(fav);
                                            addedCount++;
                                        }
                                    });

                                    FavoriteModule.Storage.save(currentFavs);
                                    this.renderItems();
                                    alert(`Successfully imported ${addedCount} new favorites!`);
                                } catch (err) {
                                    alert("Error importing file. Please ensure it is a valid JSON backup.");
                                }
                            };
                            reader.readAsText(file);
                        };

                        const handleExport = () => {
                            const favs = FavoriteModule.Storage.get();
                            if (favs.length === 0) return alert('Nothing to export!');

                            const choiceOverlay = document.createElement('div');
                            choiceOverlay.id = 'hitomi-export-choice';
                            choiceOverlay.style.cssText = TemplateModule.CSS.exportChoiceOverlay;
                            choiceOverlay.innerHTML = `<div style="${TemplateModule.CSS.exportChoiceBox}">${TemplateModule.HTML.exportChoiceTemplate}</div>`;

                            overlay.appendChild(choiceOverlay);

                            const closeChoice = () => choiceOverlay.remove();

                            choiceOverlay.querySelector('#btn-export-json').onclick = () => {
                                const blob = new Blob([JSON.stringify(favs, null, 2)], { type: 'application/json' });
                                this.downloadFile(blob, `hitomi_favs_${new Date().toISOString().slice(0, 10)}.json`);
                                closeChoice();
                            };

                            choiceOverlay.querySelector('#btn-export-txt').onclick = () => {
                                const urlList = favs.map(i => i.url || `https://hitomi.la/galleries/${i.id}.html`).join('\n');
                                const blob = new Blob([urlList], { type: 'text/plain' });
                                this.downloadFile(blob, `hitomi_favs_${new Date().toISOString().slice(0, 10)}.txt`);
                                closeChoice();
                            };

                            choiceOverlay.querySelector('#btn-export-cancel').onclick = closeChoice;
                        };

                        const handleClose = () => {
                            window.location.hash === '#favorites' ? window.history.back() : this.toggleModal(false);
                        };

                        const controlsContainer = TemplateModule.createFavControls(handleImport, handleExport, handleClose);
                        const content = document.createElement('div');
                        content.id = 'hitomi-fav-content';
                        content.style.cssText = TemplateModule.CSS.favContent;

                        overlay.appendChild(controlsContainer);
                        overlay.appendChild(content);
                        document.body.appendChild(overlay);
                    }

                    overlay.style.display = 'block';
                    document.body.style.overflow = 'hidden';

                    if (!fromHistory) {
                        history.pushState(null, '', '#favorites');
                        // FIXED: Removed the recursive call `this.Page.toggleModal(true)` here
                    }
                    this.renderItems();
                } else {
                    if (overlay) {
                        overlay.style.display = 'none';
                        const choice = document.getElementById('hitomi-export-choice');
                        if (choice) choice.remove();
                    }
                    document.body.style.overflow = 'auto';
                    if (!fromHistory && window.location.hash === '#favorites') window.history.back();
                }
            },
            renderItems() {
                const content = document.getElementById('hitomi-fav-content');
                content.innerHTML = '';
                const favs = FavoriteModule.Storage.get();
                const customWidth = StorageModule.get(CONFIG.storageKeys.favEntryWidth);
                const imgHeight = Math.round(customWidth * 1.39);

                if (favs.length === 0) {
                    content.innerHTML = TemplateModule.HTML.favEmptyState;
                    return;
                }

                favs.forEach(fav => {
                    const card = document.createElement('div');
                    card.style.cssText = TemplateModule.CSS.favCard(customWidth);
                    card.innerHTML = TemplateModule.HTML.favCardInner(fav, imgHeight);

                    card.querySelector('.remove-fav-btn').onclick = () => {
                        FavoriteModule.Storage.remove(fav.id);
                        card.remove();
                        if (content.children.length === 0) content.innerHTML = TemplateModule.HTML.favEmptyState;
                    };
                    content.appendChild(card);
                });
            }
        },
        init() {
            this.Page.injectNavButton();
            window.addEventListener('popstate', () => this.Page.toggleModal(window.location.hash === '#favorites', true));
        }
    };

    // ==========================================
    // INITIALIZATION PIPELINE
    // Purpose: Sets up DOM MutationObserver to detect current page context
    // and fire off respective initialization scripts.
    // ==========================================

    // 1. Add the debounce helper function
    const debounce = (func, delay) => {
        let timeout;
        return (...args) => {
            clearTimeout(timeout);
            timeout = setTimeout(() => func.apply(this, args), delay);
        };
    };

    function runDetectionPipeline(obs) {
        const isReader = window.location.pathname.includes('/reader/');
        const isGallery = document.querySelector(CONFIG.selectors.galleryParent) !== null;
        const isListPage = document.querySelector(CONFIG.selectors.listTitles) !== null;

        if (isReader && StorageModule.get(CONFIG.storageKeys.readerEnabled)) {
            ReaderExecution.run();
        } else if (isGallery) {
            GalleryPageExecution.init();
            GalleryGridExecution.init();
            FavoriteModule.UI.injectGalleryButton();
            if (StorageModule.get(CONFIG.storageKeys.galleryEnabled)) ReaderExecution.run();
        } else if (isListPage || window.location.pathname === '/') {
            ListPageExecution.init();
        }

        // This logic stops the observer once the content is found
        const shouldStop = isReader || isGallery || isListPage;
        if (shouldStop && obs) {
            obs.disconnect();
            Logger.log("Target found. Observer offline.");
        }
    }

    UIModule.init();
    FavoriteModule.init();

    // 2. Wrap the pipeline in a 200ms debounce
    const debouncedPipeline = debounce((mutations, obs) => {
        runDetectionPipeline(obs);
    }, 200);

    // 3. Initialize the observer with the debounced function
    const observer = new MutationObserver(debouncedPipeline);
    observer.observe(document.body, { childList: true, subtree: true });

    // 4. Run once immediately on load to check if the content is already there
    runDetectionPipeline(observer);

})();