Currency Converter

try to take over the world!

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Currency Converter
// @namespace    http://tampermonkey.net/
// @version      2.3.0
// @description  try to take over the world!
// @icon         http://store.steampowered.com/favicon.ico
// @author       Bisumaruko
// @include      http*://yuplay.ru/*
// @include      http*://*.gamersgate.com/*
// @include      http*://www.greenmangaming.com/*
// @include      http*://gama-gama.ru/*
// @include      http*://*.gamesplanet.com/*
// @include      http*://www.cdkeys.com/*
// @include      http*://directg.net/*
// @include      http*://www.humblebundle.com/*
// @include      http*://www.indiegala.com/*
// @include      http*://www.bundlestars.com/*
// @include      http*://www.opiumpulses.com/*
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.slim.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/limonte-sweetalert2/6.6.6/sweetalert2.min.js
// @resource     SweetAlert2CSS https://cdnjs.cloudflare.com/ajax/libs/limonte-sweetalert2/6.6.6/sweetalert2.min.css
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @run-at       document-start
// @connect      www.ecb.europa.eu
// ==/UserScript==

/* global swal, games */

// inject swal css
GM_addStyle(
    GM_getResourceText('SweetAlert2CSS'),
);

// setup swal
swal.setDefaults({
    timer: 3000,
    useRejections: false,
});

// load config
const config = JSON.parse(
    GM_getValue('Bisko_CC', '{}'),
);
const interval = 3 * 60 * 60 * 1000; // update exchange rate every 3 hours
const currencies = {
    ORI: {
        nameEN: '',
        nameZH: '恢復',
        symbol: '',
    },
    AUD: {
        nameEN: 'Australian Dollar',
        nameZH: '澳幣',
        symbol: 'AU$',
    },
    CAD: {
        nameEN: 'Canadian Dollar',
        nameZH: '加幣',
        symbol: 'CA$',
    },
    CNY: {
        nameEN: 'Chinese Yuan',
        nameZH: '人民幣',
        symbol: 'CN¥',
    },
    EUR: {
        nameEN: 'Euro',
        nameZH: '歐元',
        symbol: '€',
    },
    GBP: {
        nameEN: 'Great Britain Pound',
        nameZH: '英鎊',
        symbol: '£',
    },
    HKD: {
        nameEN: 'Hong Kong Dollar',
        nameZH: '港幣',
        symbol: 'HK$',
    },
    JPY: {
        nameEN: 'Japanese Yen',
        nameZH: '日圓',
        symbol: 'JP¥',
    },
    KRW: {
        nameEN: 'South Korean Won',
        nameZH: '韓圓',
        symbol: '₩',
    },
    MYR: {
        nameEN: 'Malaysian Ringgit',
        nameZH: '令吉',
        symbol: 'RM',
    },
    NTD: {
        nameEN: 'New Taiwan Dollar',
        nameZH: '台幣',
        symbol: 'NT$',
    },
    NZD: {
        nameEN: 'New Zealand Dollar',
        nameZH: '紐幣',
        symbol: 'NZ$',
    },
    RUB: {
        nameEN: 'Russian Ruble',
        nameZH: '盧布',
        symbol: 'руб',
    },
    USD: {
        nameEN: 'United States Dollar',
        nameZH: '美元',
        symbol: 'US$',
    },
};

let originalCurrency = 'USD';

// jQuery extension
$.fn.price = function price() {
    return this.eq(0).text().replace(/[^.0-9]/g, '');
};

$.fn.bindPriceData = function bindPriceData() {
    return this.each((index, element) => {
        const $ele = $(element);

        $ele
            .addClass('CCPrice')
            .attr('data-CCPrice', JSON.stringify({
                currency: originalCurrency,
                price: $ele.text().replace(/[^.0-9]/g, ''),
            }));
    });
};

// constructing functions
const has = Object.prototype.hasOwnProperty;
const preferredCurrency = () => (has.call(config.exchangeRate.rates, config.preferredCurrency) ? config.preferredCurrency : 'CNY'); // default to CNY
const updateCurrency = (currency = null) => {
    let targetCurrency = currency || preferredCurrency();

    $('.CCPrice').each((index, element) => {
        const $ele = $(element);
        const data = JSON.parse(
            $ele.attr('data-CCPrice'),
        );

        let convertedPrice = 0;

        if (targetCurrency === 'ORI') {
            targetCurrency = data.currency;
            convertedPrice = data.price;
        } else {
            const rates = config.exchangeRate.rates;

            convertedPrice = data.price * (rates[targetCurrency] / rates[data.currency]);
        }

        $ele.text(
            convertedPrice
                .toLocaleString('en', {
                    style: 'currency',
                    currency: targetCurrency,
                    maximumFractionDigits: 2,
                })
                .replace('MYR', currencies.MYR.symbol)
                .replace('NTD', currencies.NTD.symbol),
        );
    });
};
const constructMenu = () => {
    const $li = $('<li class="Bisko_CC_Menu"><a>Currencies</a></li>');
    const $ul = $('<ul></ul>').appendTo($li);
    const preferred = preferredCurrency();

    GM_addStyle(`
        .Bisko_CC_Menu ul {
            display: none;
            position: absolute;
            padding: 0;
            background-color: #272727;
            z-index: 9999;
        }
        .Bisko_CC_Menu:hover ul { display: block; }
        .Bisko_CC_Menu li { padding: 2px 10px; list-style-type: none; cursor: pointer; }
        .Bisko_CC_Menu li:hover, .preferred { background-color: SandyBrown; }
    `);

    Object.keys(currencies).forEach((currency) => {
        const itemName = `${currency} ${currencies[currency].nameZH}`;

        $ul.append(
            $(`<li class="${currency}">${itemName}</li>`)
                .addClass(() => (preferred === currency ? 'preferred' : ''))
                .click(() => {
                    config.preferredCurrency = currency;

                    GM_setValue('Bisko_CC', JSON.stringify(config));
                    updateCurrency(currency);

                    $('.Bisko_CC_Menu .preferred').removeClass('preferred');
                    $(`.Bisko_CC_Menu .${currency}`).addClass('preferred');
                }),
        );
    });

    return $li;
};
const handler = () => {
    switch (location.host) {
        case 'yuplay.ru':
            originalCurrency = 'RUB';

            GM_addStyle(`
                .games-pricedown span.CCPrice { font-size: 18px; }
                .good-title span.CCPrice { margin-right: 3px; font-size: 22px; }
            `);

            $('.header-right').append(
                constructMenu(),
            );
            // homepage games-pricedown
            $('.games-pricedown .sale > s, .games-pricedown .price').bindPriceData();
            // homepage game box & product page
            $('.games-box .price, .good-title .price')
                .contents()
                .each((index, node) => {
                    const $node = $(node);
                    const text = $node.text().trim();

                    if (node.tagName === 'S') $node.bindPriceData(); // retail price
                    if (node.tagName === 'SPAN') $node.remove(); // currency
                    else if (node.nodeType === 3 && text.length > 0) { // sales price
                        $node.replaceWith(
                            $(`<span>${text}<span>`).bindPriceData(),
                        );
                    }
                });
            break;
        case 'gama-gama.ru':
            originalCurrency = 'RUB';

            GM_addStyle(`
                .Bisko_CC_Menu > a, .Bisko_CC_Menu li { color: white; }
                .Bisko_CC_Menu ul { margin: 0; }
            `);

            $('#top_back').append(
                constructMenu()
                    .wrapInner('<div class="Bisko_CC_Menu top_menu"></div>')
                    .children()
                    .unwrap(),
            );
            // homepage
            $('.price_1, .old_price, .promo_price').bindPriceData();
            // product page
            $('.card-info-oldprice > span, .card-info-price > span').bindPriceData();
            $('.card-info-oldprice, .card-info-price')
                .contents()
                .filter((i, node) => node.nodeType !== 1)
                .remove();
            break;
        case 'ru.gamersgate.com':
        case 'cn.gamersgate.com':
            if (location.host.startsWith('ru')) originalCurrency = 'RUB';
            if (location.host.startsWith('cn')) originalCurrency = 'CNY';

            GM_addStyle(`
                .Bisko_CC_Menu { width: 49px; text-align: center; }
                .Bisko_CC_Menu svg {
                    width: 36px;
                    height: 36px;
                    background-color: #093745;
                    border: 1px solid #2c7c92;
                }
                .Bisko_CC_Menu, .Bisko_CC_Menu li { background-image: none !important; color: white; }
                .Bisko_CC_Menu li {
                    height: initial !important;
                    float: none !important;
                    padding: 5px 10px !important;
                }
                .Bisko_CC_Menu li:hover, .preferred { background-color: SandyBrown !important; }
            `);

            $('.btn_menuseparator').replaceWith(
                constructMenu(),
            );
            $('.Bisko_CC_Menu a').replaceWith(`
                <svg viewBox="0 0 24 24">
                    <path fill="#a9ebea" d="M11.8,10.9C9.53,10.31 8.8,9.7 8.8,8.75C8.8,7.66 9.81,6.9 11.5,6.9C13.28,6.9 13.94,7.75 14,9H16.21C16.14,7.28 15.09,5.7 13,5.19V3H10V5.16C8.06,5.58 6.5,6.84 6.5,8.77C6.5,11.08 8.41,12.23 11.2,12.9C13.7,13.5 14.2,14.38 14.2,15.31C14.2,16 13.71,17.1 11.5,17.1C9.44,17.1 8.63,16.18 8.5,15H6.32C6.44,17.19 8.08,18.42 10,18.83V21H13V18.85C14.95,18.5 16.5,17.35 16.5,15.3C16.5,12.46 14.07,11.5 11.8,10.9Z" />
                </svg>
            `);
            // homepage & product page
            $(`
                .prtag > span,
                .grid-old-price,
                .price_price > span,
                div > span > .bold.white,
                li > .f_right:nth-child(2) > span
            `).bindPriceData();
            break;
        case 'www.greenmangaming.com':
            GM_addStyle(`
                .Bisko_CC_Menu > a {
                    margin: 0 !important;
                    padding: 6px 15px !important;
                    font-size: 14px;
                }
                .Bisko_CC_Menu > a:hover, .Bisko_CC_Menu li:hover, .preferred { background-color: #494a4f !important; }
            `);

            $('.megamenu').append(
                constructMenu(),
            );

            try {
                // product page
                originalCurrency = games.Currency;
                $('price > span').bindPriceData();
            } catch (e) {
                // home page & search page
                const currencyObserver = new MutationObserver((mutations) => {
                    mutations.forEach((mutation) => {
                        mutation.addedNodes.forEach((addedNode) => {
                            if (addedNode.tagName === 'SCRIPT' && addedNode.src.includes('bam.nr-data.net')) {
                                const currency = decodeURI(addedNode.src).split('currency_code":"').pop().slice(0, 3);

                                if (currency.length > 0) {
                                    originalCurrency = currency;

                                    const $prices = $('.prices > span, .prices > p, .listing-price > span');
                                    // offset decimal place in Europe (. ,)
                                    if (['EUR', 'RUB'].includes(currency)) $prices.text((i, text) => text.replace(',', '.'));

                                    $prices.bindPriceData();
                                    updateCurrency();
                                }

                                currencyObserver.disconnect();
                            }
                        });
                    });
                });

                currencyObserver.observe(document.head, { childList: true });

                // search listing
                const $listing = $('.table-search-listings');

                if ($listing.length > 0) {
                    new MutationObserver((mutations) => {
                        mutations.forEach((mutation) => {
                            mutation.addedNodes.forEach((addedNode) => {
                                $(addedNode).find('.listing-price > span').bindPriceData();
                                updateCurrency();
                            });
                        });
                    }).observe($listing[0], { childList: true });
                }
            }
            break;
        case 'uk.gamesplanet.com':
        case 'de.gamesplanet.com':
        case 'fr.gamesplanet.com': {
            originalCurrency = !location.host.startsWith('uk') ? 'EUR' : 'GBP';

            const GPHandler = () => {
                const $prices = $('.price_base > strike, .price_current');

                $('.container > ul').append(
                    constructMenu(),
                );
                // offset decimal place in Europe (. ,)
                if (!location.host.startsWith('uk')) {
                    $prices.text((i, text) => text.replace(',', '.'));
                }

                $prices.bindPriceData();
                updateCurrency();
            };

            GPHandler(true);

            new MutationObserver((mutations) => {
                mutations.forEach((mutation) => {
                    mutation.removedNodes.forEach((removedNode) => {
                        if (removedNode.id === 'nprogress') GPHandler();
                    });
                });
            }).observe(document, {
                childList: true,
                subtree: true,
            });
            break;
        }
        case 'www.cdkeys.com': {
            originalCurrency = $('.currency-switcher:first-child .value').text();
            const currenciesDefault = ['AUD', 'CAD', 'EUR', 'GBP', 'JPY', 'NZD', 'USD'];
            const currenciesAppend = ['CNY', 'HKD', 'KRW', 'MYR', 'NTD', 'RUB'];
            // bind event to default currency switcher to save preferrred currency
            $('.currency-switcher:first-child ul span').each((index, element) => {
                const $ele = $(element);
                const currency = $ele.text().slice(0, 3);

                if (currenciesDefault.includes(currency)) {
                    $ele.parent().click(() => {
                        config.preferredCurrency = currency;

                        GM_setValue('Bisko_CC', JSON.stringify(config));
                    });
                }
            });
            // append currencies not in the default currency switcher
            currenciesAppend.forEach((currency) => {
                $('.currency-switcher:first-child ul').append(
                    $(`<li><a>${currency} - ${currencies[currency].nameEN}</a></li>`).click(() => {
                        config.preferredCurrency = currency;

                        GM_setValue('Bisko_CC', JSON.stringify(config));
                        updateCurrency(currency);
                    }),
                );
            });

            $('.price').bindPriceData();
            break;
        }
        case 'directg.net':
            originalCurrency = 'KRW';

            GM_addStyle(`
                .Bisko_CC_Menu ul {
                    width: 180px;
                    border-top: 3px solid #67c1f5;
                    background-color: #1b2838 !important;
                    opacity: 0.95;
                    color: rgba(255,255,255,0.5) !important;
                }
                .Bisko_CC_Menu ul li { padding: 10px 30px; font-weight: bold; }
                .Bisko_CC_Menu ul li:hover, .preferred { background-color: initial !important; color: white; }
            `);

            $('.nav').append(
                constructMenu(),
            );
            $('span.PricebasePrice, span.PricesalesPrice')
                .bindPriceData()
                .next('span[itemprop="priceCurrency"]')
                .remove();
            break;/*
        case 'www.origin.com': {
            const region = location.pathname.split('/')[1];
            const currencyRegion = {
                twn: 'NTD',
                jpn: 'JPY',
                rus: 'RUB',
                usa: 'USD',
                nzl: 'NZD',
                irl: 'EUR',
                kor: 'KRW',
            };
        }*/
        case 'www.humblebundle.com':
            originalCurrency = 'USD';

            GM_addStyle(`
                .Bisko_CC_Menu { float: left; }
                .Bisko_CC_Menu > a { display: block; padding: 15px 20px; }
                .Bisko_CC_Menu ul { background-color: #494f5c !important; color: rgba(255, 255, 255, 0.6); }
                .Bisko_CC_Menu ul li { padding: 5px 10px; }
                .Bisko_CC_Menu ul li:hover, .preferred { background-color: initial !important; color: white; }
            `);

            $('.nav:first-child').append(
                constructMenu(),
            );

            new MutationObserver((mutations) => {
                mutations.forEach((mutation) => {
                    mutation.removedNodes.forEach((removedNode) => {
                        if (removedNode.data === 'The Humble Store: Loading') {
                            $('.price, .store-price, .full-price, .current-price').bindPriceData();
                            updateCurrency();
                        }
                    });
                });
            }).observe(document.head, {
                childList: true,
                subtree: true,
            });
            break;
        case 'www.indiegala.com':
            originalCurrency = 'USD';

            GM_addStyle(`
                .Bisko_CC_Menu ul {
                    transform: translateY(-100%);
                    background-color: #474747 !important;
                    border: 1px solid #272727;
                }
                .Bisko_CC_Menu ul li { padding: 8px 15px; color: #dad6ca; }
                .Bisko_CC_Menu ul li:hover, .preferred { background-color: #2E2E2E !important; color: white; }
            `);

            $('#libdContainer > ul:not(:first-child)').append(
                constructMenu(),
            );
            $('.Bisko_CC_Menu > a').addClass('libd-group-item libd-bounce');
            // homepage & product page
            $('.inner-info, .inner, .price-container')
                .contents()
                .each((index, node) => {
                    const $node = $(node);
                    const text = $node.text().trim();

                    $node.parent().parent().css('overflow', 'hidden');
                    if (node.nodeType === 1 && node.tagName !== 'BR') $node.bindPriceData();
                    else if (node.nodeType === 3 && text.length) {
                        $node.replaceWith(
                            $(`<a>${text}</a>`).bindPriceData(),
                        );
                    }
                });
            // search result
            $('.price-cont').bindPriceData();
            break;
        case 'www.bundlestars.com':
            originalCurrency = 'USD';

            GM_addStyle(`
                .Bisko_CC_Menu ul { background-color: #212121 !important; }
                .Bisko_CC_Menu ul li { padding: 8px 15px; }
                .Bisko_CC_Menu ul li:hover, .preferred { background-color: #1A1A1A !important; }
            `);

            $('.nav').eq(0).append(
                constructMenu(),
            );
            new MutationObserver((mutations) => {
                mutations.forEach((mutation) => {
                    mutation.removedNodes.forEach((removedNode) => {
                        if (removedNode.id === 'loading-bar-spinner') {
                            $('.bs-price, .bs-currency-discount > span, .bs-currency-price').bindPriceData();
                            $('.bs-card-meta > .bs-pricing, .bs-card-meta > .bs-currency-discount').css({
                                position: 'absolute',
                                right: '19px',
                            });
                            updateCurrency();
                        }
                    });
                });
            }).observe(document.body, { childList: true });
            break;
        case 'www.opiumpulses.com':
            originalCurrency = 'USD';

            GM_addStyle(`
                .Bisko_CC_Menu {
                    float: right;
                    margin-top: 5px;
                    padding: 1px 5px;
                    vertical-align: middle;
                    background-image: linear-gradient(#4E4E4E, #101112 40%, #191b1d);
                    font-size: 12px;
                    line-height: 1.5;
                    border-radius: 3px;
                    border-bottom-right-radius: 0;
                    border-top-right-radius: 0;
                }
                .Bisko_CC_Menu > a { color: #f89406; }
            `);

            $('.top-bar > .container > form').after(
                constructMenu()
                    .wrapInner('<div class="Bisko_CC_Menu top_menu"></div>')
                    .children()
                    .unwrap(),
            );
            // homepage
            $('.album__container p > span:first-of-type').bindPriceData();
            // store page
            $('.product-box s, .product-box .text-danger').bindPriceData();
            new MutationObserver((mutations) => {
                mutations.forEach((mutation) => {
                    mutation.addedNodes.forEach((addedNode) => {
                        if (addedNode.id === 'productlistview') {
                            $(addedNode).find('.product-box s, .product-box .text-danger').bindPriceData();
                            updateCurrency();
                        }
                    });
                });
            }).observe(document.body, {
                childList: true,
                subtree: true,
            });
            break;
        default:
    }
    // only update prices when appended currencies are selected
    if (originalCurrency !== preferredCurrency()) updateCurrency();
};
const getExchangeRate = () => {
    GM_xmlhttpRequest({
        method: 'GET',
        url: 'http://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml',
        onload: (res) => {
            if (res.status === 200) {
                try {
                    config.exchangeRate = {
                        lastUpdate: Date.now(),
                        rates: {},
                    };

                    res.response.split("\n").forEach((line) => {
                        if (line.includes('currency=')) {
                            const currency = line.split('currency=\'').pop().slice(0, 3);
                            const rate = line.trim().split('rate=\'').pop().slice(0, -3);

                            config.exchangeRate.rates[currency] = parseFloat(rate);
                        }
                    });
                    config.exchangeRate.rates.EUR = 1;

                    // get NTD
                    GM_xmlhttpRequest({
                        method: 'GET',
                        url: 'https://www.google.com/search?q=1+EUR+%3D+NTD',
                        onload: (searchRes) => {
                            const rate = parseFloat(searchRes.response.split('<div class="vk_ans vk_bk">').pop().slice(0, 7).trim());
                            const NTDRate = isNaN(rate) ? config.exchangeRate.rates.HKD * 3.75 : rate;

                            config.exchangeRate.rates.NTD = NTDRate;
                            GM_setValue('Bisko_CC', JSON.stringify(config));
                        },
                        onerror: () => {
                            config.exchangeRate.rates.NTD = config.exchangeRate.rates.HKD * 3.75;
                        },
                    });

                    handler();
                } catch (e) {
                    swal(
                        'Parsing Failed',
                        'An error occured when parsing exchange rate data, please reload to try again',
                        'error',
                    );
                }
            } else {
                swal(
                    'Loading Failed',
                    'Unable to fetch exchange rate data, please reload to try again',
                    'error',
                );
            }
        },
    });
};

$(() => {
    if (Object.keys(config).length === 0) getExchangeRate(); // first installed
    else if (Date.now() - interval > config.lastUpdate) getExchangeRate(); // update exchange rate
    else handler();
});