Hitomi Enhancer

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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

})();