Currency Converter

try to take over the world!

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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();
});