Recon+

Unlocks Paywalls in recon

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         Recon+
// @namespace    https://update.greasyfork.org/scripts/582087/Recon%2B.user.js
// @description  Unlocks Paywalls in recon
// @version      1.9
// @license      MIT
// @match        https://www.recon.com/*
// @grant        unsafeWindow
// @grant        GM_xmlhttpRequest
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
// @run-at       document-start
// ==/UserScript==

(function () {
    'use strict';

    const win = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
    const profileDetailsCache = new Map();
    let lastBlockedRequest = null;
    let lastProfileDetailPageData = null;
    const authHeaders = {};

    // Complete dictionary for interest IDs
    const interestMap = {
        1: "Recon Men", 2: "Skinheads", 4: "Leather", 5: "Rubber", 6: "Sports Gear",
        7: "Military", 8: "Hoods & Masks", 9: "Muscle", 10: "Punks", 13: "Bikers",
        15: "Bondage", 18: "Suits", 19: "Fisting", 20: "Masters & Slaves", 27: "Boots",
        30: "Tattoos & Piercings", 31: "Bears", 34: "Fighting", 36: "Feet",
        37: "Pups & Handlers", 38: "Smokers", 39: "Gunge", 40: "Trackies",
        41: "Underwear", 42: "Chastity", 43: "Watersports", 44: "Impact Play",
        45: "Electro", 46: "Sneakers & Socks", 47: "ADBL"
    };

    // -------------------------------------------------------------------------
    // 1. CSS INJECTION (Hiding Angular's original elements)
    // -------------------------------------------------------------------------
    const style = document.createElement('style');
    style.innerHTML = `
        .cdk-overlay-container:has(t101-paywall), t101-paywall { display: none !important; opacity: 0 !important; pointer-events: none !important; }
        html.cdk-global-scrollblock, body { position: static !important; overflow-y: auto !important; overflow: auto !important; width: auto !important; }
        .photo-tile { pointer-events: auto !important; cursor: pointer !important; }
        .text-container .interests-container { display: none !important; }
        .text-container { height: auto !important; max-height: none !important; padding-bottom: 15px !important; }
    `;
    if (document.head) { document.head.appendChild(style); } else {
        const docObserver = new MutationObserver(() => { if (document.head) { document.head.appendChild(style); docObserver.disconnect(); } });
        docObserver.observe(document.documentElement, { childList: true });
    }

    // -------------------------------------------------------------------------
    // 2. TOAST SYSTEM
    // -------------------------------------------------------------------------
    function showToast(message, type = 'success') {
        let container = document.getElementById('custom-toast-container');
        if (!container) {
            container = document.createElement('div');
            container.id = 'custom-toast-container';
            container.style.cssText = `position: fixed; top: 20px; right: 20px; z-index: 99999; display: flex; flex-direction: column; gap: 10px; pointer-events: none;`;
            document.body.appendChild(container);
        }
        const toast = document.createElement('div');
        toast.textContent = message;
        toast.style.cssText = `background: ${type === 'error' ? '#cf2a2a' : '#2acf65'}; color: #fff; padding: 12px 24px; border-radius: 4px; font-family: sans-serif; font-weight: bold; font-size: 14px; box-shadow: 0 4px 12px rgba(0,0,0,0.3); opacity: 0; transform: translateY(-20px); transition: all 0.3s ease;`;
        container.appendChild(toast);
        setTimeout(() => { toast.style.opacity = '1'; toast.style.transform = 'translateY(0)'; }, 10);
        setTimeout(() => { toast.style.opacity = '0'; toast.style.transform = 'translateY(-20px)'; setTimeout(() => toast.remove(), 300); }, 3500);
    }

    // -------------------------------------------------------------------------
    // 3. XHR INTERCEPTOR
    // -------------------------------------------------------------------------
    const origXHR = win.XMLHttpRequest;
    win.XMLHttpRequest = function () {
        const xhr = new origXHR();
        const origOpen = xhr.open;
        const origSend = xhr.send;
        const origSetHeader = xhr.setRequestHeader;

        xhr._headers = {};
        xhr.setRequestHeader = function (header, value) {
            this._headers[header] = value;
            return origSetHeader.apply(this, arguments);
        };

        xhr.open = function (method, url) {
            this._customUrl = url;
            if (typeof url === 'string' && url.includes('/visitedProfiles/')) {
                if (!this._isManualTrigger) {
                    this.send = function (body) {
                        lastBlockedRequest = {
                            url: url,
                            method: method,
                            headers: this._headers,
                            body: body
                        };
                        Object.defineProperty(this, 'readyState', { value: 4 });
                        Object.defineProperty(this, 'status', { value: 200 });
                        Object.defineProperty(this, 'responseText', { value: '{"success":true}' });
                        if (this.onreadystatechange) this.onreadystatechange();
                        if (this.onload) this.onload();
                    };
                    return origOpen.apply(this, arguments);
                }
            }
            return origOpen.apply(this, arguments);
        };

        xhr.send = function () {
            if (this._headers) {
                const authKeys = ['authorization', 'x-xsrf-token', 'client-id', 'x-client-id'];
                for (const key of Object.keys(this._headers)) {
                    if (authKeys.includes(key.toLowerCase())) {
                        authHeaders[key] = this._headers[key];
                    }
                }
            }
            this.addEventListener('load', function () {
                if (this._customUrl && this._customUrl.includes('/api/profile/profiles/')) {
                    try {
                        const data = JSON.parse(this.responseText);
                        if (data && data.id) {
                            if (this._customUrl.includes('/detail/')) {
                                lastProfileDetailPageData = data;
                                const existing = document.getElementById('custom-profile-last-updated');
                                if (existing) existing.remove();
                                setTimeout(() => paintProfileDetailPage(data), 50);
                                saveProfileToIndexedDB(data.id, data);
                            } else {
                                profileDetailsCache.set(data.id, data);
                                setTimeout(() => paintProfileData(data.id), 50);
                            }
                        }
                    } catch (e) { console.error("[Script] Error parsing profile data", e); }
                }
            });
            return origSend.apply(this, arguments);
        };
        return xhr;
    };

    // Helper to query location from IndexedDB (RECON_DB -> App_Locations)
    function getLocationFromIndexedDB(locationId) {
        return new Promise((resolve) => {
            try {
                const request = win.indexedDB.open("RECON_DB");
                request.onsuccess = function (event) {
                    const db = event.target.result;
                    if (!db.objectStoreNames.contains("App_Locations")) {
                        db.close();
                        resolve(null);
                        return;
                    }
                    const transaction = db.transaction("App_Locations", "readonly");
                    const store = transaction.objectStore("App_Locations");

                    function checkVal(val) {
                        if (val && val.data) {
                            if (val.expiry) {
                                const expiryDate = new Date(val.expiry);
                                if (expiryDate > new Date()) {
                                    return val.data;
                                }
                            } else {
                                return val.data;
                            }
                        }
                        return null;
                    }

                    // Try querying as number first
                    const getRequest = store.get(Number(locationId));
                    getRequest.onsuccess = function () {
                        let result = checkVal(getRequest.result);
                        if (result) {
                            db.close();
                            resolve(result);
                        } else {
                            // Try querying as string
                            const getRequestStr = store.get(String(locationId));
                            getRequestStr.onsuccess = function () {
                                db.close();
                                resolve(checkVal(getRequestStr.result));
                            };
                            getRequestStr.onerror = function () {
                                db.close();
                                resolve(null);
                            };
                        }
                    };
                    getRequest.onerror = function () {
                        db.close();
                        resolve(null);
                    };
                };
                request.onerror = function () {
                    resolve(null);
                };
            } catch (e) {
                resolve(null);
            }
        });
    }

    // Helper to save location to IndexedDB (RECON_DB -> App_Locations)
    function saveLocationToIndexedDB(locationId, locationUrl, name, longName) {
        try {
            const request = win.indexedDB.open("RECON_DB");
            request.onsuccess = function (event) {
                const db = event.target.result;
                if (!db.objectStoreNames.contains("App_Locations")) {
                    db.close();
                    return;
                }
                const transaction = db.transaction("App_Locations", "readwrite");
                const store = transaction.objectStore("App_Locations");

                const expiryDate = new Date();
                expiryDate.setFullYear(expiryDate.getFullYear() + 1); // 1 year expiry

                const value = {
                    data: {
                        locationId: Number(locationId),
                        locationUrl: locationUrl,
                        longName: longName,
                        name: name
                    },
                    expiry: expiryDate.toString()
                };
                store.put(value, Number(locationId));
                transaction.oncomplete = function () {
                    db.close();
                };
            };
        } catch (e) {
            console.error("[Script] Error saving location to IndexedDB", e);
        }
    }

    // Helper to query profile from IndexedDB (RECON_DB -> Profile_Cache)
    function getProfileFromIndexedDB(profileId) {
        return new Promise((resolve) => {
            try {
                const request = win.indexedDB.open("RECON_DB");
                request.onsuccess = function (event) {
                    const db = event.target.result;
                    if (!db.objectStoreNames.contains("Profile_Cache")) {
                        db.close();
                        resolve(null);
                        return;
                    }
                    const transaction = db.transaction("Profile_Cache", "readonly");
                    const store = transaction.objectStore("Profile_Cache");

                    function checkVal(val) {
                        if (val) {
                            if (val.data) {
                                if (val.expiry) {
                                    const expiryDate = new Date(val.expiry);
                                    if (expiryDate > new Date()) {
                                        return val.data;
                                    }
                                } else {
                                    return val.data;
                                }
                            }
                            return val;
                        }
                        return null;
                    }

                    const getRequest = store.get(Number(profileId));
                    getRequest.onsuccess = function () {
                        let result = checkVal(getRequest.result);
                        if (result) {
                            db.close();
                            resolve(result);
                        } else {
                            const getRequestStr = store.get(String(profileId));
                            getRequestStr.onsuccess = function () {
                                db.close();
                                resolve(checkVal(getRequestStr.result));
                            };
                            getRequestStr.onerror = function () {
                                db.close();
                                resolve(null);
                            };
                        }
                    };
                    getRequest.onerror = function () {
                        db.close();
                        resolve(null);
                    };
                };
                request.onerror = function () {
                    resolve(null);
                };
            } catch (e) {
                resolve(null);
            }
        });
    }

    // Helper to save profile to IndexedDB (RECON_DB -> Profile_Cache)
    function saveProfileToIndexedDB(profileId, data) {
        try {
            const request = win.indexedDB.open("RECON_DB");
            request.onsuccess = function (event) {
                const db = event.target.result;
                if (!db.objectStoreNames.contains("Profile_Cache")) {
                    db.close();
                    return;
                }
                const transaction = db.transaction("Profile_Cache", "readwrite");
                const store = transaction.objectStore("Profile_Cache");

                const expiryDate = new Date();
                expiryDate.setFullYear(expiryDate.getFullYear() + 1); // 1 year expiry

                const value = {
                    data: data,
                    expiry: expiryDate.toString()
                };
                store.put(value, Number(profileId));
                transaction.oncomplete = function () {
                    db.close();
                };
            };
        } catch (e) {
            console.error("[Script] Error saving profile to IndexedDB", e);
        }
    }

    function checkAndPaintProfileFromIndexedDB() {
        const match = win.location.pathname.match(/\/members\/([^/]+)/);
        if (match) {
            const profileId = match[1];
            if (profileId && !isNaN(Number(profileId))) {
                getProfileFromIndexedDB(profileId).then(cachedProfile => {
                    if (cachedProfile) {
                        lastProfileDetailPageData = cachedProfile;
                        paintProfileDetailPage(cachedProfile);
                    }
                });
            }
        }
    }


    // -------------------------------------------------------------------------
    // 4. DOM INJECTION ENGINE
    // -------------------------------------------------------------------------
    function paintProfileData(profileId) {
        const card = document.getElementById(profileId);
        if (!card || card.dataset.reconInjected) return;
        const data = profileDetailsCache.get(profileId);
        const textContainer = card.querySelector('.text-container');
        if (!data || !textContainer) return;

        card.dataset.reconInjected = "true";

        // A. Inject Location (Using IndexedDB with Fetch fallback)
        if (data.location && data.location.locationId) {
            let locDiv = document.createElement('div');
            locDiv.className = 'custom-injected-location';
            locDiv.style.cssText = `font-family: 'Raleway', sans-serif; font-size: 13px; font-weight: 700; color: #e32222; margin: 4px 0; text-align: left;`;
            locDiv.innerHTML = `📍 Loading...`;

            const nameNode = textContainer.querySelector('.name');
            if (nameNode) nameNode.after(locDiv); else textContainer.prepend(locDiv);

            const rawLocId = data.location.locationId;
            if (isNaN(Number(rawLocId))) {
                locDiv.innerHTML = `📍 ${rawLocId}`;
            } else {
                getLocationFromIndexedDB(rawLocId).then(cachedLoc => {
                    if (cachedLoc) {
                        const locName = cachedLoc.name || cachedLoc.longName || rawLocId;
                        locDiv.innerHTML = `📍 ${locName}`;
                    } else {
                        let fetchUrl = data.location.locationUrl || `https://www.recon.com/api/location/locations/${rawLocId}`;
                        if (!fetchUrl.includes('culture=')) {
                            fetchUrl += (fetchUrl.includes('?') ? '&' : '?') + 'culture=en';
                        }
                        win.fetch(fetchUrl, {
                            headers: {
                                ...authHeaders
                            }
                        })
                            .then(res => res.json())
                            .then(locData => {
                                const name = locData.shortLocationName || locData.name || locData.longLocationName;
                                if (name) {
                                    const longName = locData.longLocationName || locData.longName || name;
                                    locDiv.innerHTML = `📍 ${name}`;
                                    saveLocationToIndexedDB(rawLocId, fetchUrl, name, longName);
                                } else {
                                    locDiv.innerHTML = `📍 Loc ID: ${rawLocId}`;
                                }
                            }).catch(() => { locDiv.innerHTML = `📍 Loc ID: ${rawLocId}`; });
                    }
                });
            }
        }

        // B. Inject Full Interests Array
        if (data.interests && data.interests.length > 0) {
            let intDiv = document.createElement('div');
            intDiv.className = 'custom-injected-interests';
            intDiv.style.cssText = `display: flex; flex-wrap: wrap; gap: 5px; margin-top: 8px; justify-content: flex-start;`;
            data.interests.forEach(id => {
                let span = document.createElement('span');
                span.style.cssText = `background-color: rgba(255, 255, 255, 0.2); color: #fff; padding: 3px 8px; border-radius: 10px; font-size: 11px; font-weight: bold;`;
                span.textContent = interestMap[id] || `Tag: ${id}`;
                intDiv.appendChild(span);
            });
            textContainer.appendChild(intDiv);
        }
    }

    function paintProfileDetailPage(data) {
        if (!data || !data.lastUpdatedDate) return;

        let container = document.querySelector('.profile-header, t101-profile-header, .member-info, .profile-info-container');
        if (!container) {
            container = document.querySelector('h1');
        }

        if (!container) {
            const cruiseBtn = document.querySelector('a[href*="/cruise"], button:has(.fa-ship), [class*="cruise"], button:has(t101-icon-cruise), button:has(t101-icon-cruise-icon), t101-icon-cruise, t101-icon-cruise-icon, button:has([class*="cruise"]), button:has([id*="cruise"])');
            if (cruiseBtn) {
                container = cruiseBtn.parentElement;
            }
        }

        if (!container) return;

        if (document.getElementById('custom-profile-last-updated')) return;

        const dateDiv = document.createElement('div');
        dateDiv.id = 'custom-profile-last-updated';
        dateDiv.style.cssText = `
            font-family: 'Raleway', sans-serif;
            font-size: 14px;
            font-weight: 700;
            color: #adadad;
            margin: 10px 0;
            display: inline-flex;
            align-items: center;
            gap: 6px;
        `;

        try {
            const dateObj = new Date(data.lastUpdatedDate);
            const formattedDate = dateObj.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
            dateDiv.innerHTML = `📅 Profile Updated: <span style="color: #fff; font-weight: 800;">${formattedDate}</span>`;
        } catch (e) {
            dateDiv.innerHTML = `📅 Profile Updated: <span style="color: #fff; font-weight: 800;">${data.lastUpdatedDate}</span>`;
        }

        if (container.tagName === 'H1') {
            container.after(dateDiv);
        } else {
            container.appendChild(dateDiv);
        }
    }

    // -------------------------------------------------------------------------
    // 5. RESTORED FEATURES (Buttons & Lightbox)
    // -------------------------------------------------------------------------
    async function fetchImageAsBlob(url) {
        try {
            const res = await win.fetch(url);
            if (res.ok) return await res.blob();
        } catch (e) {
            console.warn(`[Script] Standard fetch failed for ${url}, trying GM_xmlhttpRequest:`, e);
        }

        if (typeof GM_xmlhttpRequest !== 'undefined') {
            return new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: url,
                    responseType: 'blob',
                    onload: function (response) {
                        if (response.status >= 200 && response.status < 300) {
                            resolve(response.response);
                        } else {
                            reject(new Error(`GM_xmlhttpRequest failed with status ${response.status}`));
                        }
                    },
                    onerror: function (err) {
                        reject(err);
                    }
                });
            });
        }

        throw new Error(`Cannot fetch ${url} without CORS or GM_xmlhttpRequest`);
    }

    function getJSZip() {
        if (typeof JSZip !== 'undefined') return Promise.resolve(JSZip);
        if (win.JSZip) return Promise.resolve(win.JSZip);
        return new Promise((resolve, reject) => {
            const script = document.createElement('script');
            script.src = 'https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js';
            script.onload = () => resolve(window.JSZip || win.JSZip);
            script.onerror = () => reject(new Error('Failed to load JSZip'));
            document.head.appendChild(script);
        });
    }

    async function exportProfileData() {
        showToast('Preparing profile export...', 'success');

        let JSZipLib;
        try {
            JSZipLib = await getJSZip();
        } catch (e) {
            console.error(e);
            showToast('Failed to load JSZip library.', 'error');
            return;
        }

        const data = lastProfileDetailPageData;
        const profileId = data ? data.id : (win.location.pathname.match(/\/members\/([^/]+)/) || [])[1] || 'unknown';
        const displayName = data ? (data.displayName || data.username) : (document.title || 'Profile');

        // 1. Gather all unique photo URLs
        const photoUrls = new Set();

        // From DOM photo-tiles
        document.querySelectorAll('.photo-tile').forEach(t => {
            const bgImg = t.style.backgroundImage;
            if (bgImg) {
                const url = bgImg.replace(/^url\(["']?/, '').replace(/["']?\)$/, '').split('?')[0];
                if (url) photoUrls.add(url);
            }
        });

        // From DOM profile avatars
        document.querySelectorAll('.profile-avatar').forEach(t => {
            const bgImg = t.style.backgroundImage;
            if (bgImg) {
                const url = bgImg.replace(/^url\(["']?/, '').replace(/["']?\)$/, '').split('?')[0];
                if (url) photoUrls.add(url);
            }
        });

        // From lastProfileDetailPageData recursively
        if (data) {
            const findUrls = (obj) => {
                if (typeof obj === 'string') {
                    if (obj.startsWith('http') && (obj.includes('/photos/') || obj.includes('/images/') || obj.match(/\.(jpg|jpeg|png|webp|gif)/i))) {
                        photoUrls.add(obj.split('?')[0]);
                    }
                } else if (typeof obj === 'object' && obj !== null) {
                    for (const key in obj) {
                        if (Object.prototype.hasOwnProperty.call(obj, key)) {
                            findUrls(obj[key]);
                        }
                    }
                }
            };
            findUrls(data);
        }

        const urlsArray = Array.from(photoUrls);
        if (urlsArray.length === 0) {
            showToast('No photos found to export.', 'error');
            return;
        }

        showToast(`Found ${urlsArray.length} photos. Starting download...`, 'success');

        const zip = new JSZipLib();

        // 2. Add profile_info.txt
        let infoText = '';
        infoText += `=== RECON PROFILE EXPORT ===\n`;
        infoText += `Export Date: ${new Date().toLocaleString()}\n`;
        infoText += `Profile URL: ${win.location.href}\n\n`;

        if (data) {
            infoText += `Display Name: ${data.displayName || 'N/A'}\n`;
            infoText += `Username: ${data.username || 'N/A'}\n`;
            infoText += `Profile ID: ${data.id || 'N/A'}\n`;
            if (data.location) {
                infoText += `Location: ID ${data.location.locationId || 'N/A'}\n`;
            }
            if (data.lastUpdatedDate) {
                infoText += `Last Updated: ${data.lastUpdatedDate}\n`;
            }
            if (data.interests && data.interests.length > 0) {
                infoText += `Interests: ${data.interests.map(id => interestMap[id] || `Tag ${id}`).join(', ')}\n`;
            }

            infoText += `\n--- PROFILE INFO (RAW KEY-VALUES) ---\n`;
            for (const [key, val] of Object.entries(data)) {
                if (typeof val !== 'object' && val !== null) {
                    infoText += `${key}: ${val}\n`;
                }
            }
        }

        // Gather bio and details text from DOM
        const bioSection = document.querySelector('.text-container, .bio-container, .member-bio');
        if (bioSection) {
            infoText += `\n--- PROFILE BIO ---\n`;
            infoText += bioSection.innerText.trim() + `\n`;
        }

        const infoSections = document.querySelectorAll('.details-container, .stats-container, .profile-details');
        if (infoSections.length > 0) {
            infoText += `\n--- PROFILE DETAILS ---\n`;
            infoSections.forEach(section => {
                infoText += section.innerText.trim() + `\n\n`;
            });
        }

        zip.file("profile_info.txt", infoText);

        // 3. Add raw JSON if present
        if (data) {
            zip.file("profile_data.json", JSON.stringify(data, null, 4));
        }

        // 4. Download and add photos
        let successCount = 0;
        const photosFolder = zip.folder("photos");

        const downloadPromises = urlsArray.map(async (url, idx) => {
            try {
                const blob = await fetchImageAsBlob(url);
                const ext = url.split('.').pop().split(/[?#]/)[0] || 'jpg';
                photosFolder.file(`photo_${idx + 1}.${ext}`, blob);
                successCount++;
            } catch (err) {
                console.error(`[Script] Failed to download photo from ${url}:`, err);
            }
        });

        await Promise.all(downloadPromises);

        if (successCount === 0) {
            showToast('Failed to download any profile photos.', 'error');
            return;
        }

        showToast(`Zipping ${successCount} photos...`, 'success');

        try {
            const content = await zip.generateAsync({ type: 'blob' });
            const blobUrl = URL.createObjectURL(content);
            const a = document.createElement('a');
            const safeName = displayName.replace(/[^a-z0-9_-]/gi, '_').toLowerCase();
            a.href = blobUrl;
            a.download = `${safeName}_${profileId}_export.zip`;
            a.click();
            URL.revokeObjectURL(blobUrl);
            showToast(`Export complete! Saved ${successCount}/${urlsArray.length} photos.`, 'success');
        } catch (err) {
            console.error('[Script] Error creating zip file:', err);
            showToast('Error generating zip archive.', 'error');
        }
    }

    function injectViewedButton() {
        const cruiseBtn = document.querySelector('a[href*="/cruise"], button:has(.fa-ship), [class*="cruise"], button:has(t101-icon-cruise), button:has(t101-icon-cruise-icon), t101-icon-cruise, t101-icon-cruise-icon, button:has([class*="cruise"]), button:has([id*="cruise"])');
        if (cruiseBtn) {
            const targetEl = cruiseBtn.closest('button') || cruiseBtn.closest('a') || cruiseBtn;

            // Inject Viewed Button
            let viewBtn = document.getElementById('manual-view-btn');
            if (!viewBtn) {
                viewBtn = document.createElement('button');
                viewBtn.id = 'manual-view-btn';
                viewBtn.textContent = 'Show As Viewed';
                viewBtn.style.cssText = `background-color: #2b2b2b; color: #ffffff; border: 1px solid #444; padding: 8px 16px; margin-left: 8px; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 14px; transition: background 0.2s;`;
                viewBtn.onmouseover = () => viewBtn.style.backgroundColor = '#3d3d3d';
                viewBtn.onmouseout = () => viewBtn.style.backgroundColor = '#2b2b2b';
                viewBtn.addEventListener('click', () => {
                    if (!lastBlockedRequest) return showToast('No profile visit tracker captured yet.', 'error');
                    const xhr = new origXHR();
                    xhr._isManualTrigger = true;
                    xhr.open(lastBlockedRequest.method, lastBlockedRequest.url);
                    if (lastBlockedRequest.headers) {
                        for (const [key, value] of Object.entries(lastBlockedRequest.headers)) {
                            xhr.setRequestHeader(key, value);
                        }
                    }
                    xhr.onload = function () {
                        if (this.status >= 200 && this.status < 300) {
                            showToast('Profile marked as viewed successfully!');
                        } else {
                            let errorMsg = `Failed to contact server: Status ${this.status}`;
                            try {
                                const errData = JSON.parse(this.responseText);
                                if (errData && errData.title) {
                                    errorMsg = errData.title;
                                }
                            } catch (e) { }
                            showToast(errorMsg, 'error');
                        }
                    };
                    xhr.onerror = () => showToast('Failed to contact server.', 'error');
                    xhr.send(lastBlockedRequest.body);
                });
                targetEl.parentNode.insertBefore(viewBtn, targetEl.nextSibling);
            }

            // Inject Export Button
            if (!document.getElementById('custom-export-profile-btn')) {
                const exportBtn = document.createElement('button');
                exportBtn.id = 'custom-export-profile-btn';
                exportBtn.textContent = 'Export Profile';
                exportBtn.style.cssText = `background-color: #2b2b2b; color: #ffffff; border: 1px solid #444; padding: 8px 16px; margin-left: 8px; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 14px; transition: background 0.2s;`;
                exportBtn.onmouseover = () => exportBtn.style.backgroundColor = '#3d3d3d';
                exportBtn.onmouseout = () => exportBtn.style.backgroundColor = '#2b2b2b';
                exportBtn.addEventListener('click', exportProfileData);
                viewBtn.parentNode.insertBefore(exportBtn, viewBtn.nextSibling);
            }
        }
    }

    function setupPhotoViewer() {
        const tiles = document.querySelectorAll('.photo-tile:not([data-lightbox-ready])');
        tiles.forEach(tile => {
            tile.setAttribute('data-lightbox-ready', 'true');
            tile.style.pointerEvents = 'auto'; tile.style.cursor = 'pointer';
            tile.addEventListener('click', (e) => {
                e.preventDefault(); e.stopPropagation();

                // Get all photo tiles in the same gallery container, or globally on the page if not grouped
                const container = tile.closest('.member-photos, t101-profile-photos, .gallery-grid, .photo-grid') || document.body;
                const allTiles = Array.from(container.querySelectorAll('.photo-tile'));
                const urls = allTiles.map(t => {
                    const bgImg = t.style.backgroundImage;
                    return bgImg ? bgImg.replace(/^url\(["']?/, '').replace(/["']?\)$/, '').split('?')[0] : null;
                }).filter(Boolean);

                const bgImg = tile.style.backgroundImage;
                if (!bgImg) return;
                let currentUrl = bgImg.replace(/^url\(["']?/, '').replace(/["']?\)$/, '').split('?')[0];
                let currentIndex = urls.indexOf(currentUrl);
                if (currentIndex === -1) {
                    currentIndex = 0;
                    urls.unshift(currentUrl);
                }

                createFullscreenLightbox(urls, currentIndex);
            });
        });
    }

    function createFullscreenLightbox(urls, startIndex) {
        if (document.getElementById('custom-photo-lightbox')) return;
        document.documentElement.style.setProperty('overflow', 'hidden', 'important');
        document.body.style.setProperty('overflow', 'hidden', 'important');

        let currentIndex = startIndex;

        const overlay = document.createElement('div');
        overlay.id = 'custom-photo-lightbox';
        overlay.style.cssText = `position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background-color: rgba(0, 0, 0, 0.95); z-index: 100000; display: flex; align-items: center; justify-content: center; opacity: 0; transition: opacity 0.25s ease;`;

        const img = document.createElement('img');
        img.src = urls[currentIndex];
        img.style.cssText = `max-width: 90%; max-height: 90%; object-fit: contain; box-shadow: 0 0 20px rgba(0,0,0,0.8); border-radius: 4px; user-select: none; transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);`;

        const closeBtn = document.createElement('div');
        closeBtn.innerHTML = '&#x2715;';
        closeBtn.style.cssText = `position: absolute; top: 20px; right: 25px; color: #ffffff; font-size: 35px; cursor: pointer; user-select: none; z-index: 100002; transition: transform 0.1s;`;

        function updateImage(index) {
            currentIndex = (index + urls.length) % urls.length;
            img.style.transform = 'scale(0.97)';
            img.style.opacity = '0';
            setTimeout(() => {
                img.src = urls[currentIndex];
                img.onload = () => {
                    img.style.transform = 'scale(1)';
                    img.style.opacity = '1';
                };
            }, 150);
        }

        if (urls.length > 1) {
            const leftBtn = document.createElement('div');
            leftBtn.innerHTML = '&#x276E;';
            leftBtn.style.cssText = `position: absolute; left: 30px; top: 50%; transform: translateY(-50%); width: 55px; height: 55px; background: rgba(255,255,255,0.08); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); display: flex; align-items: center; justify-content: center; border-radius: 50%; color: #fff; font-size: 24px; cursor: pointer; user-select: none; transition: all 0.25s ease; border: 1px solid rgba(255,255,255,0.15); z-index: 100001;`;
            leftBtn.onmouseover = () => { leftBtn.style.background = 'rgba(255,255,255,0.2)'; leftBtn.style.transform = 'translateY(-50%) scale(1.08)'; };
            leftBtn.onmouseout = () => { leftBtn.style.background = 'rgba(255,255,255,0.08)'; leftBtn.style.transform = 'translateY(-50%) scale(1)'; };
            leftBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                updateImage(currentIndex - 1);
            });

            const rightBtn = document.createElement('div');
            rightBtn.innerHTML = '&#x276F;';
            rightBtn.style.cssText = `position: absolute; right: 30px; top: 50%; transform: translateY(-50%); width: 55px; height: 55px; background: rgba(255,255,255,0.08); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); display: flex; align-items: center; justify-content: center; border-radius: 50%; color: #fff; font-size: 24px; cursor: pointer; user-select: none; transition: all 0.25s ease; border: 1px solid rgba(255,255,255,0.15); z-index: 100001;`;
            rightBtn.onmouseover = () => { rightBtn.style.background = 'rgba(255,255,255,0.2)'; rightBtn.style.transform = 'translateY(-50%) scale(1.08)'; };
            rightBtn.onmouseout = () => { rightBtn.style.background = 'rgba(255,255,255,0.08)'; rightBtn.style.transform = 'translateY(-50%) scale(1)'; };
            rightBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                updateImage(currentIndex + 1);
            });

            overlay.appendChild(leftBtn);
            overlay.appendChild(rightBtn);
        }

        const handleKeyDown = (e) => {
            if (e.key === 'ArrowLeft' && urls.length > 1) {
                updateImage(currentIndex - 1);
            } else if (e.key === 'ArrowRight' && urls.length > 1) {
                updateImage(currentIndex + 1);
            } else if (e.key === 'Escape') {
                closeLightbox();
            }
        };

        const closeLightbox = () => {
            win.removeEventListener('keydown', handleKeyDown);
            overlay.style.opacity = '0';
            setTimeout(() => {
                overlay.remove();
                document.documentElement.style.removeProperty('overflow');
                document.body.style.removeProperty('overflow');
            }, 250);
        };

        win.addEventListener('keydown', handleKeyDown);
        closeBtn.addEventListener('click', closeLightbox);
        overlay.addEventListener('click', (e) => { if (e.target === overlay) closeLightbox(); });

        overlay.appendChild(img);
        overlay.appendChild(closeBtn);
        document.body.appendChild(overlay);

        setTimeout(() => overlay.style.opacity = '1', 10);
    }

    // -------------------------------------------------------------------------
    // 6. OBSERVER LIFECYCLE
    // -------------------------------------------------------------------------
    const observer = new MutationObserver(() => {
        injectViewedButton();
        setupPhotoViewer();
        if (lastProfileDetailPageData) {
            paintProfileDetailPage(lastProfileDetailPageData);
        }

        profileDetailsCache.forEach((data, profileId) => {
            const card = document.getElementById(profileId);
            if (card && !card.dataset.reconInjected) paintProfileData(profileId);
        });
    });

    observer.observe(document.documentElement, { childList: true, subtree: true });

    // Handle initial state if page is loaded
    if (lastProfileDetailPageData) {
        paintProfileDetailPage(lastProfileDetailPageData);
    }

})();