Gallery Long-Strip Reader

Enable long-strip reader mode for any user-defined gallery website (with image source URL is predictable).

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

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

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

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

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

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

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

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

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

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

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

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

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

// ==UserScript==
// @name         Gallery Long-Strip Reader
// @namespace    http://tampermonkey.net/
// @version      8.7.4
// @description  Enable long-strip reader mode for any user-defined gallery website (with image source URL is predictable).
// @author       php
// @match        *://nhentai.net/*
// @match        *://imhentai.xxx/view/*
// @require      https://code.jquery.com/jquery-3.5.1.min.js
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_registerMenuCommand
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // Save references to original console logging methods before potential debugging override
    const _origLog = console.log;
    const _origWarn = console.warn;

    // =====================================================================
    // --- 1. CONFIGURATION & DATA STORAGE MODULE ---
    // Houses the master default template and local DB storage operations.
    // =====================================================================
    const CONFIG_MODULE = {
        STORAGE_KEY: "gallery_long-strip_reader_cfg",

        // Default layout for creating entirely new websites from scratch
        BLANK_PROFILE_TEMPLATE: {
            urlTemplate: "https://{cdn}.example.com/{gid}/{page}.{ext}",
            firstImgSelector: "",
            cdnList: "",
            allowedExtensions: "webp, jpg, png, gif",
            readerUrlRegex: "",
            pagesSelector: "",
            icSelector: "",
            imageFitMode: "smart",
            preloadCount: 3,
            throttleDelay: 200,
            imageGap: 15,
            loadTimeout: 5000,
            showFloatingIndicator: true,
            cdnHealth: 3,
            extHealth: 3,
            maxConcurrentLoads: 4,
            removeElements: ""
        },

        // Unified JSON-like default data object containing global settings and site profiles
        DEFAULT_DATA: {
            settings: {
                debugMode: false
            },
            profiles: {
                "nhentai": {
                    urlTemplate: "https://{cdn}.nhentai.net/galleries/{gid}/{page}.{ext}",
                    firstImgSelector: "#image-container img@src",
                    cdnList: "i1, i2, i3, i4",
                    allowedExtensions: "webp, jpg, png, gif",
                    readerUrlRegex: String.raw`https?:\/\/nhentai\.net\/g\/\d+\/\d+\/?`,
                    pagesSelector: "span.num-pages",
                    icSelector: "#image-container",
                    imageFitMode: "smart",
                    preloadCount: 3,
                    throttleDelay: 200,
                    imageGap: 15,
                    loadTimeout: 5000,
                    showFloatingIndicator: true,
                    cdnHealth: 3,
                    extHealth: 3,
                    maxConcurrentLoads: 4,
                    removeElements: ""
                },
                "imhentai": {
                    urlTemplate: "https://{cdn}.imhentai.xxx/{gid}/{page}.{ext}",
                    firstImgSelector: "#gimg@src",
                    cdnList: "m11, m10, m2",
                    allowedExtensions: "webp, jpg, png, gif",
                    readerUrlRegex: String.raw`https?:\/\/imhentai\.xxx\/view\/\d+\/\d+\/?`,
                    pagesSelector: "body > div.overlay > div > div.row.gallery_view > div > div:nth-child(1) > button > span.total_pages",
                    icSelector: "body > div.overlay > div > div.row.gallery_view > div > div.pre_img",
                    imageFitMode: "smart",
                    preloadCount: 3,
                    throttleDelay: 200,
                    imageGap: 15,
                    loadTimeout: 5000,
                    showFloatingIndicator: true,
                    cdnHealth: 3,
                    extHealth: 3,
                    maxConcurrentLoads: 4,
                    removeElements: "#bar, #footer, .nav_pagination.col-md-12:nth-child(2 of .nav_pagination.col-md-12), a.return_btn.btn.btn-primary"
                }
            }
        },

        getInitialStorage: function() {
            return JSON.parse(JSON.stringify(this.DEFAULT_DATA));
        }
    };

    // Load multi-profile setup from Tampermonkey local database
    let globalStorage = GM_getValue(CONFIG_MODULE.STORAGE_KEY);

    // Safety Fallbacks for migrating users from older versions
    if (!globalStorage || !globalStorage.profiles) {
        globalStorage = CONFIG_MODULE.getInitialStorage();
    }
    if (!globalStorage.settings) {
        globalStorage.settings = { debugMode: false };
    }

    // --- Dynamic Runtime Directives ---
    let activeProfileName = "";
    let CONFIG = null;
    let GLOBAL_SETTINGS = globalStorage.settings;

    let currentCdnList = [];
    let currentExtList = [];
    let healthRegistry = { cdn: 3, ext: 3 };
    let activeGid = "";
    let lastProcessedGid = "";

    // Apply global debug configuration
    if (GLOBAL_SETTINGS.debugMode) {
        console.log = _origLog;
        console.warn = _origWarn;
    } else {
        console.log = console.warn = () => {};
    }

    // =====================================================================
    // --- 2. HTML/CSS ASSET REPOSITORY (UI_ASSETS) ---
    // Centralized configuration-driven design. No inline styling in core logic.
    // =====================================================================
    const UI_ASSETS = {
        indicatorStyle: `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;`,

        buildPlaceholderHtml: function(pageNum, gap) {
            const style = `min-height:80vh; width:100%; text-align:center; margin-top:${gap}px; background:#111; display:flex; align-items:center; justify-content:center; color:#aaa;`;
            return `<div id="p-con-${pageNum}" class="strip-page" data-page="${pageNum}" style="${style}">Loading Page ${pageNum}...</div>`;
        },

        buildFailureBoxHtml: function(pageNum, triedUrlsHtml) {
            const containerStyle = `padding: 20px; border: 1px solid #444; background: #1a1a1a; color: #ff4d4d; width: 80%; max-width: 600px; margin: 0 auto; display: flex; flex-direction: column; align-items: center;`;
            const btnStyle = `padding: 6px 16px; background: #6f42c1; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; margin-bottom: 12px; font-size: 13px;`;
            const logBoxStyle = `max-height: 120px; overflow-y: auto; text-align: left; background: #111; border: 1px solid #333; padding: 8px; width: 100%; box-sizing: border-box;`;

            return `
                <div style="${containerStyle}">
                    <p style="font-weight:bold; margin: 0 0 10px 0;">❌ Page ${pageNum} Failed All Fallbacks</p>
                    <button class="refetch-btn" style="${btnStyle}">🔄 Refetch Page ${pageNum}</button>
                    <div style="${logBoxStyle}">${triedUrlsHtml}</div>
                </div>`;
        },

        escapeHtmlAttr: function(str) {
            if (!str) return '';
            return str.toString().replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/'/g, '&#39;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
        },

        // Modals for settings UI
        modalCss: `<style>
            #scroll-overlay { position: fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.3); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); z-index:999999; display:flex; align-items:center; justify-content:center; padding: 10px; box-sizing: border-box; }
            #scroll-modal { background: rgba(255, 255, 255, 0.75); backdrop-filter: blur(20px) saturate(150%); -webkit-backdrop-filter: blur(20px) saturate(150%); border: 1px solid rgba(255, 255, 255, 0.4); padding:20px 25px; border-radius:12px; width:100%; max-width:700px; max-height: 95vh; overflow-y: auto; font-family: system-ui, sans-serif; box-shadow:0 10px 40px rgba(0,0,0,0.2); color:#222; }

            #scroll-modal h3 { margin:0 0 15px; font-size:20px; border-bottom:2px solid rgba(111, 66, 193, 0.5); padding-bottom:8px; display: flex; justify-content: center; align-items: center; font-weight: bold;}
            #scroll-modal label { display:block; margin:12px 0 4px; font-weight:700; font-size:11px; color:#444; text-transform:uppercase; letter-spacing: 0.5px; white-space: normal; word-wrap: break-word; overflow-wrap: break-word; line-height: 1.3; }

            #scroll-modal input, #scroll-modal select, #scroll-modal textarea { width:100%; padding:10px; border:1px solid rgba(0,0,0,0.15); border-radius:6px; box-sizing:border-box; font-size:13px; background: rgba(255,255,255,0.6); transition: border 0.2s, background 0.2s; font-family: monospace; }
            #scroll-modal input:focus, #scroll-modal select:focus, #scroll-modal textarea:focus { border-color: #6f42c1; outline: none; background: #fff; }

            /* Profile Outer Card Wrapper */
            .profile-wrapper-card { background: rgba(0, 0, 0, 0.03); border: 1px solid rgba(0, 0, 0, 0.08); border-radius: 12px; padding: 12px; margin-top: 15px; box-shadow: inset 0 2px 10px rgba(0,0,0,0.02); }

            /* Reusable Layout Cards */
            .section-container { background: rgba(0, 0, 0, 0.02); border: 1px solid rgba(0, 0, 0, 0.08); border-radius: 8px; padding: 10px; margin-bottom: 15px; }
            .section-title { margin: 0 0 15px 0; font-size: 15px; color: #333; text-align: center; border-bottom: 1px solid rgba(0,0,0,0.1); padding-bottom: 8px; }

            .s-section { margin-bottom: 15px; padding: 10px; border-radius: 8px; border: 1px solid rgba(233, 236, 239, 0.5); background: rgba(248, 249, 250, 0.5); }

            /* Responsive Flexbox */
            .s-flex { display: flex; flex-wrap: wrap; gap: 15px; }
            .s-flex > div { flex: 1 1 200px; box-sizing: border-box; min-width: 0; }
            .s-btns { display:flex; justify-content:flex-end; gap:10px; margin-top:20px; align-items: center; position: sticky; bottom: -20px; background: rgba(255,255,255,0.85); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); padding: 15px 25px; margin: 20px -25px -20px -25px; border-top: 1px solid rgba(0,0,0,0.1); z-index: 10; border-radius: 0 0 12px 12px; }
            .s-btn { padding:10px 20px; border-radius:6px; cursor:pointer; border:none; font-weight:bold; font-size: 13px; transition: opacity 0.2s; }
            .s-btn:hover { opacity: 0.85; }
            .s-save { background:#6f42c1; color:#fff; }
            .s-reset { background:#dc3545; color:#fff; margin-right: auto; }
            .s-cancel { background: rgba(0,0,0,0.1); color:#333; }
            .s-secondary { background:#28a745; color:#fff; }

            /* Improved Hint Styling */
            p.s-hint { font-size:11px; color:#555; margin: 6px 0 0; line-height: 1.4; }
            code { background: rgba(0,0,0,0.06); padding: 2px 4px; border-radius: 4px; font-family: monospace; font-size: 11px; color: #d63384; font-weight: bold; }

            .checkbox-item { flex: 1 1 180px; display: flex; flex-direction: column; justify-content: center; min-width: 160px; }
            #scroll-modal input[type="checkbox"] { width: auto !important; margin: 0 8px 0 0 !important; cursor: pointer; padding: 0; background: none; transform: scale(1.1); }
            .checkbox-label { display: flex !important; align-items: center; justify-content: flex-start; text-transform: none !important; cursor: pointer; margin: 4px 0 !important; font-size: 13px !important; color: #222 !important; white-space: normal; line-height: 1.4; }
        </style>`,

        getModalHtml: function(storage, selectedProfileName) {
            let profileOptions = "";
            Object.keys(storage.profiles).forEach(pName => {
                const selected = pName === selectedProfileName ? "selected" : "";
                profileOptions += `<option value="${this.escapeHtmlAttr(pName)}" ${selected}>${this.escapeHtmlAttr(pName)}</option>`;
            });
            profileOptions += `<option value="__new__">+ Add New Profile...</option>`;

            return `
            ${this.modalCss}
            <div id="scroll-overlay">
                <div id="scroll-modal">
                    <h3>Configuration</h3>

                    <div class="s-section" style="background: rgba(40, 167, 69, 0.1); border-color: rgba(40, 167, 69, 0.3);">
                        <label style="color: #28a745; font-size: 12px;">Global Settings</label>
                        <div class="checkbox-item">
                             <label class="checkbox-label"><input type="checkbox" id="cfg-debug" ${storage.settings.debugMode ? 'checked' : ''}> Show Console Log</label>
                        </div>
                    </div>

                    <div class="profile-wrapper-card">
                        <div class="s-section" style="background: rgba(241, 240, 246, 0.6); border-color: rgba(111, 66, 193, 0.4);">
                            <label style="color: #6f42c1; font-size: 12px;">Active Profile (Website)</label>
                            <div class="s-flex" style="align-items: center;">
                                <div class="s-flex" style="align-items: center; flex: 1 1 70%;">
                                    <div style="flex: 2; display: flex; gap: 8px;">
                                        <select id="set-profile-selector" style="flex: 1;">
                                            ${profileOptions}
                                        </select>
                                        <button class="s-btn s-reset" id="btn-delete-profile" style="padding: 10px; margin: 0; display: ${selectedProfileName === '__new__' ? 'none' : 'block'};" title="Delete Active Profile">🗑️</button>
                                    </div>
                                    <div id="new-profile-name-container" style="flex: 2; display: none;">
                                        <input id="set-new-profile-name" type="text" placeholder="Profile name">
                                    </div>
                                </div>
                                <div style="display: flex; gap: 5px; justify-content: flex-end; width: 100%; flex: 1 1 70%;">
                                    <button class="s-btn s-secondary" id="btn-export" title="Export All Profiles to file" style="padding: 10px 12px; flex: 1;">📤 Export</button>
                                    <button class="s-btn s-secondary" id="btn-import" title="Import Profiles from file" style="padding: 10px 12px; background: #007bff; flex: 1;">📥 Import</button>
                                    <input type="file" id="import-file-input" style="display: none;" accept=".json">
                                </div>
                            </div>
                        </div>

                        <div id="profile-fields-container"></div>
                    </div>

                    <div class="s-btns">
                        <button class="s-btn s-reset" id="set-reset">Reset All Profiles</button>
                        <button class="s-btn s-cancel" id="set-cancel">Cancel</button>
                        <button class="s-btn s-save" id="set-save">Save & Reload</button>
                    </div>
                </div>
            </div>`;
        },

        getFieldsHtml: function(cfg) {
            return `
            <div class="section-container">
                <h4 class="section-title">🖼️ Image Fetching Configuration</h4>

                <div class="s-section">
                    <label style="text-align: center; font-size: 14px; font-weight: bold;">1. Site urls</label>
                    <div class="s-flex">
                        <div>
                            <label>Gallery Reader Page URL (Regex)</label>
                            <input id="set-regex" value="${this.escapeHtmlAttr(cfg.readerUrlRegex)}">
                        </div>
                    </div>
                    <div style="margin-top: 10px;">
                        <label>Image source URL Template</label>
                        <input id="set-template" value="${this.escapeHtmlAttr(cfg.urlTemplate)}">
                        <p class="s-hint">Require placeholders: <code>{cdn}</code>, <code>{gid}</code>, <code>{page}</code>, and <code>{ext}</code>.</p>
                        <p style="display:block; text-align:left; font-size:11px; margin-top:5px; padding-left: 5px; border-left: 2px solid #6f42c1; color: #555;">
                            <code>{cdn}</code>: CDN node | <code>{gid}</code>: Gallery ID | <code>{page}</code>: Page number | <code>{ext}</code>: Image extension
                        </p>
                    </div>
                </div>

                <div class="s-section">
                    <label style="text-align: center; font-size: 14px; font-weight: bold;">2. Initial Placeholders</label>
                    <div class="s-flex">
                        <div>
                            <label>First Page Image Source URL (CSS-Selector@Attr)</label>
                            <input id="set-first" value="${this.escapeHtmlAttr(cfg.firstImgSelector)}">
                            <p class="s-hint">Use <code>@ATTR</code> for HTML Attribute (e.g., <code>@src</code>). Omit <code>@ATTR</code> to use innerText.</p>
                        </div>
                    </div>
                    
                </div>

                <div class="s-section">
                    <label style="text-align: center; font-size: 14px; font-weight: bold;">3. Fallback Placeholders</label>
                    <div class="s-flex">
                        <div>
                            <label>CDN nodes (Comma separated)</label>
                            <input id="set-cdn-list" value="${this.escapeHtmlAttr(cfg.cdnList)}">
                        </div>
                        <div>
                            <label>Extensions (Comma separated)</label>
                            <input id="set-exts" value="${this.escapeHtmlAttr(cfg.allowedExtensions)}">
                        </div>
                    </div>
                    <p class="s-hint">Re-fetch of image after a failed request will use these placeholders prioritized from left to right.</p>
                </div>

                <div class="s-section">
                    <label style="text-align: center; font-size: 14px; font-weight: bold;">4. Page elements</label>
                    <div class="s-flex">
                        <div>
                            <label>Total Page Counts (CSS-Selector@Attr)</label>
                            <input id="set-pages" value="${this.escapeHtmlAttr(cfg.pagesSelector)}">
                             <p class="s-hint">Use <code>@ATTR</code> for HTML Attribute (e.g., <code>@src</code>). Omit <code>@ATTR</code> to use innerText.</p>
                        </div>
                        <div>
                            <label>Container for All Images(CSS-Selector)</label>
                            <input id="set-ic" value="${this.escapeHtmlAttr(cfg.icSelector)}">
                        </div>
                    </div>
                </div>

                <div class="s-section">
                    <label style="text-align: center; font-size: 14px; font-weight: bold;">5. (Advanced) Network request setting</label>
                    <div class="s-flex">
                        <div>
                            <label>Max Concurrency</label>
                            <input id="set-concurrent" type="number" value="${cfg.maxConcurrentLoads}">
                            <p class="s-hint">Max simultaneous network requests (slots). Should be less than browser limits (6 for most browsers).</p>
                        </div>
                    </div>
                    <div class="s-flex">
                        <div>
                            <label>Load Timeout (ms)</label>
                            <input id="set-timeout" type="number" value="${cfg.loadTimeout}">
                            <p class="s-hint">Max latency allowed before considering a fetch failed.</p>
                        </div>
                        <div>
                            <label>Throttle Delay (ms)</label>
                            <input id="set-delay" type="number" value="${cfg.throttleDelay}">
                            <p class="s-hint">Delay between network requests.</p>
                        </div>
                    </div>
                    <div class="s-flex">
                        <div>
                            <label>CDN Health (Strikes)</label>
                            <input id="set-cdn-health" type="number" value="${cfg.cdnHealth}">
                            <p class="s-hint">Failed fetches allowed before updating CDN nodes priority.</p>
                        </div>
                        <div>
                            <label>Extension Health (Strikes)</label>
                            <input id="set-ext-health" type="number" value="${cfg.extHealth}">
                            <p class="s-hint">Failed fetches allowed before updating file Extension priority.</p>
                        </div>
                    </div>
                </div>
            </div> <div class="section-container">
                <h4 class="section-title">📖 Reader Configuration</h4>

                <div class="s-section">
                    <label style="text-align: center; font-size: 14px; font-weight: bold;">UI setting</label>
                    <div class="s-flex" style="margin-bottom: 10px;">
                        <div>
                            <label>Image Fit Mode</label>
                            <select id="set-fit-mode">
                                <option value="width" ${cfg.imageFitMode === 'width' ? 'selected' : ''}>Fit Width</option>
                                <option value="height" ${cfg.imageFitMode === 'height' ? 'selected' : ''}>Fit Height</option>
                                <option value="smart" ${cfg.imageFitMode === 'smart' ? 'selected' : ''}>Smart Fit</option>
                            </select>
                        </div>
                        <div>
                            <label>Preload Page Count</label>
                            <input id="set-preload" type="number" value="${cfg.preloadCount}">
                        </div>
                        <div>
                            <label>Gap Size (px)</label>
                            <input id="set-gap" type="number" value="${cfg.imageGap}">
                        </div>
                    </div>

                    <div class="s-flex" style="margin-bottom: 10px;">
                        <div style="margin-top:5px; width:100%;">
                            <label>HTML Elements to Remove (CSS-Selector, Comma separated)</label>
                            <textarea id="set-remove">${this.escapeHtmlAttr(cfg.removeElements || "")}</textarea>
                            <p class="s-hint">Example: <code>#header, .ads-wrapper, .footer-links</code></p>
                        </div>
                    </div>
                    <div class="checkbox-item">
                         <label class="checkbox-label"><input type="checkbox" id="cfg-floating" ${cfg.showFloatingIndicator ? 'checked' : ''}> Show current page number</label>
                    </div>
                </div>
            </div> `;
        }
    };

    // =====================================================================
    // --- 3. SETTINGS UI MANAGER ---
    // Handles the rendering and saving of the Tampermonkey overlay menu.
    // =====================================================================
    function showSettings() {
        if (document.getElementById('scroll-overlay')) return;

        let workingStorage = JSON.parse(JSON.stringify(globalStorage));
        let currentEditingProfile = activeProfileName;

        const overlayDiv = document.createElement('div');
        overlayDiv.innerHTML = UI_ASSETS.getModalHtml(workingStorage, currentEditingProfile);
        document.body.appendChild(overlayDiv);

        const fieldsContainer = document.getElementById('profile-fields-container');
        const selector = document.getElementById('set-profile-selector');
        const newProfileNameContainer = document.getElementById('new-profile-name-container');
        const newProfileNameInput = document.getElementById('set-new-profile-name');

        function saveFieldsToWorkingCopy(profileName) {
            if (!profileName || profileName === '__new__') return;
            if (!workingStorage.profiles[profileName]) {
                workingStorage.profiles[profileName] = {};
            }

            // Save Global Settings
            workingStorage.settings.debugMode = document.getElementById('cfg-debug').checked;

            // Save Specific Profile Settings
            const prof = workingStorage.profiles[profileName];
            prof.urlTemplate = document.getElementById('set-template').value;
            prof.firstImgSelector = document.getElementById('set-first').value;
            prof.cdnList = document.getElementById('set-cdn-list').value;
            prof.allowedExtensions = document.getElementById('set-exts').value;
            prof.readerUrlRegex = document.getElementById('set-regex').value;
            prof.pagesSelector = document.getElementById('set-pages').value;
            prof.icSelector = document.getElementById('set-ic').value;
            prof.imageFitMode = document.getElementById('set-fit-mode').value;
            prof.preloadCount = parseInt(document.getElementById('set-preload').value) || 3;
            prof.imageGap = parseInt(document.getElementById('set-gap').value) || 15;
            prof.removeElements = document.getElementById('set-remove').value;
            prof.showFloatingIndicator = document.getElementById('cfg-floating').checked;
            prof.maxConcurrentLoads = parseInt(document.getElementById('set-concurrent').value) || 4;
            prof.loadTimeout = parseInt(document.getElementById('set-timeout').value) || 5000;
            prof.throttleDelay = parseInt(document.getElementById('set-delay').value) || 200;
            prof.cdnHealth = parseInt(document.getElementById('set-cdn-health').value) || 3;
            prof.extHealth = parseInt(document.getElementById('set-ext-health').value) || 3;
        }

        function renderFieldsForProfile(profileName) {
            let cfg = CONFIG_MODULE.BLANK_PROFILE_TEMPLATE;
            if (profileName !== '__new__' && workingStorage.profiles[profileName]) {
                cfg = workingStorage.profiles[profileName];
            }
            fieldsContainer.innerHTML = UI_ASSETS.getFieldsHtml(cfg);
        }

        renderFieldsForProfile(currentEditingProfile);

        selector.onchange = () => {
            const nextProfile = selector.value;
            document.getElementById('btn-delete-profile').style.display = (nextProfile === '__new__') ? 'none' : 'block';

            if (currentEditingProfile !== '__new__') {
                saveFieldsToWorkingCopy(currentEditingProfile);
            }

            if (nextProfile === '__new__') {
                newProfileNameContainer.style.display = 'block';
                newProfileNameInput.value = '';
                renderFieldsForProfile('__new__');
            } else {
                newProfileNameContainer.style.display = 'none';
                renderFieldsForProfile(nextProfile);
            }
            currentEditingProfile = nextProfile;
        };

        // Delete Profile Logic
        document.getElementById('btn-delete-profile').onclick = () => {
            if (currentEditingProfile === '__new__') return;
            const profilesArray = Object.keys(workingStorage.profiles);
            if (profilesArray.length <= 1) {
                alert("Cannot delete the last remaining profile.");
                return;
            }
            if (confirm(`Are you sure you want to delete the profile "${currentEditingProfile}"?`)) {
                delete workingStorage.profiles[currentEditingProfile];
                const updatedProfiles = Object.keys(workingStorage.profiles);
                currentEditingProfile = updatedProfiles[0];

                // Re-render dropdown
                let newOptions = "";
                updatedProfiles.forEach(pName => {
                    const selected = pName === currentEditingProfile ? "selected" : "";
                    newOptions += `<option value="${UI_ASSETS.escapeHtmlAttr(pName)}" ${selected}>${UI_ASSETS.escapeHtmlAttr(pName)}</option>`;
                });
                newOptions += `<option value="__new__">+ Add New Profile...</option>`;
                selector.innerHTML = newOptions;

                renderFieldsForProfile(currentEditingProfile);
            }
        };

        // File I/O Logic
        document.getElementById('btn-export').onclick = () => {
            if (currentEditingProfile !== '__new__') saveFieldsToWorkingCopy(currentEditingProfile);

            // Construct local YYYY-MM-DD_HH-mm-ss format
            const now = new Date();
            const y = now.getFullYear();
            const m = String(now.getMonth() + 1).padStart(2, '0');
            const d = String(now.getDate()).padStart(2, '0');
            const H = String(now.getHours()).padStart(2, '0');
            const M = String(now.getMinutes()).padStart(2, '0');
            const S = String(now.getSeconds()).padStart(2, '0');
            const formattedTime = `${y}-${m}-${d}_${H}-${M}-${S}`;

            // Reconstruct object to force _meta to the top and drop currentProfileName
            const exportData = {
                _meta: {
                    scriptVersion: typeof GM_info !== 'undefined' ? GM_info.script.version : "Unknown",
                    exportDate: formattedTime
                },
                settings: workingStorage.settings,
                profiles: workingStorage.profiles
            };

            const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(exportData, null, 4));
            const downloadAnchor = document.createElement('a');
            downloadAnchor.setAttribute("href", dataStr);
            downloadAnchor.setAttribute("download", `Gallery_Long-Strip_Reader_backup_${formattedTime}.json`);
            document.body.appendChild(downloadAnchor);
            downloadAnchor.click();
            downloadAnchor.remove();
        };

        const importInput = document.getElementById('import-file-input');
        document.getElementById('btn-import').onclick = () => importInput.click();
        importInput.onchange = (e) => {
            const file = e.target.files[0];
            if (!file) return;
            const reader = new FileReader();
            reader.onload = (event) => {
                try {
                    const imported = JSON.parse(event.target.result);
                    if (imported && imported.profiles && typeof imported.profiles === 'object') {
                        workingStorage = imported;
                        if(!workingStorage.settings) workingStorage.settings = { debugMode: false }; // safety check

                        let profileOptions = "";
                        Object.keys(workingStorage.profiles).forEach(pName => {
                            profileOptions += `<option value="${UI_ASSETS.escapeHtmlAttr(pName)}">${UI_ASSETS.escapeHtmlAttr(pName)}</option>`;
                        });
                        profileOptions += `<option value="__new__">+ Add New Profile...</option>`;
                        selector.innerHTML = profileOptions;

                        const profiles = Object.keys(workingStorage.profiles);
                        currentEditingProfile = profiles[0]; // Simply grab the first profile
                        selector.value = currentEditingProfile;

                        newProfileNameContainer.style.display = 'none';
                        document.getElementById('cfg-debug').checked = workingStorage.settings.debugMode;
                        renderFieldsForProfile(currentEditingProfile);
                        alert("Environment maps synchronized! Click 'Save & Reload' to apply structures permanently.");
                    } else {
                        alert("Invalid deployment profile document configuration schema.");
                    }
                } catch (err) {
                    alert("Parse processing fault tracking context file: " + err.message);
                }
            };
            reader.readAsText(file);
        };

        document.getElementById('set-cancel').onclick = () => overlayDiv.remove();

        document.getElementById('set-reset').onclick = () => {
            if (confirm("Reset local architecture allocations? This removes custom profiles.")) {
                GM_deleteValue(CONFIG_MODULE.STORAGE_KEY);
                location.reload();
            }
        };

        document.getElementById('set-save').onclick = () => {
            let targetProfileName = currentEditingProfile;

            if (currentEditingProfile === '__new__') {
                const newName = newProfileNameInput.value.trim();
                if (!newName || newName === '__new__') {
                    alert("Please identify a clean unique identification string profile label name.");
                    return;
                }
                targetProfileName = newName;
                workingStorage.profiles[targetProfileName] = JSON.parse(JSON.stringify(CONFIG_MODULE.BLANK_PROFILE_TEMPLATE));
            }

            saveFieldsToWorkingCopy(targetProfileName);

            GM_setValue(CONFIG_MODULE.STORAGE_KEY, workingStorage);
            location.reload();
        };
    }

    GM_registerMenuCommand("Configuration", showSettings);

    // =====================================================================
    // --- 4. INDEPENDENT FEATURE MODULE ---
    // Contains standalone UI overlays and UX quality of life tools.
    // =====================================================================
    const FEATURE_MODULE = {
        /**
         * Parses 'removeElements' config string and uses jQuery to hide unwanted UI bloat.
         */
        cleanupUI: function() {
            if (!CONFIG || !CONFIG.removeElements) return;
            const selectors = CONFIG.removeElements.split(',').map(s => s.trim()).filter(s => s);
            selectors.forEach(selector => {
                $(selector).hide();
            });
        },

        /**
         * Creates a fixed UI element that updates dynamically based on scroll position.
         */
        setupFloatingPageIndicator: function(currentPage, totalPages) {
            if (!CONFIG.showFloatingIndicator) return;

            $('#floating-page-indicator').remove();

            // Tag the original container and new pages for tracking
            const ic = $(CONFIG.icSelector);
            ic.attr('data-page', currentPage).addClass('tracked-page');
            $('.strip-page').addClass('tracked-page');

            const indicator = document.createElement("div");
            indicator.id = "floating-page-indicator";
            indicator.style.cssText = UI_ASSETS.indicatorStyle;
            indicator.innerText = `${currentPage} / ${totalPages}`;
            document.body.appendChild(indicator);

            // Create observer (the 'tripwire' in the exact center of the screen)
            const observer = new IntersectionObserver((entries) => {
                entries.forEach(entry => {
                    if (entry.isIntersecting) {
                        const pNum = entry.target.getAttribute('data-page');
                        if (pNum) indicator.innerText = `${pNum} / ${totalPages}`;
                    }
                });
            }, { rootMargin: '-50% 0px -50% 0px' });

            document.querySelectorAll('.tracked-page').forEach(div => observer.observe(div));
        }
    };

    // =====================================================================
    // --- 5. NETWORK & CONSENSUS ENGINE ---
    // Manages the async dispatch queues and consensus rotation strategy.
    // =====================================================================
    const NETWORK_ENGINE = {
        requestQueue: [],
        activeLoadCount: 0,
        isProcessingQueue: false,

        /**
         * Modifies CDN and Extension priorities based on success or failure events.
         */
        updatePriorityWithConsensus: function(array, successValue, type, currentUrl = "", isFailure = false, isBlueprint = false) {
            if (!successValue) return;
            const val = successValue.trim();
            const index = array.indexOf(val);
            const maxHealth = type === 'cdn' ? CONFIG.cdnHealth : CONFIG.extHealth;

            // Blueprint Phase - Force override the tracker to index 0 based on extracted manifest
            if (isBlueprint) {
                if (index > 0) {
                    array.splice(index, 1);
                    array.unshift(val);
                    console.log(`[Blueprint] '${val}' forced to front for ${type.toUpperCase()}.`);
                } else if (index === -1) {
                    array.unshift(val);
                }
                healthRegistry[type] = maxHealth;
                return;
            }

            // Failure Phase - Strike a health point from the leader
            if (isFailure) {
                healthRegistry[type] = Math.max(0, healthRegistry[type] - 1);
                console.warn(`[Consensus] ${type.toUpperCase()} head strike! Health: ${healthRegistry[type]}/${maxHealth} | Value: ${val} | Failed URL: ${currentUrl}`);
                return;
            }

            // Success Phase - Heal leader or promote fallback
            if (index === 0) {
                const oldHealth = healthRegistry[type];
                healthRegistry[type] = Math.min(maxHealth, healthRegistry[type] + 1);
                if (healthRegistry[type] > oldHealth) {
                    console.log(`[Consensus] ${type.toUpperCase()} head heal! Health: ${healthRegistry[type]}/${maxHealth} | Value: ${val} | Success URL: ${currentUrl}`);
                }
                return;
            }

            if (healthRegistry[type] <= 0) {
                if (index > 0) {
                    array.splice(index, 1);
                    array.unshift(val);
                    console.log(`[${type.toUpperCase()} Rotation] '${val}' moved to front. Triggered by success URL: ${currentUrl}`);
                } else if (index === -1) {
                    array.unshift(val);
                    console.log(`[Consensus] New ${type.toUpperCase()} discovered and added: '${val}'`);
                }
                console.log(`[${type.toUpperCase()} Updated List Order]:`, array);
                healthRegistry[type] = maxHealth;
            }
        },

        loadImage: function(i, container) {
            this.requestQueue.push({ i, container });
            if (!this.isProcessingQueue) this.processQueue();
        },

        processQueue: async function() {
            if (this.isProcessingQueue) return;
            this.isProcessingQueue = true;

            while (this.requestQueue.length > 0) {
                // Concurrency limiting based on browser hardware caps
                if (this.activeLoadCount >= CONFIG.maxConcurrentLoads) {
                    await new Promise(r => setTimeout(r, 100));
                    continue;
                }

                const { i, container } = this.requestQueue.shift();
                this.activeLoadCount++;

                this.executeLoad(i, container);
                await new Promise(r => setTimeout(r, CONFIG.throttleDelay));
            }
            this.isProcessingQueue = false;
        },

        executeLoad: function(i, container) {
            const self = this; // Maintain context inside recursive closure

            const tryMatrix = (cdnIdx, extIdx) => {
                // 1. Matrix Exhaustion (All CDNs Failed)
                if (cdnIdx >= currentCdnList.length) {
                    let triedUrlsHtml = "";
                    currentCdnList.forEach(cdn => {
                        currentExtList.forEach(ext => {
                            const url = CONFIG.urlTemplate.replace('{page}', i).replace('{cdn}', cdn).replace('{ext}', ext).replace('{gid}', activeGid);
                            triedUrlsHtml += `<div style="font-size:10px; word-break:break-all; margin-top:5px; color:#888;">${UI_ASSETS.escapeHtmlAttr(url)}</div>`;
                        });
                    });

                    container.innerHTML = UI_ASSETS.buildFailureBoxHtml(i, triedUrlsHtml);
                    container.querySelector('.refetch-btn').addEventListener('click', () => {
                        container.innerHTML = `<div style="color:#444;">Refetching Page ${i}...</div>`;
                        self.loadImage(i, container);
                    });

                    self.activeLoadCount--;
                    return;
                }

                // 2. Extension Exhaustion (Strike CDN Leader, loop next)
                if (extIdx >= currentExtList.length) {
                    if (cdnIdx === 0) {
                        const failUrl = CONFIG.urlTemplate.replace('{page}', i).replace('{cdn}', currentCdnList[0]).replace('{ext}', 'ALL').replace('{gid}', activeGid);
                        self.updatePriorityWithConsensus(currentCdnList, currentCdnList[cdnIdx], 'cdn', failUrl, true);
                    }
                    tryMatrix(cdnIdx + 1, 0);
                    return;
                }

                const img = new Image();
                let isFinished = false;

                const targetUrl = CONFIG.urlTemplate
                    .replace('{page}', i)
                    .replace('{cdn}', currentCdnList[cdnIdx])
                    .replace('{ext}', currentExtList[extIdx])
                    .replace('{gid}', activeGid);

                // Setup Timeout watchdog
                const timer = setTimeout(() => {
                    if (!isFinished) {
                        isFinished = true;
                        img.src = "";
                        if (cdnIdx === 0) self.updatePriorityWithConsensus(currentCdnList, currentCdnList[cdnIdx], 'cdn', targetUrl, true);
                        tryMatrix(cdnIdx + 1, 0);
                    }
                }, CONFIG.loadTimeout);

                // Success Handler
                img.onload = () => {
                    if (isFinished) return;
                    isFinished = true;
                    clearTimeout(timer);
                    self.activeLoadCount--;
                    container.style.minHeight = '0'; // Collapse placeholder height

                    self.updatePriorityWithConsensus(currentCdnList, currentCdnList[cdnIdx], 'cdn', targetUrl);
                    self.updatePriorityWithConsensus(currentExtList, currentExtList[extIdx], 'ext', targetUrl);

                    container.innerHTML = '';
                    container.appendChild(img);
                };

                // Error Handler
                img.onerror = () => {
                    if (isFinished) return;
                    isFinished = true;
                    clearTimeout(timer);

                    if (extIdx === 0) {
                        self.updatePriorityWithConsensus(currentExtList, currentExtList[extIdx], 'ext', targetUrl, true);
                    }
                    tryMatrix(cdnIdx, extIdx + 1);
                };

                img.src = targetUrl;
            };

            tryMatrix(0, 0);
        }
    };


    // =====================================================================
    // --- 6. CORE WATCHDOG & SPA ROUTER ---
    // Controls the script injection loop and React/Vue SPA lifecycle handling.
    // =====================================================================
    let isWaitingForData = false;

    function initLongStrip() {
        const pCfg = CONFIG.pagesSelector;
        const lastAtP = pCfg.lastIndexOf('@');
        const pSelector = lastAtP !== -1 ? pCfg.substring(0, lastAtP) : pCfg;
        const pagesEl = $(pSelector).first();
        const totalPages = parseInt((lastAtP !== -1 ? (pagesEl.attr(pCfg.substring(lastAtP + 1)) || "") : pagesEl.text()).replace(/\D/g, ''));

        const ic = $(CONFIG.icSelector);
        if (ic.length === 0 || isNaN(totalPages)) { isWaitingForData = true; return; }

        if ($("#scroller-initialized").length > 0) return;
        isWaitingForData = false;
        ic.append('<div id="scroller-initialized" style="display:none;"></div>');

        // Apply Global Image Fit Mode Styling (Targets ALL child images in the container)
        $('#scroll-fit-mode-style').remove();
        const fitMode = CONFIG.imageFitMode || 'width';
        let fitCss = "";
        if (fitMode === 'width') {
            fitCss = "width: 100% !important; height: auto !important; max-width: 100vw !important;";
        } else if (fitMode === 'height') {
            fitCss = "height: 100vh !important; width: auto !important; max-width: 100vw !important; object-fit: contain !important;";
        } else if (fitMode === 'smart') {
            fitCss = "max-width: 100vw !important; max-height: 100vh !important; width: auto !important; height: auto !important; object-fit: contain !important;";
        }
        $('<style id="scroll-fit-mode-style">').text(`${CONFIG.icSelector} img { ${fitCss} display: block; margin: 0 auto; }`).appendTo('head');

        // Blueprint Extraction Phase
        const iCfg = CONFIG.firstImgSelector;
        const lastAtI = iCfg.lastIndexOf('@');
        const fSelector = lastAtI !== -1 ? iCfg.substring(0, lastAtI) : iCfg;
        const fEl = $(fSelector).first();
        const firstSrc = lastAtI !== -1 ? fEl.attr(iCfg.substring(lastAtI + 1)) : fEl.text();

        let currentPage = 1;

        if (firstSrc && CONFIG.urlTemplate) {
            try {
                // Dynamically build the extraction regex from the URL Template
                const escapedTemplate = CONFIG.urlTemplate.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
                const regexString = escapedTemplate
                    .replace(/^https?:\\\/\\\//i, 'https?:\\/\\/') // Allow flexible http:// or https:// matching
                    .replace(/\\{cdn\\}/g, '(?<cdn>.+)')
                    .replace(/\\{gid\\}/g, '(?<gid>.+)')
                    .replace(/\\{page\\}/g, '(?<page>\\d+)')
                    .replace(/\\{ext\\}/g, '(?<ext>\\w+)');

                const match = firstSrc.match(new RegExp(regexString));
                if (match && match.groups) {
                    const { cdn, gid, page: extractedPage, ext } = match.groups;
                    currentPage = parseInt(extractedPage) || 1;

                    if (gid && gid !== lastProcessedGid) {
                        activeGid = gid;
                        lastProcessedGid = gid;

                        if (cdn) NETWORK_ENGINE.updatePriorityWithConsensus(currentCdnList, cdn, 'cdn', "", false, true);
                        if (ext) NETWORK_ENGINE.updatePriorityWithConsensus(currentExtList, ext, 'ext', "", false, true);

                        console.log(`[Blueprint] Current CDN Order:`, currentCdnList);
                        console.log(`[Blueprint] Current EXT Order:`, currentExtList);
                    }
                }
            } catch (e) { console.error("Regex Matching Fault Instance", e); }
        }

        const queuedPages = new Set();

        // Inject UI Placeholders
        for (let i = currentPage + 1; i <= totalPages; i++) {
            ic.append(UI_ASSETS.buildPlaceholderHtml(i, CONFIG.imageGap));
        }

        // Initialize Native Lazy Loader
        const observer = new IntersectionObserver((entries) => {
            entries.forEach(entry => {
                if (entry.isIntersecting) {
                    const pNum = parseInt(entry.target.getAttribute('data-page'));
                    for (let j = pNum; j <= Math.min(pNum + CONFIG.preloadCount, totalPages); j++) {
                        if (j > 0 && !queuedPages.has(j)) {
                            queuedPages.add(j);
                            NETWORK_ENGINE.loadImage(j, document.getElementById(`p-con-${j}`));
                        }
                    }
                }
            });
        }, { rootMargin: '1000px' });

        document.querySelectorAll('.strip-page').forEach(div => observer.observe(div));
        FEATURE_MODULE.setupFloatingPageIndicator(currentPage, totalPages);
    }

    /**
     * Identifies proper profile via URL Regex. Avoids wiping arrays on localized DOM changes.
     */
    function selectProfileForUrl(url) {
        for (const [name, prof] of Object.entries(globalStorage.profiles)) {
            try {
                if (prof.readerUrlRegex && new RegExp(prof.readerUrlRegex).test(url)) {
                    if (activeProfileName !== name) {
                        activeProfileName = name;
                        CONFIG = prof;
                        currentCdnList = CONFIG.cdnList.split(',').map(c => c.trim());
                        currentExtList = CONFIG.allowedExtensions.split(',').map(e => e.trim().replace(/^\./, ''));
                        healthRegistry = { cdn: CONFIG.cdnHealth, ext: CONFIG.extHealth };
                    }
                    return true;
                }
            } catch (e) { } // Ignore corrupt regex maps
        }

        // Fallback default assignments
        if (!CONFIG) {
            activeProfileName = Object.keys(globalStorage.profiles)[0];
            CONFIG = globalStorage.profiles[activeProfileName];
        }
        return false;
    }

    // SPA Native History Monkey-patching
    const patchHistory = (type) => {
        const orig = history[type];
        return function() {
            const rv = orig.apply(this, arguments);
            window.dispatchEvent(new Event(type.toLowerCase()));
            return rv;
        };
    };
    history.pushState = patchHistory('pushState');
    history.replaceState = patchHistory('replaceState');

    let lastUrl = location.href;

    // Core Validation loop hook
    function triggerCheck() {
        const currentUrl = location.href;
        const isMatched = selectProfileForUrl(currentUrl);

        // Safety checkpoint for routing changes MUST happen before early exit
        if (currentUrl !== lastUrl) {
            lastUrl = currentUrl;
            $("#scroller-initialized").remove();
            $("#floating-page-indicator").remove();

            // Explicit reset of network trackers for entirely new galleries
            if (CONFIG) {
                currentCdnList = CONFIG.cdnList.split(',').map(c => c.trim());
                currentExtList = CONFIG.allowedExtensions.split(',').map(e => e.trim().replace(/^\./, ''));
                healthRegistry = { cdn: CONFIG.cdnHealth, ext: CONFIG.extHealth };
                lastProcessedGid = "";
            }
        }

        if (!isMatched) return;

        FEATURE_MODULE.cleanupUI();

        if ($("#scroller-initialized").length && !isWaitingForData) return;

        initLongStrip();
    }

    // Attach listeners
    window.addEventListener('pushstate', triggerCheck);
    window.addEventListener('replacestate', triggerCheck);
    window.addEventListener('popstate', triggerCheck);

    const spaObserver = new MutationObserver(triggerCheck);
    spaObserver.observe(document.body, { childList: true, subtree: true });

    // Initial deployment pass execution loop kick-off
    triggerCheck();
})();