Currency Converter

try to take over the world!

// ==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();
});