XBDeals Price Toolkit

The essential toolkit for xbdeals.net. Converts all prices (lists, history, charts) to USD

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         XBDeals Price Toolkit
// @namespace    https://github.com/sinazadeh/userscripts
// @version      1.0.2
// @description  The essential toolkit for xbdeals.net. Converts all prices (lists, history, charts) to USD
// @author       TheSina
// @match        *://xbdeals.net/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @connect      cdn.jsdelivr.net
// @license      MIT
// ==/UserScript==
/* jshint esversion: 11 */
(function () {
    'use strict';

    // --- Constants and Global State ---
    const API_URL =
        'https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/usd.json';
    const hiddenStores = ['ae'];
    const currencyMap = {
        ae: 'aed',
        ar: 'ars',
        at: 'eur',
        au: 'aud',
        be: 'eur',
        br: 'brl',
        ca: 'cad',
        ch: 'chf',
        cl: 'clp',
        co: 'cop',
        cz: 'czk',
        de: 'eur',
        dk: 'dkk',
        es: 'eur',
        fi: 'eur',
        fr: 'eur',
        gb: 'gbp',
        gr: 'eur',
        hk: 'hkd',
        hu: 'huf',
        ie: 'eur',
        il: 'ils',
        in: 'inr',
        it: 'eur',
        jp: 'jpy',
        kr: 'krw',
        mx: 'mxn',
        nl: 'eur',
        no: 'nok',
        nz: 'nzd',
        pl: 'pln',
        pt: 'eur',
        sa: 'sar',
        se: 'sek',
        sg: 'sgd',
        sk: 'eur',
        tr: 'try',
        tw: 'twd',
        us: 'usd',
        za: 'zar',
    };

    let ratesCache = null;

    // --- Utility Functions ---

    GM_addStyle(`
    .usd-price-appendix {
        display: block; font-size: 0.8em; font-weight: normal;
        color: #888; margin-top: 2px;
    }
    `);

    /**
     * Parses a price string into a float. Handles "FREE".
     */
    function parseAmount(text) {
        const cleanText = text.trim().toUpperCase();
        if (cleanText === 'FREE') return 0;

        let cleaned = text.replace(/[^\d.,]/g, '');
        if (cleaned.includes(',') && cleaned.includes('.')) {
            const lastComma = cleaned.lastIndexOf(',');
            const lastDot = cleaned.lastIndexOf('.');
            if (lastDot > lastComma) cleaned = cleaned.replace(/,/g, '');
            else cleaned = cleaned.replace(/\./g, '').replace(',', '.');
        } else if (cleaned.includes(',')) {
            const parts = cleaned.split(',');
            if (parts.length === 2 && parts[1].length <= 2)
                cleaned = cleaned.replace(',', '.');
            else cleaned = cleaned.replace(/,/g, '');
        }
        return parseFloat(cleaned);
    }

    async function fetchRates() {
        if (ratesCache) return ratesCache;
        return new Promise(resolve => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: API_URL,
                onload: res => {
                    try {
                        ratesCache = JSON.parse(res.responseText).usd;
                        resolve(ratesCache);
                    } catch (e) {
                        console.error('Error parsing currency data:', e);
                        resolve(null);
                    }
                },
                onerror: err => {
                    console.error('Error fetching currency data:', err);
                    resolve(null);
                },
            });
        });
    }

    // --- Page-Specific Conversion Functions ---

    async function convertAndSort(container) {
        // Mark as processed immediately to prevent multiple runs on the same mutation event
        container.dataset.usdConverted = 'true';
        console.log('Running conversion for price comparison list...');

        const rates = await fetchRates();
        if (!rates) {
            delete container.dataset.usdConverted; // Allow a retry if API fails
            return;
        }

        container
            .closest('.compare-prices-container')
            ?.style.setProperty('height', 'auto', 'important');
        container.style.display = 'grid';
        container.style.gridTemplateColumns = 'repeat(3, 1fr)';
        container.style.gap = '10px';

        const entries = Array.from(container.querySelectorAll('a'))
            .map(link => {
                const priceEl = link.querySelector('.compare-prices-price');
                if (!priceEl) return null;

                const regionCode = link
                    .getAttribute('href')
                    ?.match(/^\/([a-z]{2})-store/i)?.[1]
                    .toLowerCase();
                if (!regionCode || hiddenStores.includes(regionCode))
                    return null;

                const currencyCode = currencyMap[regionCode];
                const rate = rates[currencyCode];
                if (!currencyCode || !rate) return null;

                const rawText = priceEl.textContent
                    .replace(/\(≈.*?\)/g, '')
                    .trim();
                const localAmount = parseAmount(rawText);
                if (isNaN(localAmount)) return null;

                const usd = localAmount / rate;
                priceEl.textContent = `${rawText} ($${usd.toFixed(2)} USD)`;
                return {link, usdValue: usd};
            })
            .filter(Boolean);

        if (entries.length > 0) {
            entries.sort((a, b) => a.usdValue - b.usdValue);
            container.innerHTML = '';
            entries.forEach(entry => container.appendChild(entry.link));
        }
    }

    async function convertPriceHistory(container) {
        container.dataset.usdConverted = 'true';
        console.log('Running conversion for price history...');

        const rates = await fetchRates();
        if (!rates) {
            delete container.dataset.usdConverted; // Allow retry
            return;
        }

        const breadcrumbLink = document.querySelector(
            '.breadcrumb a[itemprop="item"]',
        );
        const regionCode = breadcrumbLink
            ?.getAttribute('href')
            ?.match(/^\/([a-z]{2})-store/i)?.[1]
            .toLowerCase();
        if (!regionCode) return;

        const currencyCode = currencyMap[regionCode];
        const rate = rates[currencyCode];
        if (!currencyCode || !rate) return;

        container.querySelectorAll('.game-stats-col-number-big').forEach(el => {
            if (el.parentNode.querySelector('.usd-price-appendix')) return; // Already done
            const localAmount = parseAmount(el.textContent);
            if (!isNaN(localAmount)) {
                const usd = localAmount / rate;
                const usdText = document.createElement('small');
                usdText.className = 'usd-price-appendix';
                usdText.textContent = `($${usd.toFixed(2)} USD)`;
                el.parentNode.appendChild(usdText);
            }
        });
    }

    // --- Main Execution Logic ---

    function runConversions() {
        // PATIENCE: Check for a container that is NOT processed AND has content.
        const listContainer = document.querySelector(
            '#compare-prices:not([data-usd-converted])',
        );
        if (
            listContainer &&
            listContainer.querySelector('a .compare-prices-price')
        ) {
            convertAndSort(listContainer);
        }

        // PATIENCE: Check for a container that is NOT processed AND has content.
        const historyContainer = document.querySelector(
            '.game-stats-price-history:not([data-usd-converted])',
        );
        if (
            historyContainer &&
            historyContainer.querySelector('.game-stats-col-number-big')
        ) {
            convertPriceHistory(historyContainer);
        }
    }

    // PERSISTENCE: This observer keeps watching the page for new content.
    const observer = new MutationObserver(runConversions);
    observer.observe(document.body, {
        childList: true,
        subtree: true,
    });
})();