VgenUnlocked

Adds minimum price badges, advanced sorting, price filtering, quick preview modals, help popup, and mature content hover unblur to VGen listings.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         VgenUnlocked
// @namespace    https://vgen.co/
// @version      1.0
// @description  Adds minimum price badges, advanced sorting, price filtering, quick preview modals, help popup, and mature content hover unblur to VGen listings.
// @author       TheDarkEnjoyer
// @match        https://vgen.co/*
// @run-at       document-start
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // Global state caches
    const loadedServicesMap = new Map(); // serviceID => service details object
    const categoryOrders = new Map(); // pathname => array of serviceIDs in original order
    let exchangeRates = null;

    // Panel controls state
    let isPricesEnabled = true;
    let sortType = 'default'; // 'default', 'price', 'rating', 'reviews'
    let minPriceInput = '';
    let maxPriceInput = '';
    let isMatureUnblurEnabled = true;
    let isPreviewEnabled = false;

    // React state hook tracking
    let activeHook = null;
    let originalDispatch = null;

    // 1. Intercept Fetch requests immediately at document-start
    const originalFetch = window.fetch;
    window.fetch = async function(...args) {
        // Safe fetch invocation
        const response = await originalFetch(...args);

        // Safe URL extraction
        let url = '';
        if (typeof args[0] === 'string') {
            url = args[0];
        } else if (args[0] instanceof URL) {
            url = args[0].href;
        } else if (args[0] && typeof args[0] === 'object' && args[0].url) {
            url = args[0].url;
        }

        if (url) {
            // Intercept search pagination API
            if (url.includes('/commission/services/search')) {
                try {
                    const clone = response.clone();
                    const data = await clone.json();
                    if (data && Array.isArray(data.services)) {
                        addToCache(data.services);
                        triggerObserveAndRender();
                    }
                } catch (e) {
                    console.error('[VGen Sorter] Error parsing search fetch:', e);
                }
            }

            // Intercept client-side Next.js route data loads (for category changes)
            if (url.includes('/_next/data/') && url.endsWith('.json')) {
                try {
                    const clone = response.clone();
                    const data = await clone.json();
                    if (data && data.pageProps && data.pageProps.initialServices && Array.isArray(data.pageProps.initialServices.services)) {
                        addToCache(data.pageProps.initialServices.services);
                        triggerObserveAndRender();
                    }
                } catch (e) {
                    console.error('[VGen Sorter] Error parsing SPA route fetch:', e);
                }
            }
        }

        return response;
    };

    // Helper: Parse current category path
    function getCurrentCategory() {
        return window.location.pathname;
    }

    // Helper: Add services to local cache and maintain relative original order
    function addToCache(services) {
        const category = getCurrentCategory();
        if (!categoryOrders.has(category)) {
            categoryOrders.set(category, []);
        }
        const orderArray = categoryOrders.get(category);

        services.forEach(s => {
            if (s && s.serviceID) {
                loadedServicesMap.set(s.serviceID, s);
                if (!orderArray.includes(s.serviceID)) {
                    orderArray.push(s.serviceID);
                }
            }
        });
    }

    // Fetch exchange rates from VGen API
    async function fetchExchangeRates() {
        try {
            const res = await originalFetch('https://api.vgen.co/exchange-rate');
            const data = await res.json();
            if (data && data.rates) {
                exchangeRates = data.rates;
                console.log('[VGen Sorter] Exchange rates loaded successfully.');
            }
        } catch (e) {
            console.error('[VGen Sorter] Failed to fetch exchange rates:', e);
        }
    }

    // Parse server-rendered initial data __NEXT_DATA__
    function parseInitialData() {
        const nextDataEl = document.getElementById('__NEXT_DATA__');
        if (nextDataEl) {
            try {
                const data = JSON.parse(nextDataEl.textContent);
                if (data.props && data.props.pageProps && data.props.pageProps.initialServices && Array.isArray(data.props.pageProps.initialServices.services)) {
                    addToCache(data.props.pageProps.initialServices.services);
                    console.log(`[VGen Sorter] Cached ${data.props.pageProps.initialServices.services.length} initial services.`);
                }
            } catch (e) {
                console.error('[VGen Sorter] Error parsing __NEXT_DATA__:', e);
            }
        }
    }

    // Parse VGen's currently selected currency from navbar or cookies
    function getSelectedCurrency() {
        // Option 1: Parse from the nav bar element
        const currencyEl = document.querySelector('[class*="DiscoveryNavbar"] [class*="currency"] p, [class*="DiscoveryNavbar"] [class*="currency"] span');
        if (currencyEl) {
            const txt = currencyEl.innerText.trim().toUpperCase();
            if (txt.length === 3) return txt;
        }

        // Option 2: Parse from v-guest cookie
        const guestCookie = document.cookie.split('; ').find(row => row.startsWith('v-guest='));
        if (guestCookie) {
            try {
                const jwt = guestCookie.split('=')[1];
                const payload = JSON.parse(atob(jwt.split('.')[1]));
                if (payload && payload.currency) {
                    return payload.currency.toUpperCase();
                }
            } catch(e) {}
        }

        return 'USD'; // default fallback
    }

    // Currency conversion logic using API rates
    function convertCurrency(value, fromCurrency, toCurrency) {
        if (!exchangeRates) return value;
        if (fromCurrency === toCurrency) return value;

        // Convert from base currency to USD
        const rateFromToUSD = exchangeRates[fromCurrency] ? exchangeRates[fromCurrency]['USD'] : 1;
        const valUSD = value * rateFromToUSD;

        // Convert from USD to target currency
        const rateUSDToTarget = exchangeRates['USD'] && exchangeRates['USD'][toCurrency] ? exchangeRates['USD'][toCurrency] : 1;
        return valUSD * rateUSDToTarget;
    }

    // Calculate minimum price applying active discounts
    function getServicePriceInfo(service) {
        const basePrice = service.basePrice / 100;
        const currency = service.currency || 'USD';

        let discountPercent = 0;
        const now = new Date();
        const hasActiveDiscountDate = service.activeDiscountEndDate && new Date(service.activeDiscountEndDate) > now;

        if (hasActiveDiscountDate && Array.isArray(service.discounts)) {
            const activeDiscount = service.discounts.find(d => {
                const start = d.startDate ? new Date(d.startDate) : null;
                const end = d.endDate ? new Date(d.endDate) : null;
                return d.status === 'PUBLISHED' &&
                       (!start || start <= now) &&
                       (!end || end >= now);
            });
            if (activeDiscount && typeof activeDiscount.percentage === 'number') {
                discountPercent = activeDiscount.percentage;
            }
        }

        const finalPrice = basePrice * (1 - discountPercent);
        return {
            basePrice,
            finalPrice,
            currency,
            hasDiscount: discountPercent > 0,
            discountPercent: Math.round(discountPercent * 100)
        };
    }

    // Convert price to USD using exchange rates table
    function getUSDPrice(service) {
        if (!service) return 0;
        const priceInfo = getServicePriceInfo(service);
        return convertCurrency(priceInfo.finalPrice, priceInfo.currency, 'USD');
    }

    // Format price for rendering (normalizing to selected currency)
    const currencySymbols = {
        USD: '$', EUR: '€', GBP: '£', JPY: '¥', CAD: 'CA$', AUD: 'A$', HKD: 'HK$', SGD: 'S$', TWD: 'NT$', KRW: '₩'
    };

    function formatPriceHTML(priceInfo) {
        const targetCurrency = getSelectedCurrency();

        // Convert prices to normalized target currency
        const convertedFinal = convertCurrency(priceInfo.finalPrice, priceInfo.currency, targetCurrency);
        const convertedBase = convertCurrency(priceInfo.basePrice, priceInfo.currency, targetCurrency);

        const symbol = currencySymbols[targetCurrency] || (targetCurrency + ' ');
        const formattedFinal = convertedFinal % 1 === 0 ? convertedFinal.toFixed(0) : convertedFinal.toFixed(2);

        if (priceInfo.hasDiscount) {
            const formattedOrig = convertedBase % 1 === 0 ? convertedBase.toFixed(0) : convertedBase.toFixed(2);
            return `<span style="text-decoration: line-through; opacity: 0.5; font-size: 0.75rem; margin-right: 4px;">${symbol}${formattedOrig}</span>` +
                   `<span>${symbol}${formattedFinal}</span>` +
                   `<span style="margin-left: 4px; font-size: 0.7rem; color: #ffbe0b; font-weight: 800;">(-${priceInfo.discountPercent}%)</span>`;
        }

        return `${symbol}${formattedFinal}`;
    }

    // String cleaners for DOM-to-Cache matching
    function cleanString(str) {
        if (!str) return '';
        return str.trim().toLowerCase().replace(/\s+/g, ' ');
    }

    function getUsernameFromHref(href) {
        if (!href) return '';
        try {
            if (href.startsWith('http')) {
                return new URL(href).pathname.replace(/^\//, '').split('?')[0].toLowerCase();
            }
            return href.replace(/^\//, '').split('?')[0].toLowerCase();
        } catch (e) {
            return href.replace(/^\//, '').split('?')[0].toLowerCase();
        }
    }

    // Match card to cached service details
    function findMatchedService(card) {
        const profileLink = card.querySelector('a[href]');
        if (!profileLink) return null;

        const username = getUsernameFromHref(profileLink.getAttribute('href'));

        const titleEl = card.querySelector('[class*="serviceNameContainer"] p, [class*="ServiceNameContainer"] p, p[class*="ServiceGridCard__ClickableText"]');
        if (!titleEl) return null;

        const cardTitleCleaned = cleanString(titleEl.innerText);

        for (const service of loadedServicesMap.values()) {
            const serviceUsernameCleaned = cleanString(service.user && service.user.username ? service.user.username : '');
            const serviceNameCleaned = cleanString(service.serviceName || '');

            if (serviceUsernameCleaned === username && serviceNameCleaned === cardTitleCleaned) {
                return service;
            }
        }
        return null;
    }

    // Render price badges on cards
    function updateCardPriceBadges() {
        const cards = document.querySelectorAll('[class*="ServiceGridCard__GridCard"]');

        cards.forEach(card => {
            let badge = card.querySelector('.vgen-price-badge-container');
            let previewBtn = card.querySelector('.vgen-preview-btn');

            if (!isPricesEnabled) {
                if (badge) badge.remove();
                if (previewBtn) previewBtn.remove();
                return;
            }

            const service = findMatchedService(card);
            if (!service) {
                if (badge) badge.remove();
                if (previewBtn) previewBtn.remove();
                return;
            }

            const priceInfo = getServicePriceInfo(service);
            const badgeHTML = formatPriceHTML(priceInfo);

            // 1. Create or update price badge
            if (!badge) {
                badge = document.createElement('div');
                badge.className = 'vgen-price-badge-container';

                const targetContainer = card.querySelector('[class*="ServiceGridCard__RatingsAndSaleContainer"]');
                if (targetContainer) {
                    targetContainer.appendChild(badge);
                } else {
                    const infoContainer = card.querySelector('[class*="ServiceGridCard__ServiceInfoContainer"]');
                    if (infoContainer) infoContainer.appendChild(badge);
                }
            }
            if (badge.innerHTML !== badgeHTML) {
                badge.innerHTML = badgeHTML;
            }

            // 2. Create or update preview button
            if (isPreviewEnabled) {
                if (!previewBtn) {
                    previewBtn = document.createElement('button');
                    previewBtn.className = 'vgen-preview-btn';
                    previewBtn.innerHTML = '👁️';
                    previewBtn.title = 'Quick Preview';
                    previewBtn.addEventListener('click', (e) => {
                        e.stopPropagation();
                        e.preventDefault();
                        openPreviewModal(service);
                    });

                    const thumb = card.querySelector('[class*="ThumbnailContainer"]') || card.querySelector('a');
                    if (thumb) {
                        if (window.getComputedStyle(thumb).position === 'static') {
                            thumb.style.position = 'relative';
                        }
                        thumb.appendChild(previewBtn);
                    }
                }
            } else {
                if (previewBtn) previewBtn.remove();
            }
        });
    }

    // 2. React Fiber Hook Interception for Sorting & Filtering
    function findServicesHookAndDispatch() {
        const card = document.querySelector('[class*="ServiceGridCard__GridCard"]');
        if (!card) return null;

        const fiberKey = Object.keys(card).find(k => k.startsWith('__reactFiber$'));
        if (!fiberKey) return null;

        let fiber = card[fiberKey];
        while (fiber) {
            if (fiber.memoizedState) {
                let hook = fiber.memoizedState;
                while (hook) {
                    const val = hook.memoizedState;
                    if (Array.isArray(val) && val.length > 0 && val[0] && val[0].serviceID) {
                        if (hook.queue && typeof hook.queue.dispatch === 'function') {
                            return { fiber, hook, dispatch: hook.queue.dispatch, services: val };
                        }
                    }
                    hook = hook.next;
                }
            }
            fiber = fiber.return;
        }
        return null;
    }

    // Dynamic reconstruction of page list applying filters and sorting
    function getModifiedServicesList() {
        const category = getCurrentCategory();
        const orderArray = categoryOrders.get(category) || [];

        // Reconstruct the full list of loaded services in their original order
        let services = orderArray.map(id => loadedServicesMap.get(id)).filter(Boolean);

        // Apply Price Filtering
        const minPrice = parseFloat(minPriceInput);
        const maxPrice = parseFloat(maxPriceInput);
        const targetCurrency = getSelectedCurrency();

        if (!isNaN(minPrice) || !isNaN(maxPrice)) {
            services = services.filter(s => {
                const priceInfo = getServicePriceInfo(s);
                // Convert service price to selected currency
                const priceInSelected = convertCurrency(priceInfo.finalPrice, priceInfo.currency, targetCurrency);

                if (!isNaN(minPrice) && priceInSelected < minPrice) return false;
                if (!isNaN(maxPrice) && priceInSelected > maxPrice) return false;
                return true;
            });
        }

        // Apply Sorting
        if (sortType === 'price') {
            services.sort((a, b) => getUSDPrice(a) - getUSDPrice(b));
        } else if (sortType === 'rating') {
            services.sort((a, b) => {
                const rA = a.artistReviewStats ? a.artistReviewStats.averageRating : 0;
                const rB = b.artistReviewStats ? b.artistReviewStats.averageRating : 0;
                return rB - rA || (b.artistReviewStats?.totalReviews || 0) - (a.artistReviewStats?.totalReviews || 0);
            });
        } else if (sortType === 'reviews') {
            services.sort((a, b) => {
                const rA = a.artistReviewStats ? a.artistReviewStats.totalReviews : 0;
                const rB = b.artistReviewStats ? b.artistReviewStats.totalReviews : 0;
                return rB - rA;
            });
        } else {
            // Restore default loading order
            services.sort((a, b) => orderArray.indexOf(a.serviceID) - orderArray.indexOf(b.serviceID));
        }

        return services;
    }

    function isStateCorrect(services) {
        const expected = getModifiedServicesList();
        if (services.length !== expected.length) return false;
        for (let i = 0; i < services.length; i++) {
            if (services[i].serviceID !== expected[i].serviceID) return false;
        }
        return true;
    }

    function ensureHooked() {
        // If unmounted, clean up active tracking
        if (activeHook && !document.body.contains(activeHook.fiber.stateNode)) {
            activeHook = null;
            originalDispatch = null;
        }

        if (activeHook) {
            // Self-healing: continually feed any newly mounted services from React state into our cache
            if (Array.isArray(activeHook.hook.memoizedState)) {
                addToCache(activeHook.hook.memoizedState);
            }
            applySortingState();
            return;
        }

        const hookInfo = findServicesHookAndDispatch();
        if (hookInfo) {
            activeHook = hookInfo;
            originalDispatch = hookInfo.dispatch;

            // Self-healing: cache all existing services in the hook state immediately
            if (Array.isArray(hookInfo.services)) {
                addToCache(hookInfo.services);
            }

            // Apply active sort/restore settings immediately
            applySortingState();
        }
    }

    function applySortingState() {
        if (!activeHook) return;
        const currentServices = activeHook.hook.memoizedState;
        if (!Array.isArray(currentServices)) return;

        if (!isStateCorrect(currentServices)) {
            const modified = getModifiedServicesList();
            originalDispatch(modified);
        }
    }

    // 3. Mature Content Hover Unblur Handler
    let hoverTimeout = null;
    let currentHoveredCard = null;

    function setupMatureUnblurListeners() {
        document.body.addEventListener('mouseenter', (e) => {
            if (!isMatureUnblurEnabled) return;
            const card = e.target;
            if (!card || !card.matches || !card.matches('[class*="ServiceGridCard__GridCard"], [class*="ProductListing"]')) return;

            // Check if card contains mature warning text
            const textContent = card.innerText;
            if (textContent.includes('Mature content') || textContent.includes('Contains sexual')) {
                if (hoverTimeout) clearTimeout(hoverTimeout);
                currentHoveredCard = card;
                hoverTimeout = setTimeout(() => {
                    if (currentHoveredCard === card) {
                        revealMatureContent(card);
                    }
                }, 500);
            }
        }, true);

        document.body.addEventListener('mouseleave', (e) => {
            const card = e.target;
            if (card && card.matches && card.matches('[class*="ServiceGridCard__GridCard"], [class*="ProductListing"]')) {
                if (hoverTimeout) clearTimeout(hoverTimeout);
                if (currentHoveredCard === card) {
                    currentHoveredCard = null;
                }
                hideMatureContent(card);
            }
        }, true);
    }

    function revealMatureContent(card) {
        if (card.dataset.matureRevealed === 'true') return;
        const service = findMatchedService(card);
        if (!service) return;

        const mediaUrl = service.galleryItems?.[0]?.url;
        if (!mediaUrl) return;

        const thumb = card.querySelector('[class*="ThumbnailContainer"]') || card.querySelector('a');
        if (!thumb) return;

        const warning = card.querySelector('[class*="MatureContentWarning"]') || card.querySelector('[class*="matureContentWarning"]');

        card.dataset.matureRevealed = 'true';
        card.vgenWarningEl = warning;

        // Hide warning
        if (warning) {
            warning.style.transition = 'opacity 0.2s ease-in-out';
            warning.style.opacity = '0';
        }

        const isVideo = mediaUrl.endsWith('.webm') || mediaUrl.endsWith('.mp4');
        const mediaEl = isVideo ? document.createElement('video') : document.createElement('img');
        mediaEl.src = mediaUrl;
        mediaEl.className = 'vgen-revealed-media';
        if (isVideo) {
            mediaEl.autoplay = true;
            mediaEl.loop = true;
            mediaEl.muted = true;
            mediaEl.playsInline = true;
        }

        if (window.getComputedStyle(thumb).position === 'static') {
            thumb.style.position = 'relative';
        }
        thumb.appendChild(mediaEl);
        card.vgenRevealedMediaEl = mediaEl;

        requestAnimationFrame(() => {
            mediaEl.style.opacity = '1';
        });

        // Hide warning display after transition completes
        if (warning) {
            setTimeout(() => {
                if (card.dataset.matureRevealed === 'true' && warning.parentNode) {
                    warning.style.display = 'none';
                }
            }, 200);
        }
    }

    function hideMatureContent(card) {
        if (card.dataset.matureRevealed !== 'true') return;
        card.dataset.matureRevealed = 'false';

        // Remove media
        if (card.vgenRevealedMediaEl) {
            card.vgenRevealedMediaEl.remove();
            card.vgenRevealedMediaEl = null;
        }

        // Restore warning
        if (card.vgenWarningEl) {
            card.vgenWarningEl.style.display = '';
            requestAnimationFrame(() => {
                if (card.vgenWarningEl) {
                    card.vgenWarningEl.style.opacity = '1';
                }
            });
        }
    }

    // 4. Slate JSON Parser for Commission Descriptions
    function parseSlateJSON(slateStr) {
        try {
            const nodes = JSON.parse(slateStr);
            let html = '';
            if (Array.isArray(nodes)) {
                nodes.forEach(node => {
                    html += parseSlateNode(node);
                });
            }
            return html;
        } catch(e) {
            return `<p>${slateStr}</p>`; // fallback to raw string
        }
    }

    function parseSlateNode(node) {
        if (!node) return '';
        if (node.text !== undefined) {
            let text = node.text;
            if (node.bold) text = `<strong>${text}</strong>`;
            if (node.italic) text = `<em>${text}</em>`;
            if (node.underline) text = `<u>${text}</u>`;
            return text;
        }

        const childrenHtml = Array.isArray(node.children)
            ? node.children.map(parseSlateNode).join('')
            : '';

        switch(node.type) {
            case 'heading-one': return `<h2 style="font-size: 1.15rem; font-weight: 800; margin-top: 14px; margin-bottom: 6px; color: #ffbe0b;">${childrenHtml}</h2>`;
            case 'heading-two': return `<h3 style="font-size: 1.05rem; font-weight: 700; margin-top: 12px; margin-bottom: 4px;">${childrenHtml}</h3>`;
            case 'heading-three': return `<h4 style="font-size: 0.95rem; font-weight: 700; margin-top: 10px; margin-bottom: 4px;">${childrenHtml}</h4>`;
            case 'bulleted-list': return `<ul style="padding-left: 20px; margin-bottom: 8px; list-style-type: disc;">${childrenHtml}</ul>`;
            case 'numbered-list': return `<ol style="padding-left: 20px; margin-bottom: 8px; list-style-type: decimal;">${childrenHtml}</ol>`;
            case 'list-item': return `<li style="margin-bottom: 4px;">${childrenHtml}</li>`;
            case 'paragraph': return `<p style="margin-bottom: 8px; line-height: 1.4;">${childrenHtml}</p>`;
            default: return `<div style="margin-bottom: 6px;">${childrenHtml}</div>`;
        }
    }

    // 5. Render Preview Modal
    function openPreviewModal(service) {
        // Remove existing modal if any
        const existing = document.getElementById('vgen-preview-modal');
        if (existing) existing.remove();

        const modal = document.createElement('div');
        modal.id = 'vgen-preview-modal';
        modal.className = 'vgen-modal-overlay';

        const priceInfo = getServicePriceInfo(service);
        const formattedPrice = formatPriceHTML(priceInfo);
        const descriptionHTML = parseSlateJSON(service.description);

        // Gallery slides
        const galleryItems = service.galleryItems || [];
        let galleryHTML = '';
        if (galleryItems.length > 0) {
            galleryHTML = `
                <div class="vgen-modal-gallery">
                    ${galleryItems.map(item => `
                        <div class="vgen-gallery-item">
                            <img src="${item.url}" alt="" loading="lazy">
                            ${item.isVerified ? '<span class="vgen-verified-tag">✓ Verified</span>' : ''}
                        </div>
                    `).join('')}
                </div>
            `;
        } else {
            galleryHTML = '<p style="opacity: 0.5; font-size: 0.8rem;">No preview images available.</p>';
        }

        // Tags
        const tags = service.tags || [];
        const tagsHTML = tags.map(tag => `<span class="vgen-tag">#${tag}</span>`).join(' ');

        modal.innerHTML = `
            <div class="vgen-modal-content">
                <button class="vgen-modal-close">×</button>
                <div class="vgen-modal-body">
                    <div class="vgen-modal-header-section">
                        <h1 class="vgen-modal-title">${service.serviceName}</h1>
                        <p class="vgen-modal-author">by <strong>${service.user ? service.user.displayName : 'Artist'}</strong> (@${service.user ? service.user.username : 'username'})</p>
                        <div class="vgen-modal-price-badges">
                            <span class="vgen-modal-price-label">Starting Price:</span>
                            <span class="vgen-modal-price-val">${formattedPrice}</span>
                        </div>
                    </div>

                    ${galleryHTML}

                    <div class="vgen-modal-info-section">
                        <div class="vgen-modal-desc-heading">Details</div>
                        <div class="vgen-modal-desc-body">${descriptionHTML}</div>
                    </div>

                    ${tags.length > 0 ? `<div class="vgen-modal-tags">${tagsHTML}</div>` : ''}

                    <div class="vgen-modal-footer">
                        <a href="/commission/${service.serviceID || ''}" target="_blank" class="vgen-modal-order-btn">Open Order Page</a>
                    </div>
                </div>
            </div>
        `;

        document.body.appendChild(modal);

        // Bind Close triggers
        modal.querySelector('.vgen-modal-close').addEventListener('click', () => modal.remove());
        modal.addEventListener('click', (e) => {
            if (e.target === modal) modal.remove();
        });
    }

    // Help modal popup
    function openHelpModal() {
        // Remove existing modal if any
        const existing = document.getElementById('vgen-help-modal');
        if (existing) existing.remove();

        const modal = document.createElement('div');
        modal.id = 'vgen-help-modal';
        modal.className = 'vgen-modal-overlay';

        modal.innerHTML = `
            <div class="vgen-modal-content" style="max-width: 500px;">
                <button class="vgen-modal-close">×</button>
                <div class="vgen-modal-body">
                    <h1 class="vgen-modal-title" style="background: linear-gradient(135deg, #FFBE0B, #F084BC, #946CF3); -webkit-background-clip: text; -webkit-text-fill-color: transparent; text-align: center; margin-bottom: 20px;">Welcome to VgenUnlocked!</h1>

                    <p style="font-size: 0.9rem; line-height: 1.5; margin-bottom: 20px; opacity: 0.9; text-align: center;">
                        Your VGen experience is now enhanced with powerful tools for pricing, filtering, sorting, and content previewing.
                    </p>

                    <div style="display: flex; flex-direction: column; gap: 14px; margin-bottom: 24px;">
                        <div style="display: flex; gap: 10px; align-items: flex-start;">
                            <span style="font-size: 1.2rem;">🏷️</span>
                            <div>
                                <strong style="color: #F084BC; font-size: 0.88rem; display: block; margin-bottom: 2px;">Min Base Price Badges</strong>
                                <span style="font-size: 0.8rem; opacity: 0.8;">Displays base prices factoring in active discount percentages directly on the commission listing cards, automatically normalized to your selected active currency.</span>
                            </div>
                        </div>

                        <div style="display: flex; gap: 10px; align-items: flex-start;">
                            <span style="font-size: 1.2rem;">👁️</span>
                            <div>
                                <strong style="color: #F084BC; font-size: 0.88rem; display: block; margin-bottom: 2px;">Quick Preview Modal</strong>
                                <span style="font-size: 0.8rem; opacity: 0.8;">Click the preview eye button on any card thumbnail to view slate descriptions parsed to clean HTML, tag lists, and slide through full image galleries.</span>
                            </div>
                        </div>

                        <div style="display: flex; gap: 10px; align-items: flex-start;">
                            <span style="font-size: 1.2rem;">🫣</span>
                            <div>
                                <strong style="color: #F084BC; font-size: 0.88rem; display: block; margin-bottom: 2px;">Mature Content Hover Unblur</strong>
                                <span style="font-size: 0.8rem; opacity: 0.8;">Hover over any mature warning element for 0.5 seconds to instantly unblur the high-res thumbnail with a smooth CSS fade. Re-blurs instantly on mouse leave.</span>
                            </div>
                        </div>

                        <div style="display: flex; gap: 10px; align-items: flex-start;">
                            <span style="font-size: 1.2rem;">🎛️</span>
                            <div>
                                <strong style="color: #F084BC; font-size: 0.88rem; display: block; margin-bottom: 2px;">Price Range Filter</strong>
                                <span style="font-size: 0.8rem; opacity: 0.8;">Narrow down commissions by typing your Min and Max price targets into the float control panel in your native currency.</span>
                            </div>
                        </div>

                        <div style="display: flex; gap: 10px; align-items: flex-start;">
                            <span style="font-size: 1.2rem;">↕️</span>
                            <div>
                                <strong style="color: #F084BC; font-size: 0.88rem; display: block; margin-bottom: 2px;">Advanced Sorting Options</strong>
                                <span style="font-size: 0.8rem; opacity: 0.8;">Rearrange active grids instantly by Price (Low to High), Rating (High to Low), or total Reviews (High to Low).</span>
                            </div>
                        </div>
                    </div>

                    <div style="display: flex; justify-content: center;">
                        <button class="vgen-modal-close-btn" style="background: linear-gradient(135deg, #F41173, #913BCD); border: none; color: #fff; padding: 10px 24px; border-radius: 8px; font-weight: 700; cursor: pointer; box-shadow: 0 4px 12px rgba(244, 17, 115, 0.3); font-size: 0.85rem;">Let's Go!</button>
                    </div>
                </div>
            </div>
        `;

        document.body.appendChild(modal);

        const closeModal = () => modal.remove();
        modal.querySelector('.vgen-modal-close').addEventListener('click', closeModal);
        modal.querySelector('.vgen-modal-close-btn').addEventListener('click', closeModal);
        modal.addEventListener('click', (e) => {
            if (e.target === modal) closeModal();
        });
    }

    function showWelcomeIfNeeded() {
        const welcomed = localStorage.getItem('vgen_unlocked_welcomed');
        if (!welcomed) {
            openHelpModal();
            localStorage.setItem('vgen_unlocked_welcomed', 'true');
        }
    }

    // 6. UI Control Panel Creation
    function createControlPanel() {
        if (document.getElementById('vgen-sorter-panel')) return;

        // Styles injection
        const style = document.createElement('style');
        style.textContent = `
            /* Price badge next to reviews */
            .vgen-price-badge-container {
                display: inline-flex;
                align-items: center;
                background: linear-gradient(135deg, rgba(244, 17, 115, 0.95), rgba(145, 59, 205, 0.95));
                color: #ffffff;
                font-family: 'Satoshi', 'Inter', -apple-system, sans-serif;
                font-size: 0.82rem;
                font-weight: 700;
                padding: 4px 8px;
                border-radius: 6px;
                margin-left: 6px;
                box-shadow: 0 2px 8px rgba(244, 17, 115, 0.3);
                border: 1px solid rgba(255, 255, 255, 0.1);
                transition: all 0.2s;
            }
            .vgen-preview-btn {
                position: absolute;
                bottom: 8px;
                left: 8px;
                z-index: 8;
                background: rgba(12, 9, 13, 0.8);
                backdrop-filter: blur(8px);
                border: 1px solid rgba(255, 255, 255, 0.15);
                color: #fff;
                border-radius: 50%;
                width: 28px;
                height: 28px;
                display: inline-flex;
                align-items: center;
                justify-content: center;
                cursor: pointer;
                font-size: 0.85rem;
                transition: all 0.2s;
                box-shadow: 0 4px 10px rgba(0, 0, 0, 0.4);
            }
            .vgen-preview-btn:hover {
                background: rgba(244, 17, 115, 0.95);
                border-color: rgba(244, 17, 115, 1);
                transform: scale(1.1);
            }
            .vgen-help-icon-btn {
                background: rgba(255, 255, 255, 0.1);
                border: 1px solid rgba(255, 255, 255, 0.15);
                color: #fff;
                border-radius: 50%;
                width: 18px;
                height: 18px;
                display: inline-flex;
                align-items: center;
                justify-content: center;
                cursor: pointer;
                font-size: 0.7rem;
                font-weight: 800;
                padding: 0;
                line-height: 1;
                transition: all 0.2s;
            }
            .vgen-help-icon-btn:hover {
                background: rgba(244, 17, 115, 0.3);
                border-color: rgba(244, 17, 115, 0.6);
                transform: scale(1.1);
            }
            .vgen-modal-close-btn {
                transition: all 0.2s;
            }
            .vgen-modal-close-btn:hover {
                transform: translateY(-1px);
                box-shadow: 0 6px 16px rgba(244, 17, 115, 0.45);
            }
            .vgen-unlocked-trigger-btn {
                position: fixed;
                bottom: 24px;
                right: 24px;
                z-index: 99999;
                background: rgba(12, 9, 13, 0.85);
                backdrop-filter: blur(16px) saturate(180%);
                -webkit-backdrop-filter: blur(16px) saturate(180%);
                border: 1px solid rgba(255, 255, 255, 0.08);
                color: #ffffff;
                border-radius: 50%;
                width: 44px;
                height: 44px;
                display: flex;
                align-items: center;
                justify-content: center;
                cursor: pointer;
                box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.1);
                font-size: 1.2rem;
                transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
            }
            .vgen-unlocked-trigger-btn:hover {
                border-color: rgba(244, 17, 115, 0.4);
                box-shadow: 0 12px 36px rgba(244, 17, 115, 0.15), 0 10px 30px rgba(0, 0, 0, 0.4);
                transform: scale(1.1);
            }

            /* Mature Content Unblur Styles */
            .vgen-revealed-media {
                position: absolute;
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
                object-fit: cover;
                z-index: 5;
                border-radius: inherit;
                pointer-events: none;
                opacity: 0;
                transition: opacity 0.25s ease-in-out;
                will-change: opacity;
            }

            /* Floating Control Panel */
            .vgen-control-panel {
                position: fixed;
                bottom: 24px;
                right: 24px;
                z-index: 99999;
                width: 270px;
                background: rgba(12, 9, 13, 0.85);
                backdrop-filter: blur(16px) saturate(180%);
                -webkit-backdrop-filter: blur(16px) saturate(180%);
                border: 1px solid rgba(255, 255, 255, 0.08);
                border-radius: 16px;
                padding: 16px;
                box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.1);
                color: #ffffff;
                font-family: 'Satoshi', 'Inter', -apple-system, sans-serif;
                transition: all 0.3s ease;
            }
            .vgen-control-panel:hover {
                border-color: rgba(244, 17, 115, 0.4);
                box-shadow: 0 12px 36px rgba(244, 17, 115, 0.15), 0 10px 30px rgba(0, 0, 0, 0.4);
            }
            .vgen-panel-header {
                display: flex;
                align-items: center;
                justify-content: space-between;
                margin-bottom: 12px;
                border-bottom: 1px solid rgba(255, 255, 255, 0.1);
                padding-bottom: 8px;
            }
            .vgen-panel-title {
                font-size: 0.82rem;
                font-weight: 800;
                letter-spacing: 0.8px;
                background: linear-gradient(135deg, #FFBE0B, #F084BC, #946CF3);
                -webkit-background-clip: text;
                -webkit-text-fill-color: transparent;
                text-transform: uppercase;
            }
            .vgen-control-row {
                display: flex;
                align-items: center;
                justify-content: space-between;
                margin-bottom: 10px;
            }
            .vgen-control-label {
                font-size: 0.8rem;
                font-weight: 600;
                color: rgba(255, 255, 255, 0.85);
            }

            /* Inputs and Selects */
            .vgen-select, .vgen-input {
                background: rgba(255, 255, 255, 0.08);
                border: 1px solid rgba(255, 255, 255, 0.15);
                color: #fff;
                border-radius: 6px;
                padding: 4px 6px;
                font-family: inherit;
                font-size: 0.78rem;
                outline: none;
                width: 100px;
                transition: border-color 0.2s;
            }
            .vgen-select:focus, .vgen-input:focus {
                border-color: rgba(244, 17, 115, 0.5);
            }
            .vgen-select {
                width: 130px;
            }

            .vgen-price-filter-group {
                display: flex;
                gap: 6px;
                align-items: center;
            }
            .vgen-price-filter-group .vgen-input {
                width: 60px;
                text-align: center;
            }

            /* Switches */
            .vgen-switch {
                position: relative;
                display: inline-block;
                width: 34px;
                height: 18px;
            }
            .vgen-switch input { opacity: 0; width: 0; height: 0; }
            .vgen-slider {
                position: absolute;
                cursor: pointer;
                inset: 0;
                background-color: rgba(255, 255, 255, 0.15);
                transition: .2s;
                border-radius: 20px;
            }
            .vgen-slider:before {
                position: absolute;
                content: "";
                height: 14px;
                width: 14px;
                left: 2px;
                bottom: 2px;
                background-color: white;
                transition: .2s;
                border-radius: 50%;
            }
            input:checked + .vgen-slider {
                background-image: linear-gradient(135deg, #F41173, #D32194);
            }
            input:checked + .vgen-slider:before {
                transform: translateX(16px);
            }

            /* Preview Modal Overlay */
            .vgen-modal-overlay {
                position: fixed;
                inset: 0;
                background: rgba(0, 0, 0, 0.7);
                backdrop-filter: blur(8px);
                z-index: 100000;
                display: flex;
                align-items: center;
                justify-content: center;
                padding: 24px;
                font-family: 'Satoshi', 'Inter', -apple-system, sans-serif;
            }
            .vgen-modal-content {
                background: #151116;
                border: 1px solid rgba(255, 255, 255, 0.08);
                border-radius: 20px;
                width: 100%;
                max-width: 640px;
                max-height: 85vh;
                overflow-y: auto;
                position: relative;
                box-shadow: 0 20px 50px rgba(0,0,0,0.6);
                animation: vgen-modal-anim 0.25s cubic-bezier(0.4, 0, 0.2, 1);
            }
            @keyframes vgen-modal-anim {
                from { opacity: 0; transform: scale(0.96); }
                to { opacity: 1; transform: scale(1); }
            }
            .vgen-modal-close {
                position: absolute;
                top: 16px;
                right: 16px;
                background: none;
                border: none;
                color: rgba(255,255,255,0.6);
                font-size: 2rem;
                cursor: pointer;
                line-height: 1;
                transition: color 0.2s;
            }
            .vgen-modal-close:hover { color: #fff; }
            .vgen-modal-body {
                padding: 24px;
                color: #fff;
            }
            .vgen-modal-header-section {
                margin-bottom: 20px;
            }
            .vgen-modal-title {
                font-size: 1.4rem;
                font-weight: 800;
                margin-right: 32px;
                margin-bottom: 6px;
                color: #fff;
            }
            .vgen-modal-author {
                font-size: 0.85rem;
                opacity: 0.7;
                margin-bottom: 12px;
            }
            .vgen-modal-price-badges {
                display: inline-flex;
                align-items: center;
                background: rgba(244, 17, 115, 0.1);
                border: 1px solid rgba(244, 17, 115, 0.2);
                padding: 6px 12px;
                border-radius: 8px;
            }
            .vgen-modal-price-label {
                font-size: 0.8rem;
                opacity: 0.7;
                margin-right: 6px;
            }
            .vgen-modal-price-val {
                font-weight: 800;
                color: #ffbe0b;
                font-size: 1rem;
            }

            /* Horizontal Scrollable Gallery */
            .vgen-modal-gallery {
                display: flex;
                gap: 12px;
                overflow-x: auto;
                padding-bottom: 10px;
                margin-bottom: 20px;
                scrollbar-width: thin;
            }
            .vgen-gallery-item {
                flex: 0 0 180px;
                height: 180px;
                position: relative;
                border-radius: 10px;
                overflow: hidden;
                background: #110e12;
                border: 1px solid rgba(255,255,255,0.05);
            }
            .vgen-gallery-item img {
                width: 100%;
                height: 100%;
                object-fit: cover;
            }
            .vgen-verified-tag {
                position: absolute;
                bottom: 6px;
                left: 6px;
                background: rgba(0, 180, 100, 0.9);
                color: #fff;
                font-size: 0.65rem;
                padding: 2px 6px;
                border-radius: 4px;
                font-weight: 700;
            }

            .vgen-modal-info-section {
                border-top: 1px solid rgba(255,255,255,0.08);
                padding-top: 16px;
                margin-bottom: 20px;
            }
            .vgen-modal-desc-heading {
                font-size: 0.95rem;
                font-weight: 700;
                margin-bottom: 10px;
                opacity: 0.9;
                text-transform: uppercase;
                letter-spacing: 0.5px;
            }
            .vgen-modal-desc-body {
                font-size: 0.88rem;
                opacity: 0.85;
                max-height: 250px;
                overflow-y: auto;
                padding-right: 8px;
            }

            .vgen-modal-tags {
                margin-bottom: 24px;
            }
            .vgen-tag {
                display: inline-block;
                background: rgba(255, 255, 255, 0.05);
                border: 1px solid rgba(255,255,255,0.08);
                color: rgba(255,255,255,0.8);
                font-size: 0.75rem;
                padding: 4px 8px;
                border-radius: 6px;
                margin-right: 6px;
                margin-bottom: 6px;
            }

            .vgen-modal-footer {
                display: flex;
                justify-content: flex-end;
            }
            .vgen-modal-order-btn {
                background: linear-gradient(135deg, #F41173, #913BCD);
                color: #fff !important;
                text-decoration: none !important;
                font-weight: 700;
                font-size: 0.9rem;
                padding: 10px 20px;
                border-radius: 10px;
                box-shadow: 0 4px 12px rgba(244, 17, 115, 0.3);
                transition: all 0.2s;
            }
            .vgen-modal-order-btn:hover {
                transform: translateY(-1px);
                box-shadow: 0 6px 16px rgba(244, 17, 115, 0.45);
            }
        `;
        document.head.appendChild(style);

        // Panel element
        const panel = document.createElement('div');
        panel.id = 'vgen-sorter-panel';
        panel.className = 'vgen-control-panel';
        panel.innerHTML = `
            <div class="vgen-panel-header">
                <span class="vgen-panel-title">VgenUnlocked</span>
                <div style="display: flex; gap: 6px; align-items: center;">
                    <button id="vgen-help-btn" class="vgen-help-icon-btn" title="Show Help">?</button>
                    <button id="vgen-minimize-btn" class="vgen-help-icon-btn" title="Hide Panel">−</button>
                </div>
            </div>

            <div class="vgen-control-row">
                <span class="vgen-control-label">Show Price Badges</span>
                <label class="vgen-switch">
                    <input type="checkbox" id="vgen-toggle-prices" checked>
                    <span class="vgen-slider"></span>
                </label>
            </div>

            <div class="vgen-control-row">
                <span class="vgen-control-label">Quick Preview</span>
                <label class="vgen-switch">
                    <input type="checkbox" id="vgen-toggle-preview" checked>
                    <span class="vgen-slider"></span>
                </label>
            </div>

            <div class="vgen-control-row">
                <span class="vgen-control-label">Mature Unblur</span>
                <label class="vgen-switch">
                    <input type="checkbox" id="vgen-toggle-unblur" checked>
                    <span class="vgen-slider"></span>
                </label>
            </div>

            <div class="vgen-control-row">
                <span class="vgen-control-label">Price Range</span>
                <div class="vgen-price-filter-group">
                    <input type="text" class="vgen-input" id="vgen-min-price" placeholder="Min">
                    <span style="opacity:0.5">-</span>
                    <input type="text" class="vgen-input" id="vgen-max-price" placeholder="Max">
                </div>
            </div>

            <div class="vgen-control-row">
                <span class="vgen-control-label">Sort By</span>
                <select class="vgen-select" id="vgen-sort-select">
                    <option value="default">Default</option>
                    <option value="price">Price: Low to High</option>
                    <option value="rating">Rating: High to Low</option>
                    <option value="reviews">Reviews: High to Low</option>
                </select>
            </div>
        `;
        document.body.appendChild(panel);

        // Wire events
        document.getElementById('vgen-toggle-prices').addEventListener('change', (e) => {
            isPricesEnabled = e.target.checked;
            updateCardPriceBadges();
        });

        document.getElementById('vgen-toggle-preview').addEventListener('change', (e) => {
            isPreviewEnabled = e.target.checked;
            updateCardPriceBadges();
        });

        document.getElementById('vgen-toggle-unblur').addEventListener('change', (e) => {
            isMatureUnblurEnabled = e.target.checked;
        });

        const sortSelect = document.getElementById('vgen-sort-select');
        sortSelect.addEventListener('change', (e) => {
            sortType = e.target.value;
            applySortingState();
        });

        const minInput = document.getElementById('vgen-min-price');
        const maxInput = document.getElementById('vgen-max-price');

        const onPriceFilterChange = () => {
            minPriceInput = minInput.value.trim();
            maxPriceInput = maxInput.value.trim();
            applySortingState();
        };

        minInput.addEventListener('input', onPriceFilterChange);
        maxInput.addEventListener('input', onPriceFilterChange);

        document.getElementById('vgen-help-btn').addEventListener('click', () => {
            openHelpModal();
        });

        // 7. Create trigger button
        const trigger = document.createElement('button');
        trigger.id = 'vgen-unlocked-trigger';
        trigger.className = 'vgen-unlocked-trigger-btn';
        trigger.innerHTML = '🔓';
        trigger.title = 'Open VgenUnlocked';
        document.body.appendChild(trigger);

        const isMinimized = localStorage.getItem('vgen_unlocked_minimized') === 'true';
        if (isMinimized) {
            panel.style.display = 'none';
            trigger.style.display = 'flex';
        } else {
            panel.style.display = 'block';
            trigger.style.display = 'none';
        }

        // Minimize action
        document.getElementById('vgen-minimize-btn').addEventListener('click', () => {
            panel.style.display = 'none';
            trigger.style.display = 'flex';
            localStorage.setItem('vgen_unlocked_minimized', 'true');
        });

        // Restore action
        trigger.addEventListener('click', () => {
            panel.style.display = 'block';
            trigger.style.display = 'none';
            localStorage.removeItem('vgen_unlocked_minimized');
        });
    }

    // SPA routing monitor
    let lastUrl = window.location.href;
    setInterval(() => {
        if (window.location.href !== lastUrl) {
            lastUrl = window.location.href;
            console.log('[VGen Sorter] URL change detected. Refreshing panel and triggers.');
            // Re-apply NEXT_DATA check for newly navigated pages if available
            parseInitialData();
            triggerObserveAndRender();
        }
    }, 1000);

    // Watch for dynamic card mounts to render price badges and inject fiber hooks
    let renderTimeout = null;
    function triggerObserveAndRender() {
        if (renderTimeout) clearTimeout(renderTimeout);
        renderTimeout = setTimeout(() => {
            updateCardPriceBadges();
            ensureHooked();
        }, 150);
    }

    // Set up MutationObserver to watch DOM updates (like virtualization recyclings or scroll fetches)
    const observer = new MutationObserver((mutations) => {
        let shouldTrigger = false;
        for (const m of mutations) {
            if (m.addedNodes.length > 0) {
                shouldTrigger = true;
                break;
            }
        }
        if (shouldTrigger) {
            triggerObserveAndRender();
        }
    });

    // Run script initialization
    function init() {
        fetchExchangeRates();
        parseInitialData();
        setupMatureUnblurListeners();

        // Wait for body to inject UI controls
        if (document.body) {
            createControlPanel();
            showWelcomeIfNeeded();
            observer.observe(document.body, { childList: true, subtree: true });
            triggerObserveAndRender();
        } else {
            window.addEventListener('DOMContentLoaded', () => {
                createControlPanel();
                showWelcomeIfNeeded();
                observer.observe(document.body, { childList: true, subtree: true });
                triggerObserveAndRender();
            });
        }
    }

    init();

})();