Hitomi Enhancer

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

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Hitomi Enhancer
// @namespace    https://sleazyfork.org/zh-TW/scripts/576445-hitomi-enhancer
// @version      8.8.2
// @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',
            listThumbLink: '.gallery-content > div > a.lillie',
            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',
            readerFitMode: 'hitomi_reader_fit_mode',
            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',
            removeTitleLink: 'hitomi_remove_title_link',
            favorites: 'hitomi_favorites',
            favEntryWidth: 'hitomi_fav_width',
            favBgColor: 'hitomi_fav_bg_color'
        },
        defaults: {
            showPageCount: true,
            showGalleryPageCount: true,
            galleryEnabled: false,
            readerEnabled: true,
            readerFitMode: 'smart', // Options: 'width', 'height', 'smart'
            gapSize: 15,
            gapColor: '#000000',
            devLogEnabled: false,
            gridColumnsDesktop: 7,
            gridColumnsMobile: 2,
            scrollableGallery: true,
            gridGapV: 10,
            gridGapH: 10,
            scrollGalleryHeight: 700,
            showFloatingPageIndicator: true,
            wrapTitles: false,
            scrollTitles: true,
            removeTitleLink: true,
            favEntryWidth: 170,
            favBgColor: '#000000'
        },
        ui: {
            modalId: 'hitomi-ui-modal'
        },
        // NEW: Centralized UI Rules mapping.
        // We now represent Parent-Child dependencies AND Peer-to-Peer Exclusivity here.
        uiMap: {
            greyoutRules: [
                // Standard Parent-Child rules (If false, disable child)
                {
                    parent: 'set-scrollable-gal',
                    triggerValue: false,
                    children: ['set-scroll-h'],
                    rows: ['row-scroll-h']
                },
                {
                    parent: 'set-reader',
                    triggerValue: false,
                    children: ['set-floating-page'],
                    rows: ['row-floating-page']
                },
                // NEW: Mutual Exclusive rules (If TRUE, disable the opposing peer)
                {
                    parent: 'set-wrap-titles',
                    triggerValue: true, // If Wrap is ON...
                    children: ['set-scroll-titles'], // Grey out Scroll
                    rows: ['row-scroll-titles']
                },
                {
                    parent: 'set-scroll-titles',
                    triggerValue: true, // If Scroll is ON...
                    children: ['set-wrap-titles'], // Grey out Wrap
                    rows: ['row-wrap-titles']
                }
            ]
        }
    };

    // ==========================================
    // MODULE 2: Template Helper Module
    // Purpose: Centralized HTML structures and dynamically generated CSS.
    // ==========================================
    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: `; position: absolute !important; top: 50% !important; transform: translateY(-50%) !important;`,
            scrollTitles: `
                h1.lillie > a {
                    overflow-x: auto !important;
                    white-space: nowrap !important;
                    display: flex !important;
                    scrollbar-width: none !important;
                }
                h1.lillie > a::-webkit-scrollbar { display: none; } /* Ensures scrollbar is hidden in Chrome/Safari too */
            `,
            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: 0vh; left: 50%; transform: translateX(-50%); color: rgba(224, 224, 224, 0.9); -webkit-text-stroke: 0.7px black; font-size: clamp(10px, 3.5vw, 20px); font-family: sans-serif; font-weight: 800; z-index: 9999; pointer-events: none;`,
            favBtnLink: `display: block; margin-top: 5px; text-decoration: none;`,
            favBtnHeader: `text-align: center; transition: background 0.2s; margin: 0; padding: 0;`,
            favOverlay: (bgColor) => `position: fixed; inset: 0; background: ${bgColor}; 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;`,
            searchBarshrink: `width: 220px !important;`
        },
        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" id="row-wrap-titles">
                        <span>Wrap Gallery Title</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>Scrollable Gallery Title <br> (Horizontal) </span>
                        <label class="switch">
                            <input type="checkbox" id="set-scroll-titles" ${conf.scrollTitles ? 'checked' : ''}>
                            <span class="slider"></span>
                        </label>
                    </div>
                    <div class="ui-row">
                    <span>Remove Gallery Title Hyperlink</span>
                        <label class="switch">
                            <input type="checkbox" id="set-remove-title-link" ${conf.removeTitleLink ? '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>| | | Gap size  (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>☰ Gap size  (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">
                        <span>Image Fit Mode</span>
                        <select id="set-reader-fit" class="ui-input" style="width: 100px; cursor: pointer;">
                            <option value="width" ${conf.readerFit === 'width' ? 'selected' : ''}>Fit Width</option>
                            <option value="height" ${conf.readerFit === 'height' ? 'selected' : ''}>Fit Height</option>
                            <option value="smart" ${conf.readerFit === 'smart' ? 'selected' : ''}>Smart Fit</option>
                        </select>
                    </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>Show Current Page Number</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: #ff1493;">Favorite Page</div>
                    <div class="ui-row">
                        <span>Background Color</span>
                        <input type="color" id="set-fav-bg-color" class="ui-color-input" value="${conf.favBgColor}">
                    </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:#ff1493;">${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: #ff1493; 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;
            const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = '.json'; fileInput.style.display = 'none'; fileInput.onchange = (e) => { onImport(e); fileInput.value = ''; };
            const importBtn = document.createElement('button'); importBtn.innerText = '📥 Import'; importBtn.style.cssText = this.CSS.favActionBtn; importBtn.onclick = () => fileInput.click();
            const exportBtn = document.createElement('button'); exportBtn.innerText = '📤 Export'; exportBtn.style.cssText = this.CSS.favActionBtn; exportBtn.onclick = onExport;
            const closeBtn = document.createElement('button'); closeBtn.innerText = '✖ Close'; closeBtn.style.cssText = this.CSS.favCloseBtn; closeBtn.onclick = onClose;
            container.appendChild(fileInput); container.appendChild(importBtn); container.appendChild(exportBtn); container.appendChild(closeBtn);
            return container;
        }
    };

    // ==========================================
    // MODULE 3: Data Persistence & Logging
    // ==========================================
    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: Scans uiMap.greyoutRules and visually disables dependent/conflicting elements.
    // ==========================================
    const UIEffectModule = {
        modal: null,
        init(modal) {
            this.modal = modal;
            this.syncGreyoutRules();
        },
        syncGreyoutRules() {
            if (!CONFIG.uiMap || !CONFIG.uiMap.greyoutRules) return;

            CONFIG.uiMap.greyoutRules.forEach(rule => {
                const parent = this.modal.querySelector(`#${rule.parent}`);
                if (!parent) return;

                const currentVal = parent.type === 'checkbox' ? parent.checked : parent.value;
                const shouldGreyOut = (currentVal === rule.triggerValue);

                rule.children.forEach((childId, index) => {
                    const childInput = this.modal.querySelector(`#${childId}`);
                    const childRow = this.modal.querySelector(`#${rule.rows[index]}`);

                    if (childInput && childRow) {
                        childInput.disabled = shouldGreyOut;
                        childRow.style.opacity = shouldGreyOut ? "0.5" : "1";
                        childRow.style.pointerEvents = shouldGreyOut ? "none" : "auto";
                    }
                });
            });
        }
    };

    // ==========================================
    // MODULE 5: Live Update Dispatcher (The Switchboard)
    // Purpose: Routes UI updates to modules. Notice that Wrap and Scroll both now
    // route to a unified updateLive() handler which respects the global state.
    // ==========================================
    const LiveUpdateDispatcher = {
        dispatch(key, value) {
            const updateRegistry = {
                [CONFIG.storageKeys.showPageCount]: () => ListPageExecution.updateLive(value),
                [CONFIG.storageKeys.showGalleryPageCount]: () => GalleryPageExecution.updateLive(value),
                [CONFIG.storageKeys.wrapTitles]: () => ListTitleWrapModule.updateLive(), // UPDATED
                [CONFIG.storageKeys.scrollTitles]: () => ListTitleWrapModule.updateLive(), // UPDATED
                [CONFIG.storageKeys.removeTitleLink]: () => ListTitleWrapModule.toggleTitleLinks(),
                [CONFIG.storageKeys.gridColumnsDesktop]: () => GalleryGridExecution.updateLive(),
                [CONFIG.storageKeys.gridColumnsMobile]: () => GalleryGridExecution.updateLive(),
                [CONFIG.storageKeys.gridGapH]: () => GalleryGridExecution.updateLive(),
                [CONFIG.storageKeys.gridGapV]: () => GalleryGridExecution.updateLive(),
                [CONFIG.storageKeys.scrollableGallery]: () => GalleryGridExecution.updateLive(),
                [CONFIG.storageKeys.scrollGalleryHeight]: () => GalleryGridExecution.updateLive(),
                [CONFIG.storageKeys.gapSize]: () => ReaderExecution.updateLiveGap(),
                [CONFIG.storageKeys.gapColor]: () => ReaderExecution.updateLiveGap(),
                [CONFIG.storageKeys.readerFitMode]: () => ReaderExecution.updateLiveFitMode(),
                [CONFIG.storageKeys.showFloatingPageIndicator]: () => ReaderExecution.updateLiveIndicator(value),
                [CONFIG.storageKeys.favEntryWidth]: () => FavoriteModule.Page.updateLiveWidth(),
                [CONFIG.storageKeys.favBgColor]: () => FavoriteModule.Page.updateLiveBg(value),
            };

            if (updateRegistry[key]) updateRegistry[key]();
        }
    };

    // ==========================================
    // MODULE 6: UI Management (Global)
    // Purpose: Captures user input and dynamically resolves conflicts BEFORE routing.
    // ==========================================
    const UIModule = {
        init() {
            GM_addStyle(TemplateModule.CSS.uiStyles);
            this.createElements();
            GM_registerMenuCommand("Configuration", () => 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),
                readerFit: StorageModule.get(CONFIG.storageKeys.readerFitMode),
                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),
                removeTitleLink: StorageModule.get(CONFIG.storageKeys.removeTitleLink),
                favWidth: StorageModule.get(CONFIG.storageKeys.favEntryWidth),
                favBgColor: StorageModule.get(CONFIG.storageKeys.favBgColor)
            };

            modal.innerHTML = TemplateModule.HTML.uiSettingsTemplate(currentConf);
            document.body.appendChild(modal);
            this.bindEvents(modal);
        },

        bindEvents(modal) {
            let isDragging = false;
            let offset = { x: 0, y: 0 };

            modal.addEventListener('mousedown', (e) => {
                if (!e.target.classList.contains('ui-title')) return;
                isDragging = true;
                const rect = modal.getBoundingClientRect();
                offset.x = e.clientX - rect.left;
                offset.y = e.clientY - rect.top;
                modal.style.transform = 'none';
            });
            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);

            const tempSettings = {};

            const applyLiveUpdate = (key, value) => {
                const originalGet = StorageModule.get;
                StorageModule.get = (k) => tempSettings.hasOwnProperty(k) ? tempSettings[k] : originalGet(k);
                LiveUpdateDispatcher.dispatch(key, value);
                StorageModule.get = originalGet;
            };

            // NEW: Helper function to map element IDs to storage keys for the resolver
            const getKeyFromId = (id) => {
                if (id === 'set-wrap-titles') return CONFIG.storageKeys.wrapTitles;
                if (id === 'set-scroll-titles') return CONFIG.storageKeys.scrollTitles;
                return null;
            };

            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-fav-bg-color') { key = CONFIG.storageKeys.favBgColor; }
                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-reader-fit') { key = CONFIG.storageKeys.readerFitMode; }
                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;
                    if (id === 'set-floating-page') key = CONFIG.storageKeys.showFloatingPageIndicator;
                    if (id === 'set-devlog') key = CONFIG.storageKeys.devLogEnabled;
                    if (id === 'set-wrap-titles') key = CONFIG.storageKeys.wrapTitles;
                    if (id === 'set-scroll-titles') key = CONFIG.storageKeys.scrollTitles;
                    if (id === 'set-remove-title-link') key = CONFIG.storageKeys.removeTitleLink;
                    if (id === 'set-scrollable-gal') key = CONFIG.storageKeys.scrollableGallery;

                    // CONCEPT: Dynamic Conflict Resolver
                    // Instead of hardcoding "Wrap kills Scroll", we scan uiMap.
                    // If checking THIS box triggers a rule that disables another box, force the other box to false safely.
                    if (target.checked) {
                        CONFIG.uiMap.greyoutRules.forEach(rule => {
                            if (rule.parent === id && rule.triggerValue === true) {
                                rule.children.forEach(childId => {
                                    const childEl = document.getElementById(childId);
                                    if (childEl && childEl.checked) {
                                        childEl.checked = false; // Visually uncheck the conflicting peer
                                        const childKey = getKeyFromId(childId);
                                        if (childKey) {
                                            tempSettings[childKey] = false; // Save its false state
                                            applyLiveUpdate(childKey, false); // Dispatch the event so old CSS clears
                                        }
                                    }
                                });
                            }
                        });
                    }
                }

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

                    // Ensure all rules re-evaluate to apply grey-out styles
                    UIEffectModule.syncGreyoutRules();
                }
            });

            UIEffectModule.init(modal);

            modal.querySelector('.ui-close-btn').onclick = () => {
                Object.keys(tempSettings).forEach(k => StorageModule.set(k, tempSettings[k]));
                location.reload();
            };

            modal.querySelector('.ui-cancel-btn').onclick = () => {
                if (Object.keys(tempSettings).length > 0) location.reload();
                else this.toggle(false);
            };

            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 7: Feature - Long-Strip Reader
    // ==========================================
    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 url = win.url_from_url_from_hash(galleryid, image, 'images', image.name.split('.').pop());
                    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');
                wrapper.className = 'hitomi-reader-image-wrapper';
                wrapper.dataset.pageIndex = item.index + 1;
                wrapper.style.cssText = TemplateModule.CSS.readerWrapper(maxWidth, gapColor, `${gap}px`);

                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';

                    this.applyFitMode(img); // Apply fit logic once dimensions are known
                };

                wrapper.appendChild(img);
                container.appendChild(wrapper);
            });
        },
        applyFitMode(img) {
            const mode = StorageModule.get(CONFIG.storageKeys.readerFitMode);

            // 1 & 2: Define your precise CSS rules
            const fitWidthCSS = " width: 100% !important; height: auto !important; max-width: 100vw !important;";
            const fitHeightCSS = " height: 100vh !important; width: auto !important; max-width: 100vw !important; object-fit: contain !important;";

            if (mode === 'width') {
                img.style.cssText += fitWidthCSS;
            } else if (mode === 'height') {
                img.style.cssText += fitHeightCSS;
            } else if (mode === 'smart') {
                // 3: Smart Fit Logic
                const screenAspect = window.innerWidth / window.innerHeight;
                const imageAspect = img.naturalWidth / img.naturalHeight;

                // Compare aspect ratios. If the image is relatively "wider" than the screen,
                // fitting by height would cause horizontal overflow (cropping). Thus, we fit by width.
                // If it is relatively "taller" than the screen, fitting by width would cause vertical overflow.
                if (imageAspect > screenAspect) {
                    img.style.cssText += fitWidthCSS;
                } else {
                    img.style.cssText += fitHeightCSS;
                }
            }
        },

        updateLiveFitMode() {
            document.querySelectorAll('.hitomi-reader-image-wrapper img').forEach(img => {
                if (img.naturalWidth) this.applyFitMode(img); // Only apply if loaded
            });
        },

        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));
        },

        updateLiveGap() {
            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;
            });
        },

        updateLiveIndicator(value) {
            const indicator = document.getElementById('hitomi-floating-page-indicator');
            if (indicator) {
                indicator.style.display = value ? 'block' : 'none';
            } else if (value) {
                const total = document.querySelectorAll('.hitomi-reader-image-wrapper').length;
                if (total > 0) this.createFloatingIndicator(total);
            }
        }
    };

    // ==========================================
    // MODULE 8: Feature - List Page Enhancements
    // Purpose: Controls the Single Source of Truth for List Titles via data-attributes
    // ==========================================
    const ListTitleWrapModule = {

        toggleTitleLinks() {
            const isRemove = StorageModule.get(CONFIG.storageKeys.removeTitleLink);
            document.querySelectorAll(CONFIG.selectors.listTitles).forEach(titleLink => {
                if (isRemove) {
                    if (titleLink.href && titleLink.href !== "javascript:void(0);") {
                        titleLink.dataset.originalHref = titleLink.href;
                        titleLink.removeAttribute('href');
                        titleLink.style.cursor = 'text';
                    }
                } else if (titleLink.dataset.originalHref) {
                    titleLink.href = titleLink.dataset.originalHref;
                    titleLink.style.cursor = '';
                }
            });
        },

        init() {
            this.toggleTitleLinks();
            this.updateLive(); // Evaluates storage and applies truth immediately
        },

        // NEW: This unified function replaces the separate live updates.
        // It updates the `body` attribute and allows CSS logic to react organically.
        updateLive() {
            // 1. Establish the Single Source of Truth from Storage
            const wrapEnabled = StorageModule.get(CONFIG.storageKeys.wrapTitles);
            const scrollEnabled = StorageModule.get(CONFIG.storageKeys.scrollTitles);

            if (wrapEnabled) {
                document.body.dataset.titleMode = 'wrap';
            } else if (scrollEnabled) {
                document.body.dataset.titleMode = 'scroll';
            } else {
                document.body.dataset.titleMode = 'none';
            }

            const mode = document.body.dataset.titleMode;

            // 2. Control "Wrap" CSS visually based strictly on the Master Attribute
            let wrapStyle = document.getElementById('hitomi-live-wrap-style');
            if (!wrapStyle) {
                wrapStyle = document.createElement('style');
                wrapStyle.id = 'hitomi-live-wrap-style';
                document.head.appendChild(wrapStyle);
            }
            wrapStyle.textContent = mode === 'wrap' ? `${CONFIG.selectors.listTitles} { white-space: normal !important; overflow: visible !important; }` : '';

            // 3. Control "Scroll" CSS visually
            let scrollStyle = document.getElementById('hitomi-scroll-title-style');
            if (!scrollStyle) {
                scrollStyle = document.createElement('style');
                scrollStyle.id = 'hitomi-scroll-title-style';
                document.head.appendChild(scrollStyle);
            }
            scrollStyle.textContent = mode === 'scroll' ? TemplateModule.CSS.scrollTitles : '';

            // 4. Delegate physical DOM structural shifts to the Layout Module
            setTimeout(() => ListLayoutModule.apply(), 50);
        }
    };

    // COMBINED LAYOUT FIX MODULE
    const ListLayoutModule = {
        apply() {
            // CONCEPT: Functional Lock
            // We ignore Storage.get() here. We only care what the physical Body attribute says.
            // This prevents overlapping layout code if configs get jammed.
            const isWrap = document.body.dataset.titleMode === 'wrap';

            let styleEl = document.getElementById('hitomi-layout-mobile-fix');
            if (!styleEl) {
                styleEl = document.createElement('style');
                styleEl.id = 'hitomi-layout-mobile-fix';
                document.head.appendChild(styleEl);
            }

            if (isWrap) {
                if (window.innerWidth <= CONFIG.breakpoints.mobile) {
                    // MOBILE FIX
                    styleEl.textContent = TemplateModule.CSS.mobileListWrap(CONFIG.breakpoints.mobile);
                    document.querySelectorAll('.gallery-content > div a.lillie').forEach(el => el.style.top = '');
                    Logger.log("List Layout: Mobile fix applied based on titleMode='wrap'.");
                } else {
                    // DESKTOP FIX
                    styleEl.textContent = ''; // Remove mobile CSS
                    document.querySelectorAll('.gallery-content > div').forEach(entry => {
                        const imgCont = entry.querySelector('a.lillie > div.dj-img-cont');
                        if (imgCont) {
                            // Clear previous inline styles to prevent glitches
                            imgCont.style.top = '';
                            imgCont.style.transform = '';
                            imgCont.style.position = '';

                            // Apply new absolute center CSS
                            imgCont.style.cssText += TemplateModule.CSS.desktopListWrap;
                        }
                    });
                    Logger.log("List Layout: Desktop fix applied based on titleMode='wrap'.");
                }
            } else {
                // CLEANUP (If mode is 'scroll' or 'none')
                styleEl.textContent = '';
                document.querySelectorAll('.gallery-content > div a.lillie').forEach(el => el.style.top = '');
            }
        }
    };

    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 = () => {
                ListTitleWrapModule.toggleTitleLinks();
                const links = document.querySelectorAll(CONFIG.selectors.listThumbLink);
                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";
            });
        },

        updateLive(value) {
            document.querySelectorAll('.hitomi-page-count-row').forEach(el => el.style.display = value ? '' : 'none');
            if (value) {
                this.processLinks(document.querySelectorAll(CONFIG.selectors.listThumbLink), window);
            }
        }
    };

    // ==========================================
    // MODULE 9: Feature - Gallery Page Enhancements
    // ==========================================
    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);
            }
        },

        updateLive(value) {
            document.querySelectorAll('.hitomi-gal-page-count-row').forEach(el => el.style.display = value ? '' : 'none');
            if (value) {
                this.init();
            }
        }
    };

    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);

            let cssString = TemplateModule.CSS.galleryGrid(cols, gapH, gapV);

            if (StorageModule.get(CONFIG.storageKeys.scrollableGallery)) {
                cssString += '\n' + TemplateModule.CSS.galleryScrollable(StorageModule.get(CONFIG.storageKeys.scrollGalleryHeight));
            }

            this.injectStyles(cssString);
        },

        injectStyles(cssText) {
            let styleEl = document.getElementById('hitomi-gallery-grid-styles');
            if (!styleEl) {
                styleEl = document.createElement('style');
                styleEl.id = 'hitomi-gallery-grid-styles';
                document.head.appendChild(styleEl);
            }
            styleEl.textContent = cssText;
        },

        updateLive() {
            if (document.querySelector(CONFIG.selectors.galleryParent)) {
                this.init();
            }
        }
    };

    // ==========================================
    // MODULE 10: Feature - Favorite System
    // ==========================================
    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;

                //shrink search bar on desktop
                const searchBarContainer = document.querySelector('.header-table');
                if (searchBarContainer && window.innerWidth > CONFIG.breakpoints.mobile) {
                    searchBarContainer.style.cssText = TemplateModule.CSS.searchBarshrink;
                }
                // append favorite button
                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); });
                }
            },

            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';
                        const currentBg = StorageModule.get(CONFIG.storageKeys.favBgColor);
                        overlay.style.cssText = TemplateModule.CSS.favOverlay(currentBg);

                        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');
                    }
                    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');
                if (!content) return;
                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);
                });
            },

            updateLiveWidth() {
                if (document.getElementById('hitomi-fav-overlay')?.style.display === 'block') {
                    this.renderItems();
                }
            },

            updateLiveBg(value) {
                const overlay = document.getElementById('hitomi-fav-overlay');
                if (overlay) overlay.style.background = value;
            }
        },
        init() {
            this.Page.injectNavButton();
            window.addEventListener('popstate', () => this.Page.toggleModal(window.location.hash === '#favorites', true));
        }
    };

    // ==========================================
    // INITIALIZATION PIPELINE
    // ==========================================

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

        const shouldStop = isReader || isGallery || isListPage;
        if (shouldStop && obs) {
            obs.disconnect();
            Logger.log("Target found. Observer offline.");
        }
    }

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

    const debouncedPipeline = debounce((mutations, obs) => {
        runDetectionPipeline(obs);
    }, 200);

    const observer = new MutationObserver(debouncedPipeline);
    observer.observe(document.body, { childList: true, subtree: true });
    runDetectionPipeline(observer);

})();