All-in-One Web Scanner [BETA]

Combines scanners, persists state, stays on top, reliably appears in SPAs, with enhanced stream detection.

// ==UserScript==
// @name         All-in-One Web Scanner [BETA]
// @namespace    aio-ws-drk-beta
// @icon         https://darkie-matrix.vercel.app/scanner.png
// @supportURL   https://darkie.vercel.app/
// @version      1.7.1
// @description  Combines scanners, persists state, stays on top, reliably appears in SPAs, with enhanced stream detection.
// @author       DARKIE
// @match        *://*/*
// @exclude      *://*.youtube.com/*
// @grant        none
// @license      MIT
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    if (window.aioScannerInstanceShort) {
        console.log("AIO Scanner (Short): Initialization skipped (instance already exists).");
        return;
    }

    class AioScanner {
        // --- Constants ---
        _SK = 'aioScannerPopupState_v1';
        _Z_POP = 2147483646;
        _Z_TGL = 2147483645;
        _P_ID = 'aio-scanner-popup';
        _T_ID = 'aio-toggle-button';
        _PR_ID = 'aio-image-preview-popup';

        _STRM_MIME = {
            'application/vnd.apple.mpegurl': 'm3u8',
            'application/x-mpegurl': 'm3u8',
            'audio/mpegurl': 'm3u8',
            'video/mp4': 'mp4',
            'audio/mp4': 'mp4',
            'text/vtt': 'vtt',
            'application/x-subrip': 'srt',
            'text/plain': 'srt',
            'application/dash+xml': 'mpd',
        };
        _RGX_STRM = /["'](https?:\/\/[^"'\s]+?\.(m3u8|mp4|vtt|srt|mpd)(?:[?#][^"'\s]*)?)["']|["'](https?:\/\/[^"'\s]+?(?:hls|dash|manifest|playlist|\/video\/|\/audio\/|\/segment)[^"'\s]*?)["']/gi;

        constructor() {
            console.log("AIO Scanner (Short): Constructor called.");

            // --- Instance Variables ---
            this._urls = new Set();
            this._reqs = [];
            this._maxR = 100;
            this._lastImgScn = 0;
            this._imgScnDelay = 500;
            this._maxUrlLen = 70;
            this._prvPad = 15;
            this._prvMaxW = 300;
            this._initd = false;
            this._obs = null;
            this._lastScanTime = 0;

            // --- Configuration ---
            this._cfg = {
                ui: {
                    popup: {
                        width: '650px',
                        maxHeight: '85vh'
                    }
                },
                selectors: {
                    vtt: '#vtt-content',
                    srt: '#srt-content',
                    m3u8: '#m3u8-content',
                    mp4: '#mp4-content',
                    mpd: "#mpd-content",
                    iframe: '#iframe-content',
                    network: '#network-content',
                    image: '#image-content'
                },
                linkTypes: ['vtt', 'srt', 'm3u8', 'mp4', 'mpd']
            };

            window.allInOneScanner = this;

            // --- Debounced Functions ---
            this._debScanAll = this._dbnc(() => {
                if (this._pop && this._pop.classList.contains('aio-visible')) {
                    this._scanAll();
                }
            }, this._imgScnDelay);

            this._debEnsureUI = this._dbnc(this._ensureUI, 300);

            // --- Core Initialization Steps ---
            this._mkCSS();
            this._setupNetMon();
            this._monSetAttr();
            this._listenSPA();
            this._monBlob();

            // --- Deferred UI/Observer Setup ---
            this._waitBodyInit();
        }

        /* ==========================================================================
           Initialization Helpers
           ========================================================================== */

        _waitBodyInit() {
            if (document.body) {
                this._initUIObs();
            } else {
                const obs = new MutationObserver((muts, observer) => {
                    if (document.body) {
                        observer.disconnect();
                        this._initUIObs();
                    }
                });
                obs.observe(document.documentElement || document, {
                    childList: true
                });
            }
        }

        _initUIObs() {
            if (this._initd) return;
            this._mkUI();
            this._obsDOM();
            this._chkStUI();
            if (this._pop ?.classList.contains('aio-visible')) {
                this._scanAll();
            }
            this._initd = true;
            console.log("AIO Scanner (Short): UI Initialized and DOM Observer attached.");
        }

        /* ==========================================================================
           UI Creation and Management
           ========================================================================== */

        _mkUI() {
            this._mkTglBtn();
            this._mkPop();
            this._mkPrvPop();
            const cached = this._cacheDOM();
            if (cached) {
                this._addEvts();
                this._setupKB();
                this._setupDelEvts();
            } else {
                console.error("AIO Scanner (Short): Failed UI init - core elements missing.");
            }
        }

        _mkCSS() {
            const ZP = this._Z_POP;
            const ZT = this._Z_TGL;
            const ZPR = ZP + 1;
            const css = `
                .aio-popup {
                    position: fixed; top: 20px; right: 20px; width: ${this._cfg.ui.popup.width}; max-height: ${this._cfg.ui.popup.maxHeight};
                    background-color: rgba(35, 35, 42, 0.97); color: #e0e0e0; padding: 18px; border-radius: 14px;
                    border: 1px solid rgba(255, 255, 255, 0.1); box-shadow: 0 6px 25px rgba(0, 0, 0, 0.4);
                    font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
                    font-size: 14px; z-index: ${ZP}; display: none; overflow-y: auto; backdrop-filter: blur(8px);
                    transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out; transform: translateY(-10px); opacity: 0;
                }
                .aio-popup.aio-visible {
                    transform: translateY(0); opacity: 1; display: block;
                }
                #aio-toggle-button {
                    position: fixed; top: 20px; right: 20px; z-index: ${ZT}; padding: 8px 15px;
                    background-color: rgba(35, 35, 42, 0.95); box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
                    border: 1px solid rgba(255, 255, 255, 0.1); display: block; background-color: rgba(255, 255, 255, 0.1);
                    border: 1px solid rgba(255, 255, 255, 0.15); color: #e0e0e0; border-radius: 8px; cursor: pointer;
                    font-size: 12px; transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.1s ease; white-space: nowrap;
                }
                #aio-toggle-button:hover {
                    background-color: rgba(255, 255, 255, 0.18); border-color: rgba(255, 255, 255, 0.25);
                }
                #aio-toggle-button:active {
                    transform: scale(0.97);
                }
                 .aio-image-preview {
                    position: fixed; padding: 10px; background-color: rgba(35, 35, 42, 0.98); border: 1px solid rgba(255, 255, 255, 0.15);
                    border-radius: 8px; box-shadow: 0 5px 20px rgba(0, 0, 0, 0.5); z-index: ${ZPR}; pointer-events: none;
                    opacity: 0; transition: opacity 0.15s ease-in-out; max-width: ${this._prvMaxW}px; max-height: 300px; display: none;
                 }
                 .aio-image-preview img {
                     display: block; max-width: 100%; max-height: 280px; border-radius: 4px;
                 }
                .aio-header {
                    display: flex; justify-content: space-between; align-items: center; margin-bottom: 18px; padding-bottom: 10px;
                    border-bottom: 1px solid rgba(255, 255, 255, 0.15); margin-left: -18px; margin-right: -18px; margin-top: -18px;
                    padding-left: 18px; padding-right: 18px; padding-top: 18px;
                }
                .aio-title {
                    font-size: 17px; font-weight: 600; margin: 0; color: #fff;
                }
                .aio-controls {
                    display: flex; gap: 8px;
                }
                .aio-footer {
                    margin-top: 20px; padding-top: 10px; border-top: 1px solid rgba(255, 255, 255, 0.1);
                    text-align: center; font-size: 11px; color: rgba(255, 255, 255, 0.5);
                }
                .aio-footer .aio-heart {
                    color: #e44d4d; display: inline-block; animation: aio-heartbeat 1.5s infinite ease-in-out;
                }
                @keyframes aio-heartbeat {
                    0%, 100% { transform: scale(1); }
                    50% { transform: scale(1.2); }
                }
                .aio-button {
                    background-color: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.15); color: #e0e0e0;
                    padding: 6px 12px; border-radius: 8px; cursor: pointer; font-size: 12px;
                    transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.1s ease; white-space: nowrap;
                }
                .aio-button:hover {
                    background-color: rgba(255, 255, 255, 0.18); border-color: rgba(255, 255, 255, 0.25);
                }
                .aio-button:active {
                    transform: scale(0.97);
                }
                .aio-section {
                    margin-bottom: 20px;
                }
                .aio-section-title {
                    font-size: 15px; font-weight: 600; color: #fff; margin: 0 0 10px 0; padding-bottom: 6px;
                    border-bottom: 1px solid rgba(255, 255, 255, 0.1);
                }
                .aio-content {
                    max-height: 300px; overflow-y: auto; padding-right: 5px;
                }
                .aio-list {
                    list-style: none; padding: 0; margin: 0;
                }
                .aio-item {
                    padding: 10px 12px; margin: 6px 0; background-color: rgba(255, 255, 255, 0.04);
                    border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 8px; font-size: 12px;
                    transition: background-color 0.2s ease, border-color 0.2s ease; position: relative;
                }
                .aio-item:hover {
                    background-color: rgba(255, 255, 255, 0.08); border-color: rgba(255, 255, 255, 0.15);
                }
                .aio-item-source, .aio-network-url {
                    color: #fff; word-break: break-all; margin-bottom: 5px; display: block;
                }
                .aio-item-info, .aio-network-details {
                    color: rgba(255, 255, 255, 0.65); font-size: 11px; display: flex; gap: 10px;
                    align-items: center; flex-wrap: wrap; margin-top: 6px;
                }
                .aio-item-tag {
                    background-color: rgba(255, 255, 255, 0.1); padding: 3px 7px; border-radius: 6px;
                    font-size: 10px; white-space: nowrap;
                }
                .aio-empty {
                    color: rgba(255, 255, 255, 0.6); font-style: italic; font-size: 13px;
                    text-align: center; padding: 20px 0;
                }
                .aio-filter {
                    width: 100%; box-sizing: border-box; padding: 9px 12px; margin-bottom: 12px;
                    background: rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.1);
                    border-radius: 8px; color: #fff; font-size: 13px;
                    transition: background-color 0.2s ease, border-color 0.2s ease;
                }
                .aio-filter::placeholder {
                    color: rgba(255, 255, 255, 0.5);
                }
                .aio-filter:focus {
                    outline: none; background: rgba(255, 255, 255, 0.12); border-color: rgba(100, 150, 255, 0.5);
                }
                .aio-link {

                }
                .aio-link .timestamp {
                    font-size: 10px; color: rgba(255, 255, 255, 0.5); margin-top: 4px; display: block;
                }
                .aio-iframe-item {
                    cursor: pointer;
                }
                .aio-network-method {
                    display: inline-block; padding: 3px 8px; border-radius: 6px; margin-right: 8px;
                    font-weight: bold; font-size: 10px; color: #fff;
                }
                .aio-network-method-get {
                    background-color: rgba(64, 156, 255, 0.3); border: 1px solid rgba(64, 156, 255, 0.5);
                }
                .aio-network-method-post {
                    background-color: rgba(50, 205, 50, 0.3); border: 1px solid rgba(50, 205, 50, 0.5);
                }
                .aio-network-method-put {
                    background-color: rgba(255, 165, 0, 0.3); border: 1px solid rgba(255, 165, 0, 0.5);
                }
                .aio-network-method-delete {
                    background-color: rgba(255, 69, 0, 0.3); border: 1px solid rgba(255, 69, 0, 0.5);
                }
                .aio-network-method-error {
                    background-color: rgba(200, 0, 0, 0.3); border: 1px solid rgba(200, 0, 0, 0.5);
                }
                .aio-network-controls {
                    margin-left: auto; display: flex; gap: 8px;
                }
                .aio-network-copy, .aio-network-show-payload {
                    background-color: rgba(147, 112, 219, 0.2); border: 1px solid rgba(147, 112, 219, 0.4);
                    color: #e0e0e0; padding: 3px 8px; border-radius: 6px; cursor: pointer; font-size: 10px;
                    transition: background-color 0.2s ease, border-color 0.2s ease; white-space: nowrap;
                }
                .aio-network-copy:hover, .aio-network-show-payload:hover {
                    background-color: rgba(147, 112, 219, 0.4); border-color: rgba(147, 112, 219, 0.6);
                }
                .aio-network-copy.copied {
                    background-color: rgba(50, 205, 50, 0.4); border-color: rgba(50, 205, 50, 0.6); color: #fff;
                }
                .aio-network-payload {
                    background-color: rgba(0, 0, 0, 0.3); padding: 10px; border-radius: 6px; margin-top: 10px;
                    font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; white-space: pre-wrap;
                    word-break: break-all; max-height: 150px; overflow-y: auto; display: none;
                    border: 1px solid rgba(255, 255, 255, 0.1);
                }
                .aio-image-item {

                }
                 .aio-image-button-group {
                    margin-left: auto; display: flex; gap: 6px;
                 }
                 .aio-image-copy, .aio-image-view, .aio-image-download {
                    border: 1px solid transparent; color: #e0e0e0; padding: 3px 8px; border-radius: 6px;
                    cursor: pointer; font-size: 10px; transition: background-color 0.2s ease, border-color 0.2s ease;
                    white-space: nowrap;
                 }
                 .aio-image-copy {
                    background-color: rgba(147, 112, 219, 0.2); border-color: rgba(147, 112, 219, 0.4);
                 }
                 .aio-image-view {
                    background-color: rgba(64, 156, 255, 0.2); border-color: rgba(64, 156, 255, 0.4);
                 }
                 .aio-image-download {
                    background-color: rgba(50, 205, 50, 0.2); border-color: rgba(50, 205, 50, 0.4);
                 }
                 .aio-image-copy:hover {
                    background-color: rgba(147, 112, 219, 0.4); border-color: rgba(147, 112, 219, 0.6);
                 }
                 .aio-image-view:hover {
                    background-color: rgba(64, 156, 255, 0.4); border-color: rgba(64, 156, 255, 0.6);
                 }
                 .aio-image-download:hover {
                    background-color: rgba(50, 205, 50, 0.4); border-color: rgba(50, 205, 50, 0.6);
                 }
                 .aio-image-copy.copied {
                    background-color: rgba(50, 205, 50, 0.4); border-color: rgba(50, 205, 50, 0.6); color: #fff;
                 }
                .aio-image-truncated {
                    cursor: pointer;
                }
                .aio-image-truncated:hover {
                    text-decoration: underline;
                }
                .aio-popup::-webkit-scrollbar, .aio-content::-webkit-scrollbar {
                    width: 8px;
                }
                .aio-popup::-webkit-scrollbar-track, .aio-content::-webkit-scrollbar-track {
                    background: rgba(255, 255, 255, 0.05); border-radius: 10px;
                }
                .aio-popup::-webkit-scrollbar-thumb, .aio-content::-webkit-scrollbar-thumb {
                    background: rgba(255, 255, 255, 0.2); border-radius: 10px; border: 2px solid transparent; background-clip: content-box;
                }
                .aio-popup::-webkit-scrollbar-thumb:hover, .aio-content::-webkit-scrollbar-thumb:hover {
                    background: rgba(255, 255, 255, 0.4);
                }
             `;
            const ss = document.createElement('style');
            ss.textContent = css;
            document.head.prepend(ss);
        }

        _mkTglBtn() {
            if (document.getElementById(this._T_ID)) return;
            this._tglBtn = document.createElement('button');
            this._tglBtn.id = this._T_ID;
            this._tglBtn.title = 'Toggle Scanner (Ctrl+Shift+A)';
            this._tglBtn.textContent = '🔍';
            this._tglBtn.className = 'aio-button';
            document.body.appendChild(this._tglBtn);
        }

        _mkPop() {
            if (document.getElementById(this._P_ID)) return;
            this._pop = document.createElement('div');
            this._pop.className = 'aio-popup';
            this._pop.id = this._P_ID;
            this._pop.innerHTML = `
                <div class="aio-header">
                    <h2 class="aio-title">All-in-One Web Scanner [BETA]</h2>
                    <div class="aio-controls">
                        <button class="aio-button" id="aio-clear" title="Clear all scanned data">Clear</button>
                        <button class="aio-button" id="aio-refresh" title="Rescan the page">Refresh</button>
                        <button class="aio-button" id="aio-close" title="Close Scanner (Ctrl+Shift+A)">✕</button>
                    </div>
                </div>
                ${this._genSecs()}
                <div class="aio-footer">
                    Made with <span class="aio-heart">❤️</span> by DARKIE | v${GM_info?.script?.version || '1.7.1'}
                </div>
            `;
            document.body.appendChild(this._pop);
        }

        _genSecs() {
            const secs = [{
                id: 'stream-section',
                title: 'Stream Files',
                contentId: 'stream-content',
                hasFilter: false,
                generator: this._genStrmSecs.bind(this)
            }, {
                id: 'iframe-section',
                title: 'Iframe Sources',
                contentId: 'iframe-content',
                hasFilter: false,
                generator: this._genEmpty.bind(this, 'iframes')
            }, {
                id: 'network-section',
                title: 'Network Requests',
                contentId: 'network-content',
                hasFilter: true,
                filterPlaceholder: 'Filter requests by URL...',
                generator: this._genEmpty.bind(this, 'network requests')
            }, {
                id: 'image-section',
                title: 'Image Sources',
                contentId: 'image-content',
                hasFilter: true,
                filterPlaceholder: 'Filter images by URL...',
                generator: this._genEmpty.bind(this, 'images')
            }, ];
            return secs.map(s => `
                <div class="aio-section" id="${s.id}">
                    <h3 class="aio-section-title">${s.title}</h3>
                    ${s.hasFilter ? `<input type="text" class="aio-filter" id="${s.contentId}-filter" placeholder="${s.filterPlaceholder}">` : ''}
                    <div class="aio-content" id="${s.contentId}">
                        ${s.generator()}
                    </div>
                </div>
            `).join('');
        }

        _genStrmSecs() {
            return this._cfg.linkTypes.map(t => `
                <div class="aio-subsection" id="${t}-section">
                    <h4 class="aio-section-subtitle" style="font-size: 13px; margin-bottom: 5px; color: #ccc;">${t.toUpperCase()}</h4>
                    <div class="aio-list-container" id="${t}-content">
                         <div class="aio-empty">No ${t.toUpperCase()} files detected yet...</div>
                    </div>
                </div>
            `).join('');
        }

        _mkPrvPop() {
            if (document.getElementById(this._PR_ID)) return;
            this._prvPop = document.createElement('div');
            this._prvPop.className = 'aio-image-preview';
            this._prvPop.id = this._PR_ID;
            this._prvPop.innerHTML = '<img src="" alt="Preview">';
            document.body.appendChild(this._prvPop);
        }

        _cacheDOM() {
            this._pop = document.getElementById(this._P_ID);
            this._tglBtn = document.getElementById(this._T_ID);
            this._prvPop = document.getElementById(this._PR_ID);
            if (!this._pop || !this._tglBtn || !this._prvPop) {
                return false;
            }
            this._clrBtn = this._pop.querySelector('#aio-clear');
            this._clsBtn = this._pop.querySelector('#aio-close');
            this._refBtn = this._pop.querySelector('#aio-refresh');
            this._netFltIn = this._pop.querySelector('#network-content-filter');
            this._imgFltIn = this._pop.querySelector('#image-content-filter');
            return true;
        }

        _addEvts() {
            this._clsBtn ?.addEventListener('click', () => this._hidePop());
            this._clrBtn ?.addEventListener('click', () => this._clrAll());
            this._refBtn ?.addEventListener('click', () => this._scanAll());
            this._tglBtn ?.addEventListener('click', () => this._tglPop());
            this._netFltIn ?.addEventListener('input', this._dbnc(this._hdlNetFlt.bind(this), 300));
            this._imgFltIn ?.addEventListener('input', this._dbnc(this._hdlImgFlt.bind(this), 300));
        }

        _setupKB() {
            if (window.aioScannerKBLstnr) return;
            document.addEventListener('keydown', (e) => {
                if (e.ctrlKey && e.shiftKey && (e.key === 'A' || e.key === 'a')) {
                    e.preventDefault();
                    this._tglPop();
                }
            });
            window.aioScannerKBLstnr = true;
        }

        _setupDelEvts() {
            if (!this._pop || window.aioScannerDelLstnr) return;

            this._pop.addEventListener('click', (evt) => {
                const sl = evt.target.closest('.aio-link');
                if (sl) {
                    this._hdlStrmCp(sl);
                    return;
                }
                const ifi = evt.target.closest('.aio-iframe-item');
                if (ifi) {
                    this._hdlIfrCp(ifi);
                    return;
                }
                const ni = evt.target.closest('.aio-network-item');
                if (ni) {
                    if (evt.target.closest('.aio-network-copy')) {
                        this._hdlNetCp(evt.target.closest('.aio-network-copy'));
                    } else if (evt.target.closest('.aio-network-show-payload')) {
                        this._hdlNetPTgl(evt.target.closest('.aio-network-show-payload'));
                    }
                    return;
                }
                const imi = evt.target.closest('.aio-image-item');
                if (imi) {
                    if (evt.target.closest('.aio-image-copy')) {
                        this._hdlImgCp(evt.target.closest('.aio-image-copy'));
                    } else if (evt.target.closest('.aio-image-view')) {
                        this._hdlImgVw(evt.target.closest('.aio-image-view'));
                    } else if (evt.target.closest('.aio-image-download')) {
                        this._hdlImgDl(evt.target.closest('.aio-image-download'));
                    } else if (evt.target.closest('.aio-image-truncated')) {
                        this._hdlImgTrunClk(evt.target.closest('.aio-image-truncated'));
                    }
                    return;
                }
            });

            const imgCont = this._pop.querySelector(this._cfg.selectors.image);
            if (imgCont) {
                imgCont.addEventListener('mouseover', (evt) => {
                    const imi = evt.target.closest('.aio-image-item');
                    if (imi) {
                        this._hdlImgPrvShow(imi);
                    }
                });
                imgCont.addEventListener('mouseout', (evt) => {
                    const imi = evt.target.closest('.aio-image-item');
                    const rt = evt.relatedTarget;
                    if (imi && !imi.contains(rt) && !this._prvPop ?.contains(rt)) {
                        this._hdlImgPrvHide();
                    }
                });
            }
            window.aioScannerDelLstnr = true;
        }

        // --- Event Handlers ---

        _hdlStrmCp(el) {
            const u = el.dataset.url;
            if (u) {
                this._cpHlp(u, el);
            }
        }

        async _hdlIfrCp(el) {
            const fs = el.dataset.src;
            if (fs) {
                this._cpHlp(fs, el);
            } else {
                console.warn("AIO Scanner (Short): Could not find data-src on iframe item:", el);
                this._flashItm(el, false);
            }
        }

        async _hdlNetCp(btn) {
            const i = btn.dataset.index;
            if (i !== undefined && this._reqs[i]) {
                const u = this._reqs[i].url;
                const ok = await this._cpClip(u);
                this._flashBtn(btn, ok, 'Copied!', 'Copy URL');
            }
        }

        _hdlNetPTgl(btn) {
            const i = btn.dataset.index;
            if (i !== undefined && this._pop) {
                const pDiv = this._pop.querySelector(`#payload-${i}`);
                if (pDiv) {
                    const v = pDiv.style.display === 'block';
                    pDiv.style.display = v ? 'none' : 'block';
                    btn.textContent = v ? 'Payload' : 'Hide';
                }
            }
        }

        async _hdlImgCp(btn) {
            const itm = btn.closest('.aio-image-item');
            if (itm) {
                const s = itm.dataset.src;
                const ok = await this._cpClip(s);
                this._flashBtn(btn, ok, 'Copied!', 'Copy');
            }
        }

        _hdlImgVw(btn) {
            const itm = btn.closest('.aio-image-item');
            if (itm) {
                const s = itm.dataset.src;
                window.open(s, '_blank');
            }
        }

        _hdlImgDl(btn) {
            const itm = btn.closest('.aio-image-item');
            if (itm) {
                const s = itm.dataset.src;
                const fn = s.split('/').pop().split(/[?#]/)[0] || 'image';
                this._dlImg(s, fn);
            }
        }

        _hdlImgTrunClk(srcEl) {
            const fu = srcEl.title;
            if (srcEl.textContent === fu) {
                srcEl.textContent = this._truncUrl(fu);
            } else {
                srcEl.textContent = fu;
            }
        }

        _hdlImgPrvShow(itm) {
            if (!this._prvPop) return;
            const s = itm.dataset.src;
            const prv = this._prvPop;
            const img = prv.querySelector('img');
            img.src = s;

            const r = itm.getBoundingClientRect();
            const pW = this._prvMaxW;
            const pL = r.left - pW - this._prvPad;

            if (pL >= 0) {
                prv.style.left = `${pL}px`;
                prv.style.right = 'auto';
            } else {
                const pR = r.right + this._prvPad;
                if (pR + pW <= window.innerWidth) {
                    prv.style.left = `${pR}px`;
                    prv.style.right = 'auto';
                } else {
                    prv.style.left = `${this._prvPad}px`;
                    prv.style.right = 'auto';
                }
            }

            const pT = r.top;
            const vh = window.innerHeight;
            const pH = prv.offsetHeight || 200;
            if (pT + pH > vh - this._prvPad) {
                prv.style.top = `${Math.max(0, vh - pH - this._prvPad)}px`;
            } else {
                prv.style.top = `${Math.max(0, pT)}px`;
            }

            prv.style.display = 'block';
            requestAnimationFrame(() => prv.style.opacity = '1');
        }

        _hdlImgPrvHide() {
            if (!this._prvPop) return;
            this._prvPop.style.opacity = '0';
            setTimeout(() => {
                if (this._prvPop && this._prvPop.style.opacity === '0') {
                    this._prvPop.style.display = 'none';
                }
            }, 200);
        }

        _hdlNetFlt(evt) {
            if (!this._pop) return;
            const f = evt.target.value.toLowerCase();
            const nc = this._pop.querySelector(this._cfg.selectors.network);
            if (!nc) return;
            nc.querySelectorAll('.aio-network-item').forEach(itm => {
                const ue = itm.querySelector('.aio-network-url');
                const u = ue ? (ue.title || ue.textContent).toLowerCase() : '';
                itm.style.display = u.includes(f) ? 'block' : 'none';
            });
        }

        _hdlImgFlt(evt) {
            if (!this._pop) return;
            const f = evt.target.value.toLowerCase();
            const ic = this._pop.querySelector(this._cfg.selectors.image);
            if (!ic) return;
            ic.querySelectorAll('.aio-image-item').forEach(itm => {
                const s = itm.dataset.src ? itm.dataset.src.toLowerCase() : '';
                itm.style.display = s.includes(f) ? 'flex' : 'none';
            });
        }

        // --- State Management & SPA Handling ---

        _svSt(isOpen) {
            try {
                sessionStorage.setItem(this._SK, JSON.stringify({
                    isOpen
                }));
            } catch (e) {
                console.warn("AIO Scanner: Failed to save state to sessionStorage.", e);
            }
        }

        _ldSt() {
            try {
                const st = sessionStorage.getItem(this._SK);
                return st ? JSON.parse(st) : {
                    isOpen: false
                };
            } catch (e) {
                console.warn("AIO Scanner: Failed to load state from sessionStorage. Resetting.", e);
                sessionStorage.removeItem(this._SK);
                return {
                    isOpen: false
                };
            }
        }

        _chkStUI() {
            if (!this._pop || !this._tglBtn) {
                this._ensureUI();
                return;
            }
            const st = this._ldSt();
            const shouldVis = st.isOpen;
            const isVis = this._pop.classList.contains('aio-visible');

            if (shouldVis && !isVis) {
                this._showPop();
            } else if (!shouldVis && isVis) {
                this._hidePop();
            } else if (!shouldVis && !isVis) {
                this._pop.style.display = 'none';
                this._tglBtn.style.display = 'block';
                if (this._prvPop) {
                    this._prvPop.style.display = 'none';
                    this._prvPop.style.opacity = '0';
                }
            }
        }

        _listenSPA() {
            if (window.aioScannerNavLstnr) return;

            const navHdl = () => {
                this._ensureUI();
                setTimeout(() => this._chkStUI(), 50);
                setTimeout(() => this._scanIfNeed(), 150);
            };

            window.addEventListener('popstate', navHdl);
            window.addEventListener('hashchange', navHdl);

            const self = this;

            function wrapHist(m) {
                const orig = history[m];
                if (!orig) return;
                try {
                    history[m] = function(st, t, u) {
                        let res;
                        try {
                            res = orig.apply(this, arguments);
                        } catch (e) {
                            console.error(`AIO Scanner: Error in original history.${m}`, e);
                            throw e;
                        }
                        try {
                            const evt = new CustomEvent('historystatechange', {
                                detail: {
                                    url: u,
                                    state: st,
                                    title: t,
                                    method: m
                                }
                            });
                            window.dispatchEvent(evt);
                        } catch (e) {
                            console.warn("AIO Scanner: CustomEvent dispatch failed, using direct handler.", e);
                            navHdl();
                        }
                        return res;
                    };
                } catch (e) {
                    console.error(`AIO Scanner: Failed to wrap history.${m}`, e);
                }
            }

            wrapHist('pushState');
            wrapHist('replaceState');
            window.addEventListener('historystatechange', navHdl);

            window.aioScannerNavLstnr = true;
            console.log("AIO Scanner (Short): SPA Navigation listener attached.");
        }

        _scanIfNeed() {
            const popEl = document.getElementById(this._P_ID);
            if (popEl ?.classList.contains('aio-visible')) {
                this._scanAll();
            }
        }

        _tglPop() {
            if (!this._pop) {
                this._ensureUI();
                return;
            }
            const isVis = this._pop.classList.contains('aio-visible');
            if (isVis) {
                this._hidePop();
            } else {
                this._showPop();
                this._scanAll();
            }
        }

        _showPop() {
            if (!this._pop || !this._tglBtn) {
                this._ensureUI();
                return;
            }
            this._pop.style.display = 'block';
            requestAnimationFrame(() => {
                requestAnimationFrame(() => {
                    if (this._pop) this._pop.classList.add('aio-visible');
                });
            });
            this._tglBtn.style.display = 'none';
            this._svSt(true);
        }

        _hidePop() {
            if (!this._pop || !this._tglBtn) {
                this._ensureUI();
                return;
            }
            this._pop.classList.remove('aio-visible');
            setTimeout(() => {
                if (this._pop) this._pop.style.display = 'none';
                if (this._prvPop) {
                    this._prvPop.style.display = 'none';
                    this._prvPop.style.opacity = '0';
                }
                if (this._tglBtn) this._tglBtn.style.display = 'block';
            }, 200);
            this._svSt(false);
        }

        _obsDOM() {
            if (this._obs) {
                this._obs.disconnect();
            }
            if (!document.body) {
                console.warn("AIO Scanner: Cannot observe DOM, document.body not ready.");
                return;
            }

            try {
                this._obs = new MutationObserver(muts => {
                    let changed = false;
                    let removed = false;
                    for (const m of muts) {
                        if (m.type === 'childList') {
                            if (m.addedNodes.length > 0) {
                                changed = true;
                                m.addedNodes.forEach(n => {
                                    if (n.nodeType === Node.ELEMENT_NODE) {
                                        this._scanElStrms(n);
                                    }
                                });
                            }
                            if (m.removedNodes.length > 0) {
                                for (const n of m.removedNodes) {
                                    if (n.id === this._P_ID || n.id === this._T_ID || n.id === this._PR_ID) {
                                        removed = true;
                                        break;
                                    }
                                }
                            }
                        } else if (m.type === 'attributes') {
                            changed = true;
                            const tgt = m.target;
                            const attr = m.attributeName;
                            if (tgt && attr) {
                                const val = tgt.getAttribute(attr);
                                if (typeof val === 'string' && ['src', 'href', 'data', 'data-src', 'data-url', 'style'].includes(attr.toLowerCase())) {
                                    if (attr.toLowerCase() === 'style') {
                                        this._debScanAll();
                                    } else {
                                        this._hdlStrmUrl(val, `mutAttr-${attr}`);
                                    }
                                }
                            }
                        }
                        if (removed) break;
                    }

                    if (removed) {
                        console.warn("AIO Scanner: UI element removed, attempting to re-initialize.");
                        this._ensureUI();
                    } else {
                        this._debEnsureUI();
                    }
                });

                this._obs.observe(document.body, {
                    childList: true,
                    subtree: true,
                    attributes: true,
                    attributeFilter: ['src', 'style', 'data-src', 'data-url', 'href', 'data']
                });
            } catch (e) {
                console.error("AIO Scanner: Failed to create or attach MutationObserver.", e);
            }
        }

        _ensureUI() {
            const tglMiss = !document.getElementById(this._T_ID);
            const popMiss = !document.getElementById(this._P_ID);
            const prvMiss = !document.getElementById(this._PR_ID);

            if (tglMiss || popMiss || prvMiss) {
                console.log("AIO Scanner: UI element missing, re-initializing UI.");
                this._initd = false;
                this._initUIObs();
                this._chkStUI();
            }
        }

        /* ==========================================================================
           Scanning Logic
           ========================================================================== */

        _scanAll() {
            const now = Date.now();
            if (this._lastScanTime && (now - this._lastScanTime < 200)) {
                return;
            }
            this._lastScanTime = now;

            try {
                this._scanVidSrc();
                this._scanIfr();
                this._updNetDisp();
                this._scanImgs();
                this._scanInline();
            } catch (err) {
                console.error("AIO Scanner: Error during full scan:", err);
            }
        }

        _scanVidSrc() {
            try {
                document.querySelectorAll(
                    'video, source, track, object, embed, a, [data-src], [data-url], [data-stream-url], [data-hls-url], [data-mp4-url]'
                ).forEach(el => {
                    const urls = [
                        el.src,
                        el.currentSrc,
                        el.href,
                        el.data,
                        el.dataset ?.src,
                        el.dataset ?.url,
                        el.getAttribute('data-stream-url'),
                        el.getAttribute('data-hls-url'),
                        el.getAttribute('data-mp4-url')
                    ].filter(Boolean);

                    urls.forEach(u => {
                        if (typeof u === 'string') {
                            this._hdlStrmUrl(u, `tag-${el.tagName.toLowerCase()}`);
                        }
                    });

                    if (el.tagName === 'TRACK' && el.default && el.src) {
                        this._hdlStrmUrl(el.src, 'tag-track-default');
                    }
                });
            } catch (err) {
                console.error("AIO Scanner: Error scanning video/source elements:", err);
            }
        }

        _isPotStrmUrl(u) {
            if (typeof u !== 'string') return false;
            const lu = u.toLowerCase();
            const cu = lu.split(/[?#]/)[0];

            if (/\.(m3u8|mp4|vtt|srt|mpd)$/i.test(cu)) return true;
            if (/\/hls\/|\/dash\/|\/manifest\/|\/playlist\.m3u8|\/master\.m3u8|\/video\.mp4|\.mpd/i.test(lu)) return true;
            if (/[?&](format|type|ext)=(m3u8|mp4|vtt|srt|mpd)/i.test(lu)) return true;
            if (lu.startsWith('blob:')) return true;

            return false;
        }

        _detStrmTyp(u, m = null) {
            if (typeof u !== 'string') return null;
            const lu = u.toLowerCase();

            if (m) {
                const lm = m.toLowerCase().split(';')[0].trim();
                if (this._STRM_MIME[lm]) {
                    return this._STRM_MIME[lm];
                }
            }

            for (const t of [...this._cfg.linkTypes, 'mpd']) {
                if (lu.includes(`.${t}`)) {
                    return t;
                }
            }

            if (lu.includes('m3u8') || lu.includes('/hls/') || lu.includes('/manifest/')) return 'm3u8';
            if (lu.includes('.mpd') || lu.includes('/dash/')) return 'mpd';
            if (lu.includes('mp4') || lu.includes('/video/')) return 'mp4';
            if (lu.includes('vtt') || lu.includes('/caption')) return 'vtt';
            if (lu.includes('srt') || lu.includes('/subtitle')) return 'srt';
            if (lu.startsWith('blob:')) return 'mp4';

            return null;
        }

        _hdlStrmUrl(u, srcT = 'unknown', m = null) {
            if (typeof u !== 'string' || u.trim() === '' || u.startsWith('javascript:')) {
                return;
            }
            const absU = this._absUrl(u);
            if (!absU) return;

            if (!this._isPotStrmUrl(absU)) {
                return;
            }

            const type = this._detStrmTyp(absU, m);
            if (!type || !this._cfg.linkTypes.includes(type)) {
                return;
            }

            const clnU = this._clnUrl(absU);
            if (!this._urls.has(clnU)) {
                this._urls.add(clnU);
                this._addStrmLnk(type, absU);
            }
        }

        _scanInline() {
            try {
                document.querySelectorAll('script').forEach(scr => {
                    if (!scr.hasAttribute('src') && scr.textContent) {
                        const cont = scr.textContent;
                        let m;
                        this._RGX_STRM.lastIndex = 0;
                        while ((m = this._RGX_STRM.exec(cont)) !== null) {
                            const fUrl = m[1] || m[3];
                            if (fUrl) {
                                this._hdlStrmUrl(fUrl, 'script');
                            }
                        }
                    }
                });
            } catch (err) {
                console.error("AIO Scanner: Error scanning inline scripts:", err);
            }
        }

        _monBlob() {
            if (window.aioBlobMon || typeof URL === 'undefined' || !URL.createObjectURL) return;

            const self = this;
            const origC = URL.createObjectURL;

            try {
                URL.createObjectURL = function(obj) {
                    const res = origC.apply(this, arguments);
                    if (obj instanceof Blob && obj.type) {
                        const lt = obj.type.toLowerCase();
                        const st = Object.entries(self._STRM_MIME).find(([m]) => lt.startsWith(m)) ?. [1];
                        if (st) {
                            // Blob found, potentially useful for debugging, but rely on src assignment
                        }
                    }
                    return res;
                };
                window.aioBlobMon = true;
                console.log("AIO Scanner (Short): Blob URL monitor attached.");
            } catch (err) {
                console.error("AIO Scanner: Failed to monitor Blob URLs, reverting.", err);
                URL.createObjectURL = origC;
            }
        }

        _scanElStrms(el) {
            if (!el || typeof el.querySelectorAll !== 'function') return;

            try {
                const urlsS = [
                    el.src,
                    el.href,
                    el.data,
                    el.dataset ?.src,
                    el.dataset ?.url,
                    el.getAttribute('data-stream-url')
                ].filter(Boolean);
                urlsS.forEach(u => this._hdlStrmUrl(u, `added-${el.tagName.toLowerCase()}`));

                el.querySelectorAll(
                    'video, source, track, object, embed, a, [data-src], [data-url], [data-stream-url]'
                ).forEach(ch => {
                    const urlsC = [
                        ch.src,
                        ch.currentSrc,
                        ch.href,
                        ch.data,
                        ch.dataset ?.src,
                        ch.dataset ?.url,
                        ch.getAttribute('data-stream-url')
                    ].filter(Boolean);
                    urlsC.forEach(u => this._hdlStrmUrl(u, `added-child-${ch.tagName.toLowerCase()}`));
                    if (ch.tagName === 'TRACK' && ch.default && ch.src) {
                        this._hdlStrmUrl(ch.src, 'added-track-default');
                    }
                });
            } catch (err) {
                console.error("AIO Scanner: Error scanning added element streams:", err);
            }
        }

        _scanIfr() {
            try {
                const ifrs = Array.from(document.getElementsByTagName('iframe'));
                const ifrInfos = ifrs.map(ifr => this._extIfrInf(ifr));

                const embs = Array.from(document.getElementsByTagName('embed'));
                const objs = Array.from(document.getElementsByTagName('object'));
                const frms = Array.from(document.getElementsByTagName('frame'));

                const addSrcs = [...embs, ...objs, ...frms]
                    .map(el => ({
                        src: el.src || el.data,
                        type: el.tagName.toLowerCase(),
                        attributes: []
                    }))
                    .filter(inf => inf.src);

                const allSrcs = [...ifrInfos, ...addSrcs]
                    .map(inf => ({ ...inf,
                        src: this._absUrl(inf.src)
                    }))
                    .filter(inf => inf.src && !inf.src.startsWith('about:blank') && !inf.src.startsWith('javascript:'));

                const uniqSrcs = Array.from(new Map(allSrcs.map(itm => [itm.src, itm])).values());

                const cont = this._pop ?.querySelector(this._cfg.selectors.iframe);
                if (cont) {
                    cont.innerHTML = uniqSrcs.length > 0 ?
                        this._genIfrList(uniqSrcs) :
                        this._genEmpty('iframes');
                }
            } catch (err) {
                console.error("AIO Scanner: Error scanning iframes/embeds:", err);
            }
        }

        _extIfrInf(ifr) {
            let s = ifr.src || ifr.dataset.src;
            let t = ifr.src ? 'direct' : (ifr.dataset.src ? 'lazy' : 'unknown');
            if (!s && ifr.srcdoc) {
                s = 'srcdoc';
                t = 'srcdoc';
            }
            const a = [];
            if (ifr.allow) a.push('allow: ' + ifr.allow.substring(0, 30) + (ifr.allow.length > 30 ? '...' : ''));
            if (ifr.sandbox) a.push('sandbox');
            if (ifr.loading) a.push('loading: ' + ifr.loading);
            if (ifr.name) a.push('name: ' + ifr.name);
            if (ifr.width || ifr.height) a.push(`size: ${ifr.width || '?'}x${ifr.height || '?'}`);

            return {
                src: s || '',
                type: t,
                attributes: a
            };
        }

        _scanImgs() {
            try {
                const imgs = Array.from(document.querySelectorAll('img'));
                const imgInfos = imgs
                    .map(img => this._extImgInf(img))
                    .filter(inf => inf.src && !inf.src.startsWith('data:'));

                const els = document.getElementsByTagName('*');
                const bgImgs = Array.from(els)
                    .map(el => {
                        try {
                            const st = window.getComputedStyle(el);
                            if (!st || !st.backgroundImage || st.backgroundImage === 'none') {
                                return [];
                            }
                            const urls = (st.backgroundImage.match(/url\(['"]?(.*?)['"]?\)/g) || [])
                                .map(m => m.replace(/url\(['"]?(.*?)['"]?\)/g, '$1'))
                                .filter(u => u && !u.startsWith('data:'));

                            return urls.map(u => ({
                                src: this._absUrl(u),
                                type: 'bg',
                                attributes: [`el: ${el.tagName.toLowerCase()}${el.id ? '#' + el.id : ''}${el.className && typeof el.className ==='string' ? '.' + el.className.split(' ').filter(Boolean).join('.') : ''}`]
                            }));
                        } catch (e) {
                            return [];
                        }
                    })
                    .flat()
                    .filter(Boolean);

                const combSrcs = [...imgInfos, ...bgImgs];

                const uniqSrcs = Array.from(new Map(combSrcs.map(itm => [itm.src, itm])).values())
                    .filter(itm => itm.src);

                const cont = this._pop ?.querySelector(this._cfg.selectors.image);
                if (cont) {
                    cont.innerHTML = uniqSrcs.length > 0 ?
                        this._genImgList(uniqSrcs) :
                        this._genEmpty('images');
                    if (this._imgFltIn) {
                        this._hdlImgFlt({
                            target: this._imgFltIn
                        });
                    }
                }
            } catch (err) {
                console.error("AIO Scanner: Error scanning images:", err);
            }
        }

        _extImgInf(img) {
            const s = img.currentSrc || img.src;
            const a = [];
            if (img.alt) a.push(`alt: ${img.alt.substring(0, 30)}${img.alt.length > 30 ? '...' : ''}`);
            if (img.naturalWidth > 0 || img.naturalHeight > 0) {
                a.push(`actual: ${img.naturalWidth}x${img.naturalHeight}`);
            } else if (img.width || img.height) {
                a.push(`attr: ${img.width}x${img.height}`);
            }
            if (img.loading) a.push(`loading: ${img.loading}`);
            if (img.srcset) a.push('srcset');

            const absS = this._absUrl(s);
            const ext = absS ?.split('.').pop().split(/[#?]/)[0];
            if (ext) a.push(ext.toLowerCase());

            return {
                src: absS,
                type: 'img',
                attributes: a
            };
        }

        /* ==========================================================================
           Network Monitoring
           ========================================================================== */

        _setupNetMon() {
            if (window.aioNetMon) return;

            const self = this;

            try {
                const oF = window.fetch;
                window.fetch = async function(...args) {
                    const req = args[0];
                    const opts = args[1] || {};
                    const u = typeof req === 'string' ? req : req ?.url;
                    const m = opts.method || (typeof req === 'object' ? req ?.method : 'GET') || 'GET';
                    const sT = Date.now();
                    let pld = null;

                    try {
                        if (opts.body) {
                            pld = typeof opts.body === 'string' ? opts.body : '[Non-String Body]';
                        } else if (req instanceof Request && req.body) {
                            pld = await req.clone().text().catch(() => '[Stream Body]');
                        }
                    } catch (payloadError) {
                        pld = '[Error Reading Body]';
                        console.warn("AIO Scanner: Error reading fetch request body:", payloadError);
                    }

                    self._hdlStrmUrl(u, 'net-fetch-req');

                    try {
                        const res = await oF.apply(this, args);
                        const eT = Date.now();
                        const ct = res.headers.get('Content-Type');

                        if (ct) {
                            self._hdlStrmUrl(u, 'net-fetch-resp', ct);
                        }

                        self._addNetReq({
                            url: self._absUrl(u),
                            method: m.toUpperCase(),
                            status: res.status,
                            duration: eT - sT,
                            payload: pld,
                            headers: self._hdrsToObj(res.headers),
                            timestamp: new Date().toISOString(),
                            type: 'fetch'
                        });
                        return res;
                    } catch (err) {
                        self._addNetReq({
                            url: self._absUrl(u),
                            method: m.toUpperCase(),
                            status: 'ERROR',
                            error: err.message || String(err),
                            payload: pld,
                            timestamp: new Date().toISOString(),
                            type: 'fetch'
                        });
                        throw err;
                    }
                };
            } catch (e) {
                console.error("AIO Scanner: Failed to patch window.fetch.", e);
            }

            try {
                const X = XMLHttpRequest.prototype;
                const oO = X.open;
                const oS = X.send;
                if (!oO || !oS) throw new Error("XHR methods missing");

                X.open = function(m, u) {
                    this._reqURL = self._absUrl(u);
                    this._reqMeth = m;
                    this._startT = Date.now();
                    self._hdlStrmUrl(this._reqURL, 'net-xhr-req');
                    oO.apply(this, arguments);
                };

                X.send = function(b) {
                    if (this._aioLdLstnr) {
                        this.removeEventListener('loadend', this._aioLdLstnr);
                    }

                    this._aioLdLstnr = () => {
                        const eT = Date.now();
                        let sP = null;

                        if (b) {
                            try {
                                if (b instanceof Document) sP = '[Document Payload]';
                                else if (b instanceof Blob) sP = `[Blob Payload: ${b.type}, Size: ${b.size}]`;
                                else if (b instanceof ArrayBuffer) sP = `[ArrayBuffer Payload: ${b.byteLength} bytes]`;
                                else if (b instanceof FormData) sP = '[FormData Payload]';
                                else if (typeof b === 'string') sP = b.substring(0, 5000) + (b.length > 5000 ? '...' : '');
                                else sP = '[Unknown Payload Type]';
                            } catch (e) {
                                sP = '[Error Reading Payload]';
                                console.warn("AIO Scanner: Error reading XHR send payload:", e);
                            }
                        }

                        const ct = this.getResponseHeader('Content-Type');
                        if (ct) {
                            self._hdlStrmUrl(this._reqURL, 'net-xhr-resp', ct);
                        }

                        self._addNetReq({
                            url: this._reqURL,
                            method: (this._reqMeth || 'GET').toUpperCase(),
                            status: this.status,
                            duration: eT - (this._startT || eT),
                            payload: sP,
                            headers: self._parseHdrs(this.getAllResponseHeaders()),
                            timestamp: new Date().toISOString(),
                            type: 'xhr',
                            error: (this.status < 200 || this.status >= 400) ? `HTTP ${this.status}` : (this.status === 0 && !this.response ? 'Network Error' : null)
                        });
                    };

                    this.addEventListener('loadend', this._aioLdLstnr);
                    oS.apply(this, arguments);
                };
            } catch (e) {
                console.error("AIO Scanner: Failed to patch XMLHttpRequest.", e);
            }

            window.aioNetMon = true;
            console.log("AIO Scanner (Short): Network monitoring (fetch/XHR) attached.");
        }

        _addNetReq(req) {
            if (!req || !req.url || req.url.startsWith('data:') ||
                req.url.startsWith('chrome-extension:') || req.url.startsWith('moz-extension:')) {
                return;
            }
            this._reqs.unshift(req);
            if (this._reqs.length > this._maxR) {
                this._reqs.pop();
            }
            if (this._pop ?.classList.contains('aio-visible')) {
                this._updNetDisp();
            }
        }

        _updNetDisp() {
            const cont = this._pop ?.querySelector(this._cfg.selectors.network);
            if (!cont) return;

            let list = cont.querySelector('.aio-list');
            const es = cont.querySelector('.aio-empty');

            if (this._reqs.length === 0) {
                if (!es) {
                    cont.innerHTML = this._genEmpty('network requests');
                }
                if (list) list.innerHTML = '';
                return;
            } else {
                if (!list) list = this._mkList(cont);
                if (es) es.remove();
            }

            const existingKeys = new Set(Array.from(list.children).map(li => li.dataset.requestKey));
            let added = 0;

            for (let i = 0; i < this._reqs.length; i++) {
                const r = this._reqs[i];
                const key = `${r.url}_${r.timestamp}`;
                if (!existingKeys.has(key)) {
                    const ni = this._mkNetLi(r, i, key);
                    list.prepend(ni);
                    added++;
                }
            }

            const curCnt = list.children.length;
            if (curCnt > this._maxR + 10) {
                while (list.children.length > this._maxR) {
                    list.removeChild(list.lastChild);
                }
            }

            if (added > 0 || !this._netFiltApp) {
                if (this._netFltIn) {
                    this._hdlNetFlt({
                        target: this._netFltIn
                    });
                    this._netFiltApp = true;
                }
            }
        }

        _mkNetLi(r, i, key) {
            const li = document.createElement('li');
            li.className = 'aio-item aio-network-item';
            li.dataset.requestKey = key;
            li.dataset.url = r.url;

            const methodClass = r.status === 'ERROR' || r.status >= 400 ?
                'error' :
                r.method.toLowerCase();
            const statusColor = r.status >= 400 || r.status === 'ERROR' ? '#ff8a8a' : '#a6e22e';
            const statusText = `${r.status}${r.error ? ` (${r.error.substring(0,30)}...)` : ''}`;

            li.innerHTML = `
                <div>
                    <span class="aio-network-method aio-network-method-${methodClass}">${r.method}</span>
                    <span class="aio-network-url" title="${r.url}">${this._truncUrl(r.url)}</span>
                </div>
                <div class="aio-network-details">
                    <span>Status: <strong style="color: ${statusColor}">${statusText}</strong></span>
                    ${r.duration !== undefined ? `<span>${r.duration}ms</span>` : ''}
                    <span>${new Date(r.timestamp).toLocaleTimeString()}</span>
                    <div class="aio-network-controls">
                        ${r.payload ? `<button class="aio-network-show-payload" data-index="${i}" title="Show/Hide Request Payload">Payload</button>` : ''}
                        <button class="aio-network-copy" data-index="${i}" title="Copy URL">Copy URL</button>
                    </div>
                </div>
                ${r.payload ? `
                    <div class="aio-network-payload" id="payload-${i}">
                        <pre><code>${this._escHtml(r.payload.substring(0, 5000))} ${r.payload.length > 5000 ? '...' : ''}</code></pre>
                    </div>` : ''}
            `;
            return li;
        }


        /* ==========================================================================
           UI Updates & List Generation
           ========================================================================== */

        _addStrmLnk(type, url) {
            const cont = this._pop ?.querySelector(`#${type}-content`);
            if (!cont) return;

            const list = cont.querySelector('.aio-list') || this._mkList(cont);
            const es = cont.querySelector('.aio-empty');
            if (es) es.remove();

            const li = document.createElement('li');
            li.className = 'aio-item aio-link';
            li.dataset.url = url;
            li.title = `Click to copy: ${url}`;
            li.innerHTML = `
                <span class="aio-item-source">${this._truncUrl(url)}</span>
                <div class="aio-item-info">
                    <span class="timestamp">Detected: ${new Date().toLocaleTimeString()}</span>
                </div>
            `;
            list.prepend(li);
        }

        _genIfrList(srcs) {
            return `<ul class="aio-list">${srcs.map(inf => `
                <li class="aio-item aio-iframe-item" data-src="${this._escHtml(inf.src)}" title="Click to copy source: ${inf.src}">
                    <span class="aio-item-source" title="${inf.src}">${this._truncUrl(inf.src)}</span>
                    <div class="aio-item-info">
                        <span class="aio-item-tag">${inf.type}</span>
                        ${inf.attributes.map(a => `<span class="aio-item-tag" title="${a}">${a.substring(0, 40)}${a.length > 40 ? '...' : ''}</span>`).join('')}
                    </div>
                </li>`).join('')}</ul>`;
        }

        _genImgList(srcs) {
            return `<ul class="aio-list">${srcs.map((inf, i) => `
                <li class="aio-item aio-image-item" data-src="${inf.src}">
                    <span class="aio-item-source aio-image-truncated" title="${inf.src}">${this._truncUrl(inf.src)}</span>
                    <div class="aio-item-info">
                        <span class="aio-item-tag">${inf.type}</span>
                        ${inf.attributes.map(a => `<span class="aio-item-tag" title="${a}">${a.substring(0, 40)}${a.length > 40 ? '...' : ''}</span>`).join('')}
                        <div class="aio-image-button-group">
                            <button class="aio-image-copy" data-index="${i}" title="Copy Image URL">Copy</button>
                            <button class="aio-image-view" data-index="${i}" title="Open Image in New Tab">View</button>
                            <button class="aio-image-download" data-index="${i}" title="Download Image">Download</button>
                        </div>
                    </div>
                </li>`).join('')}</ul>`;
        }

        _genEmpty(type) {
            return `<div class="aio-empty">No ${type} found yet...</div>`;
        }

        _mkList(cont) {
            const list = document.createElement('ul');
            list.className = 'aio-list';
            cont.innerHTML = '';
            cont.appendChild(list);
            return list;
        }

        _clrAll() {
            if (!this._pop) {
                this._ensureUI();
                return;
            }
            console.log("AIO Scanner: Clearing all data.");

            this._cfg.linkTypes.forEach(t => {
                const c = this._pop.querySelector(`#${t}-content`);
                if (c) c.innerHTML = `<div class="aio-empty">No ${t.toUpperCase()} files detected yet...</div>`;
            });

            const sels = [this._cfg.selectors.iframe, this._cfg.selectors.network, this._cfg.selectors.image];
            const types = ['iframes', 'network requests', 'images'];
            sels.forEach((sel, i) => {
                const c = this._pop.querySelector(sel);
                if (c) c.innerHTML = this._genEmpty(types[i]);
            });

            this._urls.clear();
            this._reqs = [];

            if (this._netFltIn) this._netFltIn.value = '';
            if (this._imgFltIn) this._imgFltIn.value = '';
            this._netFiltApp = false;
        }


        /* ==========================================================================
           Utility Helpers
           ========================================================================== */

        _absUrl(u) {
            if (typeof u !== 'string') return null;
            if (u.startsWith('http:') || u.startsWith('https:') || u.startsWith('//') ||
                u.startsWith('data:') || u.startsWith('blob:') || u.startsWith('about:')) {
                return u;
            }
            try {
                if (u.startsWith('//')) {
                    return window.location.protocol + u;
                }
                return new URL(u, document.baseURI).href;
            } catch (e) {
                return null;
            }
        }

        _clnUrl(u) {
            if (typeof u !== 'string') return '';
            return u.split(/[?#]/)[0];
        }

        _truncUrl(u, maxL = this._maxUrlLen) {
            if (typeof u !== 'string' || u.length <= maxL) {
                return u || '';
            }

            try {
                const uo = new URL(u);
                const o = uo.origin;
                const p = uo.pathname;
                const s = uo.search ? '?...' : '';
                const h = uo.hash ? '#...' : '';

                const lo = o.length;
                const ls = s.length;
                const lh = h.length;
                const availP = maxL - lo - ls - lh - 5;

                if (availP <= 5) {
                    if (lo + ls + lh > maxL) {
                        return o.substring(0, maxL - 3) + '...';
                    }
                    return o + '/...' + s + h;
                }

                const segs = p.split('/').filter(Boolean);
                const fname = segs.pop() || '';

                if (fname.length >= availP) {
                    const half = Math.floor(availP / 2) - 1;
                    if (half > 0) {
                        const truncF = fname.substring(0, half) + '...' + fname.substring(fname.length - half);
                        return `${o}/.../${truncF}${s}${h}`;
                    } else {
                        return `${o}/.../...${s}${h}`;
                    }
                } else {
                    let truncP = fname;
                    let curLen = fname.length;
                    while (segs.length > 0) {
                        const next = segs.pop();
                        if (curLen + next.length + 1 <= availP) {
                            truncP = next + '/' + truncP;
                            curLen += next.length + 1;
                        } else {
                            break;
                        }
                    }
                    return `${o}/${segs.length > 0 ? '.../' : ''}${truncP}${s}${h}`;
                }
            } catch (e) {
                const half = Math.floor(maxL / 2) - 2;
                if (half <= 0) return u.substring(0, maxL - 3) + '...';
                const start = u.substring(0, half);
                const end = u.substring(u.length - half);
                return `${start}...${end}`;
            }
        }

        async _cpClip(txt) {
            if (!txt) return false;
            let ok = false;
            try {
                if (navigator.clipboard && window.isSecureContext) {
                    await navigator.clipboard.writeText(txt);
                    ok = true;
                } else {
                    ok = this._cpFb(txt);
                }
            } catch (err) {
                console.warn("AIO Scanner: Clipboard write failed, trying fallback.", err);
                ok = this._cpFb(txt);
            }
            return ok;
        }

        _cpFb(txt) {
            const ta = document.createElement('textarea');
            ta.value = txt;
            ta.style.position = 'fixed';
            ta.style.left = '-9999px';
            ta.style.top = '0px';
            ta.setAttribute('readonly', '');
            document.body.appendChild(ta);
            let ok = false;
            try {
                ta.select();
                ta.setSelectionRange(0, ta.value.length);
                ok = document.execCommand('copy');
            } catch (err) {
                console.error("AIO Scanner: Fallback copy (execCommand) failed.", err);
            }
            document.body.removeChild(ta);
            return ok;
        }

        async _cpHlp(txt, el) {
            const ok = await this._cpClip(txt);
            this._flashItm(el, ok);
        }

        _flashItm(el, ok) {
            if (!el || typeof el.style === 'undefined') return;
            const oBg = el.style.backgroundColor;
            const oBd = el.style.borderColor;
            el.style.transition = 'background-color 0.1s ease, border-color 0.1s ease';
            if (ok) {
                el.style.backgroundColor = 'rgba(50, 205, 50, 0.3)';
                el.style.borderColor = 'rgba(50, 205, 50, 0.5)';
            } else {
                el.style.backgroundColor = 'rgba(255, 0, 0, 0.3)';
                el.style.borderColor = 'rgba(255, 0, 0, 0.5)';
            }
            setTimeout(() => {
                if (el) {
                    el.style.transition = '';
                    el.style.backgroundColor = oBg;
                    el.style.borderColor = oBd;
                }
            }, 600);
        }

        _flashBtn(btn, ok, okTxt, origTxt) {
            if (!btn) return;
            const oCont = origTxt || btn.textContent;
            btn.disabled = true;
            btn.classList.remove('copied');

            if (ok) {
                btn.textContent = okTxt;
                btn.classList.add('copied');
            } else {
                btn.textContent = 'Error';
            }

            setTimeout(() => {
                if (btn) {
                    btn.textContent = oCont;
                    btn.classList.remove('copied');
                    btn.disabled = false;
                }
            }, 1500);
        }

        _dbnc(fn, wait) {
            let t;
            return (...args) => {
                clearTimeout(t);
                t = setTimeout(() => {
                    try {
                        fn.apply(this, args)
                    } catch (e) {
                        console.error("AIO Scanner: Error in debounced function:", e);
                    }
                }, wait);
            };
        }

        _monSetAttr() {
            if (window.aioSetAttrMon) return;
            try {
                const oS = Element.prototype.setAttribute;
                Element.prototype.setAttribute = function(n, v) {
                    const r = oS.apply(this, arguments);
                    if ((n === 'src' || n === 'style' || n.startsWith('data-')) &&
                        typeof v === 'string' && window.allInOneScanner) {
                        window.allInOneScanner._debScanAll();
                    }
                    return r;
                };
                window.aioSetAttrMon = true;
                console.log("AIO Scanner (Short): setAttribute monitor attached.");
            } catch (err) {
                console.error("AIO Scanner: Failed to monitor setAttribute.", err);
            }
        }

        _hdrsToObj(h) {
            const o = {};
            if (h && typeof h.forEach === 'function') {
                try {
                    h.forEach((v, k) => {
                        o[k] = v;
                    });
                } catch (e) {
                    console.warn("AIO Scanner: Error converting Headers object:", e);
                }
            }
            return o;
        }

        _parseHdrs(hStr) {
            const h = {};
            if (!hStr) return h;
            try {
                const ps = hStr.trim().split(/[\r\n]+/);
                for (const p of ps) {
                    const i = p.indexOf(':');
                    if (i > 0) {
                        const k = p.substring(0, i).trim().toLowerCase();
                        const v = p.substring(i + 1).trim();
                        if (h[k]) {
                            if (Array.isArray(h[k])) {
                                h[k].push(v);
                            } else {
                                h[k] = [h[k], v];
                            }
                        } else {
                            h[k] = v;
                        }
                    }
                }
            } catch (e) {
                console.warn("AIO Scanner: Error parsing header string:", e);
            }
            return h;
        }

        _escHtml(u) {
            if (!u || typeof u !== 'string') return String(u);
            return u.replace(/&/g, "&")
                .replace(/</g, "<")
                .replace(/>/g, ">")
                .replace(/"/g, '"')
                .replace(/'/g, "'");
        }

        async _dlImg(u, fn) {
            try {
                const r = await fetch(u, {
                    mode: 'cors',
                    credentials: 'omit'
                });
                if (!r.ok) throw new Error(`Fetch failed: ${r.status} ${r.statusText}`);
                const b = await r.blob();

                const bU = URL.createObjectURL(b);

                const a = document.createElement('a');
                a.href = bU;
                a.download = fn || 'image';
                document.body.appendChild(a);
                a.click();

                setTimeout(() => {
                    if (document.body.contains(a)) {
                        document.body.removeChild(a);
                    }
                    URL.revokeObjectURL(bU);
                }, 100);

            } catch (err) {
                console.error(`AIO Scanner: Image download failed for ${u}. Error: ${err.message}. Trying fallback.`);
                try {
                    const nt = window.open(u, '_blank');
                    if (!nt) {
                        alert(`Cannot open image (popup blocker?). Copy URL manually.\n\nURL: ${u}`);
                    }
                } catch (openErr) {
                    alert(`Failed to download or open image.\nURL: ${u}\nError: ${err.message}`);
                }
            }
        }

    } // End of AioScanner class

    // --- Initialization ---
    if (!window.aioScannerInstanceShort) {
        const init = () => {
            if (!window.aioScannerInstanceShort && document.body) {
                try {
                    console.log("AIO Scanner (Short): Initializing...");
                    window.aioScannerInstanceShort = new AioScanner();
                } catch (err) {
                    console.error("AIO Scanner (Short): CRITICAL INITIALIZATION ERROR:", err);
                }
            } else if (!document.body) {
                // Wait for body
            } else {
                // Instance exists
            }
        };

        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', init);
        } else {
            setTimeout(init, 50);
        }
    }

})();