Xbox PriceLens

Get a clear view of global Xbox pricing. PriceLens adds a powerful, customizable dashboard to game pages, showing you what a game costs in different countries—all in your home currency. Pin your favorite stores and let PriceLens help you focus on the best deals.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Xbox PriceLens
// @namespace    https://github.com/sinazadeh/userscripts
// @version      1.0.4
// @description  Get a clear view of global Xbox pricing. PriceLens adds a powerful, customizable dashboard to game pages, showing you what a game costs in different countries—all in your home currency. Pin your favorite stores and let PriceLens help you focus on the best deals.
// @author       TheSina
// @match        *://www.xbox.com/*/games/store/*
// @connect      cdn.jsdelivr.net
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @license      MIT
// ==/UserScript==
/* jshint esversion: 11 */
(async function () {
    'use strict';

    // 1) Centralized Configuration
    const CONFIG = {
        SELECTORS: {
            priceText: '.Price-module__boldText___1i2Li',
            insertionPoint: '.Price-module__priceBaseContainer___j9jGE',
            buyButton: 'button[data-m*="Buy"]',
            banner: '.xbox-banner',
        },
        API_BASE_URL:
            'https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/',
        RETRY_DELAY: 750,
        CACHE: {
            KEY_RATES_PREFIX: 'xboxCurrencyRates_v4.2_',
            KEY_TIMESTAMP_PREFIX: 'xboxCurrencyRatesTS_v4.2_',
            TTL: 12 * 60 * 60 * 1000,
        },
    };

    // 2) Style Injection
    function injectStyles() {
        const style = document.createElement('style');
        style.textContent = `
        .xbox-banner,
        .xbox-row,
        .xbox-modal,
        .xbox-modal-section,
        .xbox-settings-btn {
    font-family:
        system-ui,
        'Noto Sans',
        'Segoe UI',
        'Noto Color Emoji',
        'Apple Color Emoji',
        'Segoe UI Emoji',
        'Segoe UI Symbol',
        sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    font-size: 14px;
}
        .xbox-banner {
            position: relative;
            background: #fff;
            border-radius: 8px;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
            margin: 16px 0;
            padding: 12px 16px;
            font-size: 0.95rem;
            line-height: 1.4;
            color: #333;
            min-height: 50px;
        }

        .xbox-rows {
            display: flex;
            flex-wrap: wrap;
            gap: 8px;
            align-content: flex-start;
        }

        .xbox-row {
            flex: 1 1 calc(50% - 8px);
            background: #f9f9f9;
            padding: 8px;
            border-radius: 4px;
            direction: ltr;
            text-align: left;
            border-left: 3px solid transparent;
            transition:
                background-color 0.2s,
                border-color 0.2s;
        }

        .xbox-row.default-store-highlight {
            background-color: #e6ffed;
            border-left-color: #4caf50;
        }

        .xbox-row.error {
            color: #d32f2f;
        }

        .xbox-row.loading {
            width: 100%;
            display: flex;
            justify-content: center;
            align-items: center;
            color: #888;
            background: none;
        }

        .xbox-row .error-text {
            color: currentColor;
        }

        .rtl-text {
            direction: rtl;
            unicode-bidi: embed;
            display: inline-block;
        }

        .xbox-settings-btn {
            position: absolute;
            bottom: 8px;
            right: 8px;
            background: none;
            border: none;
            cursor: pointer;
            color: currentColor;
            padding: 4px;
            font-size: 1.2rem;
            z-index: 1;
        }

        .xbox-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.5);
            display: flex;
            align-items: center;
            justify-content: center;
            opacity: 0;
            transition: opacity 0.3s;
            z-index: 9999;
        }

        .xbox-overlay.show {
            opacity: 1;
        }

        .xbox-modal {
            background: #fff;
            color: #000;
            padding: 20px;
            border-radius: 8px;
            width: 500px;
            max-width: 95%;
            font-size: 1rem;
            line-height: 1.4;
        }

        .xbox-modal h3 {
            margin: 0 0 16px;
        }

        .xbox-modal h4 {
            margin: 16px 0 8px;
        }

        .stores-list {
            column-count: 2;
            column-gap: 20px;
        }

        .xbox-modal-section {
            margin-bottom: 16px;
        }

        .xbox-modal-section select {
            width: 100%;
            padding: 8px;
            border: 1px solid #ccc;
            border-radius: 4px;
        }

        .xbox-modal-actions {
            text-align: right;
            margin-top: 24px;
        }

        .xbox-modal-actions-links {
            margin-bottom: 16px;
        }

        .xbox-modal-actions-links a {
            margin-right: 12px;
            cursor: pointer;
        }

        .xbox-modal-actions button:not(:last-child) {
            margin-right: 8px;
        }

        .switch {
            display: flex;
            align-items: center;
            margin: 6px 0;
            cursor: pointer;
            break-inside: avoid-column;
        }

        .switch input {
            opacity: 0;
            width: 1px;
            height: 1px;
        }

        .switch .slider {
            width: 36px;
            height: 18px;
            background: #ccc;
            border-radius: 9px;
            margin-right: 10px;
            position: relative;
            transition: background 0.2s;
        }

        .switch .slider::after {
            content: '';
            position: absolute;
            width: 16px;
            height: 16px;
            top: 1px;
            left: 1px;
            background: #fff;
            border-radius: 50%;
            transition: transform 0.2s;
        }

        .switch input:checked + .slider {
            background: #4caf50;
        }

        .switch input:checked + .slider::after {
            transform: translateX(18px);
        }

        .switch input:focus-visible + .slider {
            box-shadow: 0 0 0 2px #0078d4;
        }

        @media (max-width: 420px) {
            .stores-list {
                column-count: 1;
            }
        }

        @media (prefers-color-scheme: dark) {
            .xbox-banner {
                background: #1e1e1e;
                color: #ddd;
            }

            .xbox-row {
                background: #2a2a2a;
            }

            .xbox-row.default-store-highlight {
                background-color: #1a3d20;
                border-left-color: #66bb6a;
            }

            .xbox-row.loading {
                color: #666;
            }

            .xbox-row.error {
                color: #ef5350;
            }

            .xbox-modal {
                background: #2a2a2a;
                color: #ccc;
            }

            .xbox-modal-section select {
                background: #333;
                color: #ccc;
                border-color: #555;
            }

            .xbox-settings-btn {
                color: #fff;
            }
        }
    `;
        document.head.appendChild(style);
    }

    // 3) Currency & Tax Definitions
    const CURRENCIES = [
        {
            code: 'ar',
            api: 'ars',
            region: 'es-ar',
            flag: '🇦🇷',
            name: 'Argentina Store',
            link: 'Argentina Store',
            tax: 0.7,
            decimal: ',',
            fmt: x =>
                `ARS${new Intl.NumberFormat('es-AR', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(x).replace(/\s/g, '')}`,
        },
        {
            code: 'au',
            api: 'aud',
            region: 'en-au',
            flag: '🇦🇺',
            name: 'Australia Store',
            link: 'Australia Store',
            tax: 0,
            decimal: '.',
            fmt: x =>
                `AU$${new Intl.NumberFormat('en-AU', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(x)}`,
        },
        {
            code: 'br',
            api: 'brl',
            region: 'pt-br',
            flag: '🇧🇷',
            name: 'Brazil Store',
            link: 'Brazil Store',
            tax: 0,
            decimal: ',',
            fmt: x =>
                new Intl.NumberFormat('pt-BR', {
                    style: 'currency',
                    currency: 'BRL',
                })
                    .format(x)
                    .replace(/\s/g, ''),
        },
        {
            code: 'ca',
            api: 'cad',
            region: 'en-ca',
            flag: '🇨🇦',
            name: 'Canada Store',
            link: 'Canada Store',
            tax: 0,
            decimal: '.',
            fmt: x =>
                `CAD ${new Intl.NumberFormat('en-CA', {style: 'currency', currency: 'CAD'}).format(x)}`,
            showPlusTax: true,
        },
        {
            code: 'ch',
            api: 'chf',
            region: 'de-ch',
            flag: '🇨🇭',
            name: 'Switzerland Store',
            link: 'Switzerland Store',
            tax: 0,
            decimal: '.',
            fmt: x =>
                `CHF ${new Intl.NumberFormat('de-CH', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(x)}`,
        },
        {
            code: 'cl',
            api: 'clp',
            region: 'es-cl',
            flag: '🇨🇱',
            name: 'Chile Store',
            link: 'Chile Store',
            tax: 0,
            decimal: ',',
            fmt: x => `$${new Intl.NumberFormat('es-CL').format(x)}`,
            showPlusTax: true,
        },
        {
            code: 'co',
            api: 'cop',
            region: 'es-co',
            flag: '🇨🇴',
            name: 'Colombia Store',
            link: 'Colombia Store',
            tax: 0,
            decimal: ',',
            fmt: x => `COP$${new Intl.NumberFormat('es-CO').format(x)}`,
            showPlusTax: true,
        },
        {
            code: 'cz',
            api: 'czk',
            region: 'cs-cz',
            flag: '🇨🇿',
            name: 'Czechia Store',
            link: 'Czechia Store',
            tax: 0,
            decimal: ',',
            fmt: x =>
                `${new Intl.NumberFormat('cs-CZ', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(x)} Kč`,
        },
        {
            code: 'gb',
            api: 'gbp',
            region: 'en-gb',
            flag: '🇬🇧',
            name: 'UK Store',
            link: 'UK Store',
            tax: 0,
            decimal: '.',
            fmt: x =>
                new Intl.NumberFormat('en-GB', {
                    style: 'currency',
                    currency: 'GBP',
                }).format(x),
        },
        {
            code: 'hk',
            api: 'hkd',
            region: 'en-hk',
            flag: '🇭🇰',
            name: 'Hong Kong Store',
            link: 'Hong Kong Store',
            tax: 0,
            decimal: '.',
            fmt: x =>
                `HK$${new Intl.NumberFormat('en-HK', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(x)}`,
        },
        {
            code: 'hu',
            api: 'huf',
            region: 'hu-hu',
            flag: '🇭🇺',
            name: 'Hungary Store',
            link: 'Hungary Store',
            tax: 0,
            decimal: ',',
            fmt: x =>
                `${new Intl.NumberFormat('hu-HU', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(x)} HUF`,
        },
        {
            code: 'in',
            api: 'inr',
            region: 'en-in',
            flag: '🇮🇳',
            name: 'India Store',
            link: 'India Store',
            tax: 0.18,
            decimal: '.',
            fmt: x =>
                new Intl.NumberFormat('en-IN', {
                    style: 'currency',
                    currency: 'INR',
                    minimumFractionDigits: 2,
                    maximumFractionDigits: 2,
                })
                    .format(x)
                    .replace(/\s/g, ''),
        },
        {
            code: 'jp',
            api: 'jpy',
            region: 'ja-jp',
            flag: '🇯🇵',
            name: 'Japan Store',
            link: 'Japan Store',
            tax: 0,
            decimal: '.',
            fmt: x =>
                new Intl.NumberFormat('ja-JP', {
                    style: 'currency',
                    currency: 'JPY',
                }).format(x),
        },
        {
            code: 'kr',
            api: 'krw',
            region: 'ko-kr',
            flag: '🇰🇷',
            name: 'South Korea Store',
            link: 'South Korea Store',
            tax: 0,
            decimal: '.',
            fmt: x =>
                new Intl.NumberFormat('ko-KR', {
                    style: 'currency',
                    currency: 'KRW',
                })
                    .format(x)
                    .replace(/\s/g, ''),
        },
        {
            code: 'mx',
            api: 'mxn',
            region: 'es-mx',
            flag: '🇲🇽',
            name: 'Mexico Store',
            link: 'Mexico Store',
            tax: 0,
            decimal: '.',
            fmt: x =>
                `MXN$${new Intl.NumberFormat('es-MX', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(x)}`,
        },
        {
            code: 'no',
            api: 'nok',
            region: 'nb-no',
            flag: '🇳🇴',
            name: 'Norway Store',
            link: 'Norway Store',
            tax: 0,
            decimal: ',',
            fmt: x =>
                `kr ${new Intl.NumberFormat('nb-NO', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(x)}`,
        },
        {
            code: 'nz',
            api: 'nzd',
            region: 'en-nz',
            flag: '🇳🇿',
            name: 'New Zealand Store',
            link: 'New Zealand Store',
            tax: 0,
            decimal: '.',
            fmt: x =>
                `NZ$${new Intl.NumberFormat('en-NZ', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(x)}`,
        },
        {
            code: 'pl',
            api: 'pln',
            region: 'pl-pl',
            flag: '🇵🇱',
            name: 'Poland Store',
            link: 'Poland Store',
            tax: 0,
            decimal: ',',
            fmt: x =>
                `${new Intl.NumberFormat('pl-PL', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(x)} zł`,
        },
        {
            code: 'sa',
            api: 'sar',
            region: 'ar-sa',
            flag: '🇸🇦',
            name: 'Saudi Arabia Store',
            link: 'Saudi Arabia Store',
            tax: 0,
            decimal: '.',
            fmt: x =>
                new Intl.NumberFormat('ar-SA', {
                    style: 'currency',
                    currency: 'SAR',
                }).format(x),
            preParse: str => str.replace(/ر\.س\.‏/g, ''),
            isRTL: true,
        },
        {
            code: 'se',
            api: 'sek',
            region: 'sv-se',
            flag: '🇸🇪',
            name: 'Sweden Store',
            link: 'Sweden Store',
            tax: 0,
            decimal: ',',
            fmt: x =>
                `${new Intl.NumberFormat('sv-SE', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(x)} kr`,
        },
        {
            code: 'sg',
            api: 'sgd',
            region: 'en-sg',
            flag: '🇸🇬',
            name: 'Singapore Store',
            link: 'Singapore Store',
            tax: 0,
            decimal: '.',
            fmt: x =>
                `S$${new Intl.NumberFormat('en-SG', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(x)}`,
        },
        {
            code: 'tr',
            api: 'try',
            region: 'tr-TR',
            flag: '🇹🇷',
            name: 'Turkey Store',
            link: 'Turkey Store',
            tax: 0,
            decimal: ',',
            fmt: x =>
                new Intl.NumberFormat('tr-TR', {
                    style: 'currency',
                    currency: 'TRY',
                    minimumFractionDigits: 2,
                    maximumFractionDigits: 2,
                })
                    .format(x)
                    .replace(/\s/g, ''),
        },
        {
            code: 'tw',
            api: 'twd',
            region: 'zh-tw',
            flag: '🇹🇼',
            name: 'Taiwan Store',
            link: 'Taiwan Store',
            tax: 0,
            decimal: '.',
            fmt: x => `NT$${new Intl.NumberFormat('zh-TW').format(x)}`,
        },
        {
            code: 'us',
            api: 'usd',
            region: 'en-us',
            flag: '🇺🇸',
            name: 'US Store',
            link: 'US Store',
            tax: 0,
            decimal: '.',
            fmt: x =>
                new Intl.NumberFormat('en-US', {
                    style: 'currency',
                    currency: 'USD',
                }).format(x),
            showPlusTax: true,
        },
        {
            code: 'za',
            api: 'zar',
            region: 'en-za',
            flag: '🇿🇦',
            name: 'South Africa Store',
            link: 'South Africa Store',
            tax: 0,
            decimal: ',',
            fmt: x =>
                `R ${new Intl.NumberFormat('en-ZA', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(x)}`,
        },
    ];

    // 4) Preference Management Module
    const Prefs = {
        visible: {},
        sortOrder: 'lowest',
        defaultStore: 'us',
        defaults: ['us', 'tr', 'in', 'ar'],
        async load() {
            this.visible = await GM_getValue('visibleCurrencies_v4.2', {});
            this.sortOrder = await GM_getValue(
                'currencySortOrder_v4.2',
                'lowest',
            );
            this.defaultStore = await GM_getValue('defaultStore_v4.2', 'us');
            let needsSave = false;
            CURRENCIES.forEach(c => {
                if (this.visible[c.code] === undefined) {
                    this.visible[c.code] = this.defaults.includes(c.code);
                    needsSave = true;
                }
            });
            if (needsSave) await this.save();
        },
        async save() {
            await GM_setValue('visibleCurrencies_v4.2', this.visible);
            await GM_setValue('currencySortOrder_v4.2', this.sortOrder);
            await GM_setValue('defaultStore_v4.2', this.defaultStore);
        },
    };

    // 5) Data Fetching, Caching & Parsing
    async function getRates(baseCurrencyApi) {
        const now = Date.now();
        const cacheKeyRates = `${CONFIG.CACHE.KEY_RATES_PREFIX}${baseCurrencyApi}`;
        const cacheKeyTs = `${CONFIG.CACHE.KEY_TIMESTAMP_PREFIX}${baseCurrencyApi}`;
        const ts = await GM_getValue(cacheKeyTs, 0);
        const cachedRates = await GM_getValue(cacheKeyRates, null);
        if (cachedRates && now - ts < CONFIG.CACHE.TTL) return cachedRates;

        return new Promise(resolve => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: `${CONFIG.API_BASE_URL}${baseCurrencyApi}.json`,
                onload: async r => {
                    if (r.status >= 200 && r.status < 300) {
                        try {
                            const data = JSON.parse(r.responseText)[
                                baseCurrencyApi
                            ];
                            await GM_setValue(cacheKeyRates, data);
                            await GM_setValue(cacheKeyTs, now);
                            resolve(data);
                        } catch (e) {
                            console.error('API parse failed:', e);
                            resolve(null);
                        }
                    } else {
                        console.error('API request failed:', r.statusText);
                        resolve(null);
                    }
                },
                onerror: e => {
                    console.error('API network error:', e);
                    resolve(null);
                },
            });
        });
    }

    function fetchWithRetry(currency, url, retries = 1) {
        return new Promise(resolve => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                onload: r => {
                    if (r.status >= 500 && retries > 0) {
                        setTimeout(
                            () =>
                                resolve(
                                    fetchWithRetry(currency, url, retries - 1),
                                ),
                            CONFIG.RETRY_DELAY,
                        );
                        return;
                    }
                    let priceStr = null,
                        error = null;
                    if (r.status >= 200 && r.status < 300) {
                        const doc = new DOMParser().parseFromString(
                            r.responseText,
                            'text/html',
                        );
                        priceStr =
                            doc
                                .querySelector(CONFIG.SELECTORS.priceText)
                                ?.textContent.replace(/\+\s*$/, '')
                                .trim() || null;
                        if (!priceStr) error = 'Price not found';
                    } else {
                        error = `Request failed (${r.status})`;
                    }
                    resolve({
                        code: currency.code,
                        priceStr,
                        error,
                    });
                },
                onerror: () => {
                    if (retries > 0) {
                        setTimeout(
                            () =>
                                resolve(
                                    fetchWithRetry(currency, url, retries - 1),
                                ),
                            CONFIG.RETRY_DELAY,
                        );
                    } else {
                        resolve({
                            code: currency.code,
                            priceStr: null,
                            error: 'Network Error',
                        });
                    }
                },
            });
        });
    }

    function fetchAllPrices(urls, currenciesToFetch) {
        const promises = currenciesToFetch.map(c =>
            fetchWithRetry(c, urls[c.code]),
        );
        return Promise.all(promises);
    }

    function parsePrice(priceString, separator = '.') {
        const cleanRegex = new RegExp(`[^\\d\\${separator}]`, 'g');
        const cleaned = priceString.replace(cleanRegex, '');
        const normalized =
            separator === '.' ? cleaned : cleaned.replace(separator, '.');
        return parseFloat(normalized);
    }

    // 6) UI & DOM Manipulation
    function createBanner() {
        const banner = document.createElement('div');
        banner.className = 'xbox-banner';
        const rowsContainer = document.createElement('div');
        rowsContainer.className = 'xbox-rows';
        const settingsBtn = document.createElement('button');
        settingsBtn.className = 'xbox-settings-btn';
        settingsBtn.textContent = '⚙️';
        settingsBtn.title = 'Settings';
        settingsBtn.setAttribute('aria-label', 'Price Settings');
        banner.append(rowsContainer, settingsBtn);
        return {
            banner,
            rowsContainer,
            settingsBtn,
        };
    }

    function updateBannerDisplay(container, lines) {
        const sorted = [...lines]
            .filter(item => Prefs.visible[item.code])
            .sort((a, b) => {
                // avoid the “line break before ‘?’” warning by using an if/else
                if (Prefs.sortOrder === 'alpha') {
                    return a.name.localeCompare(b.name);
                }
                return a.convertedPrice - b.convertedPrice;
            });

        container.innerHTML = '';

        if (sorted.length === 0) {
            container.innerHTML = `<div class="xbox-row">No stores selected.</div>`;
            return;
        }

        sorted.forEach(item => {
            const row = document.createElement('div');
            row.className = 'xbox-row';

            if (item.error) {
                row.classList.add('error');
            }
            if (item.code === Prefs.defaultStore) {
                row.classList.add('default-store-highlight');
            }

            row.innerHTML = item.html;
            container.appendChild(row);
        });
    }

    function showSettingsModal(lines, rowsContainer) {
        let lastFocusedElement = document.activeElement;
        const overlay = document.createElement('div');
        overlay.className = 'xbox-overlay';
        document.body.appendChild(overlay);
        requestAnimationFrame(() => overlay.classList.add('show'));

        const modal = document.createElement('div');
        modal.className = 'xbox-modal';
        modal.setAttribute('role', 'dialog');
        modal.setAttribute('aria-modal', 'true');
        modal.setAttribute('aria-labelledby', 'xbox-modal-title');

        const defaultStoreOptions = CURRENCIES.map(
            c =>
                `<option value="${c.code}" ${Prefs.defaultStore === c.code ? 'selected' : ''}>${c.flag} ${c.name}</option>`,
        ).join('');

        modal.innerHTML = `<h3 id="xbox-modal-title">Settings</h3>
            <div class="xbox-modal-section">
                <h4>Default Store (for Conversion)</h4>
                <select id="default-store-select">${defaultStoreOptions}</select>
            </div>
            <div class="xbox-modal-section">
                <h4>Visible Stores</h4>
                <div class="stores-list"></div>
                <div class="xbox-modal-actions-links">
                    <a href="#" data-action="default">Select Default</a>
                    <a href="#" data-action="all">Select All</a>
                    <a href="#" data-action="none">Select None</a>
                </div>
            </div>
            <div class="xbox-modal-section">
                <h4>Sort Order</h4>
                <select id="sort-order-select">
                    <option value="lowest" ${Prefs.sortOrder === 'lowest' ? 'selected' : ''}>Lowest Price</option>
                    <option value="alpha" ${Prefs.sortOrder === 'alpha' ? 'selected' : ''}>Alphabetical</option>
                </select>
            </div>
            <div class="xbox-modal-actions">
                <button class="cancel">Cancel</button>
                <button class="save">Save</button>
            </div>`;

        const storesList = modal.querySelector('.stores-list');
        CURRENCIES.forEach(c => {
            storesList.insertAdjacentHTML(
                'beforeend',
                `<label class="switch">
                <input type="checkbox" value="${c.code}" ${Prefs.visible[c.code] ? 'checked' : ''}>
                <span class="slider" role="presentation"></span><span>${c.flag} ${c.link}</span></label>`,
            );
        });
        overlay.appendChild(modal);

        const focusableElements = modal.querySelectorAll(
            'button, [href], input, select',
        );
        const firstFocusable = focusableElements[0];
        const lastFocusable = focusableElements[focusableElements.length - 1];
        firstFocusable.focus();

        const handleKeyDown = e => {
            if (e.key === 'Escape') {
                close();
                return;
            }
            if (e.key === 'Tab') {
                if (e.shiftKey) {
                    if (document.activeElement === firstFocusable) {
                        e.preventDefault();
                        lastFocusable.focus();
                    }
                } else {
                    if (document.activeElement === lastFocusable) {
                        e.preventDefault();
                        firstFocusable.focus();
                    }
                }
            }
        };

        const close = () => {
            overlay.classList.remove('show');
            overlay.addEventListener(
                'transitionend',
                () => {
                    overlay.remove();
                    document.removeEventListener('keydown', handleKeyDown);
                    lastFocusedElement?.focus();
                },
                {
                    once: true,
                },
            );
        };

        document.addEventListener('keydown', handleKeyDown);
        modal.querySelector('.cancel').addEventListener('click', close);
        overlay.addEventListener('click', e => {
            if (e.target === overlay) close();
        });
        modal.querySelector('.save').addEventListener('click', async () => {
            modal.querySelectorAll('.stores-list input').forEach(cb => {
                Prefs.visible[cb.value] = cb.checked;
            });
            Prefs.sortOrder = modal.querySelector('#sort-order-select').value;
            Prefs.defaultStore = modal.querySelector(
                '#default-store-select',
            ).value;
            await Prefs.save();
            runScript(true);
            close();
        });
        modal
            .querySelector('.xbox-modal-actions-links')
            .addEventListener('click', e => {
                e.preventDefault();
                const action = e.target.dataset.action;
                if (!action) return;
                modal.querySelectorAll('.stores-list input').forEach(cb => {
                    if (action === 'all') cb.checked = true;
                    else if (action === 'none') cb.checked = false;
                    else if (action === 'default')
                        cb.checked = Prefs.defaults.includes(cb.value);
                });
            });
    }

    // 7) Main Execution Logic
    async function main(sku) {
        const anchor = document.querySelector(CONFIG.SELECTORS.insertionPoint);
        if (!anchor) return;

        const {banner, rowsContainer, settingsBtn} = createBanner();
        banner.dataset.xboxSku = sku;
        rowsContainer.innerHTML = `<div class="xbox-row loading">Loading prices...</div>`;
        anchor.parentNode.insertBefore(banner, anchor.nextSibling);

        const parts = location.pathname.split('/').filter(p => p);
        const storeIdx = parts.indexOf('store');
        const [, slug, prod] = parts.slice(storeIdx);
        const currenciesToFetch = CURRENCIES.filter(c => Prefs.visible[c.code]);

        if (currenciesToFetch.length === 0) {
            updateBannerDisplay(rowsContainer, []);
            settingsBtn.addEventListener('click', () =>
                showSettingsModal([], rowsContainer),
            );
            return;
        }

        const urls = Object.fromEntries(
            CURRENCIES.map(c => [
                c.code,
                `https://www.xbox.com/${c.region}/games/store/${slug}/${prod}/${sku}`,
            ]),
        );
        const priceResults = await fetchAllPrices(urls, currenciesToFetch);
        const rawPrices = Object.fromEntries(
            priceResults.map(r => [
                r.code,
                {
                    priceStr: r.priceStr,
                    error: r.error,
                },
            ]),
        );
        const parsedValues = {};
        currenciesToFetch.forEach(c => {
            const raw = rawPrices[c.code];
            if (raw && raw.priceStr) {
                try {
                    let strToParse = raw.priceStr;
                    if (c.preParse) {
                        strToParse = c.preParse(strToParse);
                    }
                    parsedValues[c.code] = parsePrice(strToParse, c.decimal);
                } catch (e) {
                    raw.error = 'Parse failed';
                }
            }
        });

        const defaultCurrency = CURRENCIES.find(
            c => c.code === Prefs.defaultStore,
        );
        const rates = await getRates(defaultCurrency.api);

        if (!rates || !defaultCurrency) {
            rowsContainer.innerHTML = `<div class="xbox-row error">Could not load exchange rates for ${defaultCurrency?.name || 'default store'}.</div>`;
            return;
        }

        const displayLines = CURRENCIES.map(c => {
            if (!Prefs.visible[c.code]) {
                return {
                    code: c.code,
                    html: '',
                };
            }
            const value = parsedValues[c.code];
            const raw = rawPrices[c.code];
            let html,
                convertedPrice = Infinity;

            const linkHtml = `<a href="${urls[c.code]}" target="_blank" rel="noopener noreferrer">${c.name}</a>`;
            const nameWithFlag = `${c.flag} ${linkHtml}`;

            if (raw.error) {
                console.warn(`Could not fetch price for ${c.name}:`, raw.error);
                html = `${nameWithFlag}: <span class="error-text" title="${raw.error}">⚠️ Couldn’t load</span>`;
            } else if (value != null) {
                convertedPrice = value / rates[c.api];
                html = `${nameWithFlag}: ${defaultCurrency.fmt(convertedPrice)}`;

                if (c.code !== defaultCurrency.code) {
                    let formattedLocal = c.fmt(value);
                    if (c.isRTL) {
                        formattedLocal = `<span class="rtl-text">${formattedLocal}</span>`;
                    }
                    html += ` (${formattedLocal})`;
                }

                if (c.tax > 0) {
                    const totalConverted = convertedPrice * (1 + c.tax);
                    const totalLocalValue = value * (1 + c.tax);
                    let formattedTaxLocal = c.fmt(totalLocalValue);

                    if (c.isRTL) {
                        formattedTaxLocal = `<span class="rtl-text">${formattedTaxLocal}</span>`;
                    }
                    html += ` + Tax = ${defaultCurrency.fmt(totalConverted)}`;
                    if (c.code !== defaultCurrency.code) {
                        html += ` (${formattedTaxLocal})`;
                    }
                    convertedPrice = totalConverted;
                } else if (c.showPlusTax) {
                    html += ` + Tax`;
                }
            } else {
                html = `${nameWithFlag}: Not available`;
            }

            return {
                code: c.code,
                name: c.name,
                html,
                convertedPrice,
                error: raw.error,
            };
        });

        settingsBtn.addEventListener('click', () =>
            showSettingsModal(displayLines, rowsContainer),
        );
        updateBannerDisplay(rowsContainer, displayLines);
    }

    function getCurrentSku() {
        try {
            const buyButton = document.querySelector(
                CONFIG.SELECTORS.buyButton,
            );
            if (!buyButton) return null;
            const mData = JSON.parse(buyButton.dataset.m || '{}');
            return mData.sku || null;
        } catch (e) {
            return null;
        }
    }

    // --- Script Initialization and SPA Navigation Handling ---
    let debounceTimer;
    async function runScript(forceRefresh = false) {
        observer.disconnect();
        try {
            const currentSku = getCurrentSku();
            const existingBanner = document.querySelector(
                CONFIG.SELECTORS.banner,
            );
            if (!currentSku) {
                existingBanner?.remove();
                return;
            }
            if (
                !forceRefresh &&
                existingBanner &&
                existingBanner.dataset.xboxSku === currentSku
            ) {
                // re-observe even if we don't run
            } else {
                existingBanner?.remove();
                const insertionPoint = document.querySelector(
                    CONFIG.SELECTORS.insertionPoint,
                );
                if (insertionPoint) {
                    await main(currentSku);
                }
            }
        } catch (error) {
            console.error('Error during script execution:', error);
        } finally {
            observer.observe(document.body, {
                childList: true,
                subtree: true,
            });
        }
    }
    const observer = new MutationObserver(() => {
        clearTimeout(debounceTimer);
        debounceTimer = setTimeout(() => runScript(false), 300);
    });
    injectStyles();
    await Prefs.load();
    await runScript(true);
})();